diff options
Diffstat (limited to 'build/integration/features/bootstrap')
40 files changed, 1690 insertions, 1021 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 740a8b169a6..e8580ed537b 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Hook\Scope\AfterScenarioScope; use Behat\Behat\Hook\Scope\BeforeScenarioScope; diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php index a0b02e2b64b..aeaade85383 100644 --- a/build/integration/features/bootstrap/Auth.php +++ b/build/integration/features/bootstrap/Auth.php @@ -1,34 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Phil Davis <phil.davis@inf.org> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use GuzzleHttp\Client; +use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; -use GuzzleHttp\Cookie\CookieJar; require __DIR__ . '/../../vendor/autoload.php'; @@ -224,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, [ @@ -243,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 6b8e5d88092..beebf1c024a 100644 --- a/build/integration/features/bootstrap/Avatar.php +++ b/build/integration/features/bootstrap/Avatar.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com) - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Gherkin\Node\TableNode; use PHPUnit\Framework\Assert; @@ -26,7 +10,7 @@ use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; trait Avatar { - /** @var string **/ + /** @var string * */ private $lastAvatar; /** @AfterScenario **/ @@ -257,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; } @@ -268,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 9060c85c756..59a4312913e 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -1,40 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -45,6 +20,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; @@ -147,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; } /** @@ -179,7 +159,7 @@ trait BasicStructure { $options['auth'] = [$this->currentUser, $this->regularUser]; } $options['headers'] = [ - 'OCS_APIREQUEST' => 'true' + 'OCS-APIRequest' => 'true' ]; if ($body instanceof TableNode) { $fd = $body->getRowsHash(); @@ -197,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(); } } @@ -212,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(); @@ -306,7 +288,8 @@ trait BasicStructure { * @param string $user */ public function loggingInUsingWebAs($user) { - $loginUrl = substr($this->baseUrl, 0, -5) . '/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( @@ -329,6 +312,9 @@ trait BasicStructure { 'requesttoken' => $this->requestToken, ], 'cookies' => $this->cookieJar, + 'headers' => [ + 'Origin' => $baseUrl, + ], ] ); $this->extracRequestTokenFromResponse($response); @@ -354,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(); @@ -442,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); } @@ -485,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"); } /** @@ -505,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'); } } @@ -524,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) { @@ -536,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 49d8c8e5963..459c35089fa 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -1,36 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Phil Davis <phil.davis@inf.org> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ require __DIR__ . '/../../vendor/autoload.php'; use GuzzleHttp\Client; +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; @@ -60,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, @@ -105,6 +87,119 @@ class CalDavContext implements \Behat\Behat\Context\Context { } /** + * @When :user requests principal :principal on the endpoint :endpoint + */ + public function requestsPrincipal(string $user, string $principal, string $endpoint): void { + $davUrl = $this->baseUrl . $endpoint . $principal; + + $password = ($user === 'admin') ? 'admin' : '123456'; + try { + $this->response = $this->client->request( + 'PROPFIND', + $davUrl, + [ + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + 'Depth' => 0, + ], + 'body' => '<x0:propfind xmlns:x0="DAV:"><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:principal-collection-set/><x0:supported-report-set/></x0:prop></x0:propfind>', + 'auth' => [ + $user, + $password, + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @Then The CalDAV response should contain a property :key + * @throws \Exception + */ + public function theCaldavResponseShouldContainAProperty(string $key): void { + /** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */ + $multiStatus = $this->responseXml['value']; + $responses = $multiStatus->getResponses()[0]->getResponseProperties(); + if (!isset($responses[200])) { + throw new \Exception( + sprintf( + 'Expected code 200 got [%s]', + implode(',', array_keys($responses)), + ) + ); + } + + $props = $responses[200]; + if (!array_key_exists($key, $props)) { + throw new \Exception( + sprintf( + 'Expected property %s in %s', + $key, + json_encode($props, JSON_PRETTY_PRINT), + ) + ); + } + } + + /** + * @Then The CalDAV response should contain a property :key with a href value :value + * @throws \Exception + */ + public function theCaldavResponseShouldContainAPropertyWithHrefValue( + string $key, + string $value, + ): void { + /** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */ + $multiStatus = $this->responseXml['value']; + $responses = $multiStatus->getResponses()[0]->getResponseProperties(); + if (!isset($responses[200])) { + throw new \Exception( + sprintf( + 'Expected code 200 got [%s]', + implode(',', array_keys($responses)), + ) + ); + } + + $props = $responses[200]; + if (!array_key_exists($key, $props)) { + throw new \Exception("Cannot find property \"$key\""); + } + + $actualValue = $props[$key]->getHref(); + if ($actualValue !== $value) { + throw new \Exception("Property \"$key\" found with value \"$actualValue\", expected \"$value\""); + } + } + + /** + * @Then The CalDAV response should be multi status + * @throws \Exception + */ + public function theCaldavResponseShouldBeMultiStatus(): void { + if ($this->response->getStatusCode() !== 207) { + throw new \Exception( + sprintf( + 'Expected code 207 got %s', + $this->response->getStatusCode() + ) + ); + } + + $body = $this->response->getBody()->getContents(); + if ($body && substr($body, 0, 1) === '<') { + $reader = new Sabre\Xml\Reader(); + $reader->xml($body); + $reader->elementMap['{DAV:}multistatus'] = \Sabre\DAV\Xml\Response\MultiStatus::class; + $reader->elementMap['{DAV:}response'] = \Sabre\DAV\Xml\Element\Response::class; + $reader->elementMap['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'] = \Sabre\DAV\Xml\Property\Href::class; + $this->responseXml = $reader->parse(); + } + } + + /** * @Then The CalDAV HTTP status code should be :code * @param int $code * @throws \Exception @@ -172,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( @@ -195,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( @@ -233,4 +328,63 @@ class CalDavContext implements \Behat\Behat\Context\Context { ); } } + + /** + * @When :user sends a create calendar request to :calendar on the endpoint :endpoint + */ + public function sendsCreateCalendarRequest(string $user, string $calendar, string $endpoint) { + $davUrl = $this->baseUrl . $endpoint . $calendar; + $password = ($user === 'admin') ? 'admin' : '123456'; + + try { + $this->response = $this->client->request( + 'MKCALENDAR', + $davUrl, + [ + 'body' => '<c:mkcalendar xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:" xmlns:a="http://apple.com/ns/ical/" xmlns:o="http://owncloud.org/ns"><d:set><d:prop><d:displayname>test</d:displayname><o:calendar-enabled>1</o:calendar-enabled><a:calendar-color>#21213D</a:calendar-color><c:supported-calendar-component-set><c:comp name="VEVENT"/></c:supported-calendar-component-set></d:prop></d:set></c:mkcalendar>', + 'auth' => [ + $user, + $password, + ], + ] + ); + } catch (GuzzleException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @Given :user updates property :key to href :value of principal :principal on the endpoint :endpoint + */ + public function updatesHrefPropertyOfPrincipal( + string $user, + string $key, + string $value, + string $principal, + string $endpoint, + ): void { + $davUrl = $this->baseUrl . $endpoint . $principal; + $password = ($user === 'admin') ? 'admin' : '123456'; + + $propPatch = new \Sabre\DAV\Xml\Request\PropPatch(); + $propPatch->properties = [$key => new \Sabre\DAV\Xml\Property\Href($value)]; + + $xml = new \Sabre\Xml\Service(); + $body = $xml->write('{DAV:}propertyupdate', $propPatch, '/'); + + $this->response = $this->client->request( + 'PROPPATCH', + $davUrl, + [ + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + ], + 'body' => $body, + 'auth' => [ + $user, + $password, + ], + ] + ); + } } diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index 4fdfb3e41b0..7d09ab6ddcf 100644 --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ b/build/integration/features/bootstrap/CapabilitiesContext.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; @@ -44,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']); @@ -54,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 18a9f3dd249..733c98dca02 100644 --- a/build/integration/features/bootstrap/CardDavContext.php +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -1,35 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Phil Davis <phil.davis@inf.org> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ require __DIR__ . '/../../vendor/autoload.php'; use GuzzleHttp\Client; +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; @@ -128,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( @@ -141,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>', @@ -208,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( @@ -241,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 { @@ -267,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 { @@ -311,4 +294,64 @@ class CardDavContext implements \Behat\Behat\Context\Context { } } } + + /** + * @When :user sends a create addressbook request to :addressbook on the endpoint :endpoint + */ + public function sendsCreateAddressbookRequest(string $user, string $addressbook, string $endpoint) { + $davUrl = $this->baseUrl . $endpoint . $addressbook; + $password = ($user === 'admin') ? 'admin' : '123456'; + + try { + $this->response = $this->client->request( + 'MKCOL', + $davUrl, + [ + 'body' => '<d:mkcol xmlns:card="urn:ietf:params:xml:ns:carddav" + xmlns:d="DAV:"> + <d:set> + <d:prop> + <d:resourcetype> + <d:collection />,<card:addressbook /> + </d:resourcetype>,<d:displayname>' . $addressbook . '</d:displayname> + </d:prop> + </d:set> + </d:mkcol>', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml;charset=UTF-8', + ], + ] + ); + } catch (GuzzleException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @Then The CardDAV HTTP status code should be :code + * @param int $code + * @throws \Exception + */ + public function theCarddavHttpStatusCodeShouldBe($code) { + if ((int)$code !== $this->response->getStatusCode()) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + (int)$code, + $this->response->getStatusCode() + ) + ); + } + + $body = $this->response->getBody()->getContents(); + if ($body && substr($body, 0, 1) === '<') { + $reader = new Sabre\Xml\Reader(); + $reader->xml($body); + $this->responseXml = $reader->parse(); + } + } } diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index ae44fcb1503..c8abf91127e 100644 --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ b/build/integration/features/bootstrap/ChecksumsContext.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Phil Davis <phil.davis@inf.org> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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'; @@ -30,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; @@ -107,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()); } } @@ -151,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']); } } @@ -179,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]); } } @@ -219,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); } } @@ -228,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 adfc357b0e1..27fa1795c5d 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; @@ -73,6 +56,9 @@ class CollaborationContext implements Context { if (isset($expected['source'])) { $data['source'] = $suggestion['source']; } + if (isset($expected['status'])) { + $data['status'] = json_encode($suggestion['status']); + } return $data; }, $suggestions, $formData->getHash())); } @@ -85,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(); @@ -107,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 cba254551e0..924d723daa6 100644 --- a/build/integration/features/bootstrap/CommandLine.php +++ b/build/integration/features/bootstrap/CommandLine.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ use PHPUnit\Framework\Assert; diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php index afe17cd75a4..e7764356270 100644 --- a/build/integration/features/bootstrap/CommandLineContext.php +++ b/build/integration/features/bootstrap/CommandLineContext.php @@ -1,31 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Stefan Weil <sw@weilnetz.de> - * @author Sujith H <sharidasan@owncloud.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ require __DIR__ . '/../../vendor/autoload.php'; +use Behat\Behat\Context\Exception\ContextNotFoundException; use Behat\Behat\Hook\Scope\BeforeScenarioScope; use PHPUnit\Framework\Assert; @@ -35,6 +17,8 @@ class CommandLineContext implements \Behat\Behat\Context\Context { private $lastTransferPath; private $featureContext; + private $localBaseUrl; + private $remoteBaseUrl; public function __construct($ocPath, $baseUrl) { $this->ocPath = rtrim($ocPath, '/') . '/'; @@ -59,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) { @@ -69,7 +57,7 @@ class CommandLineContext implements \Behat\Behat\Context\Context { foreach ($results as $path => $data) { $path = rawurldecode($path); $parts = explode(' ', $path); - if (basename($parts[0]) !== 'transferred') { + if (basename($parts[0]) !== 'Transferred') { continue; } if (isset($parts[2]) && $parts[2] === $sourceUser) { @@ -122,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 ad2d752b4dd..53001b1c204 100644 --- a/build/integration/features/bootstrap/CommentsContext.php +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ require __DIR__ . '/../../vendor/autoload.php'; @@ -49,6 +30,35 @@ class CommentsContext implements \Behat\Behat\Context\Context { } } + /** + * get a named entry from response instead of picking a random entry from values + * + * @param string $path + * + * @return array|string + * @throws Exception + */ + private function getValueFromNamedEntries(string $path, array $response): mixed { + $next = ''; + if (str_contains($path, ' ')) { + [$key, $next] = explode(' ', $path, 2); + } else { + $key = $path; + } + + foreach ($response as $entry) { + if ($entry['name'] === $key) { + if ($next !== '') { + return $this->getValueFromNamedEntries($next, $entry['value']); + } else { + return $entry['value']; + } + } + } + + return null; + } + /** @AfterScenario */ public function teardownScenario() { $client = new \GuzzleHttp\Client(); @@ -127,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() . ')'); } } @@ -169,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->response[0]['value'][2]['value'][0]['value'][0]['value'] ?? 0); + $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); } } @@ -227,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() . ')'); } } @@ -238,7 +248,8 @@ class CommentsContext implements \Behat\Behat\Context\Context { * @throws \Exception */ public function theResponseShouldContainAPropertyWithValue($key, $value) { - $keys = $this->response[0]['value'][2]['value'][0]['value']; + // $keys = $this->response[0]['value'][1]['value'][0]['value']; + $keys = $this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop', $this->response); $found = false; foreach ($keys as $singleKey) { if ($singleKey['name'] === '{http://owncloud.org/ns}' . substr($key, 3)) { @@ -263,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 . ')'); } } @@ -293,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 0506d827a39..f6bf6b9422b 100644 --- a/build/integration/features/bootstrap/ContactsMenu.php +++ b/build/integration/features/bootstrap/ContactsMenu.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021 Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use PHPUnit\Framework\Assert; 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 e5e6dc64853..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,32 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.com) - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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 **/ @@ -38,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); @@ -56,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' ); } @@ -71,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' ); } @@ -95,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']; @@ -115,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' ); } @@ -135,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 9d3b5979114..32387869edd 100644 --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ // Code below modified from https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/fakeSMTP.php, // which is under the MIT license (https://github.com/axllent/fake-smtp/blob/f0856f8a0df6f4ca5a573cf31428c09ebc5b9ea3/LICENSE) @@ -52,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); @@ -78,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))) { @@ -88,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'); @@ -97,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) { @@ -106,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]; @@ -127,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 a3a600d6625..ab37556f931 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -1,40 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu> - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; 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 423708adc10..95dc8119ad6 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -1,34 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; @@ -60,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 $!'); } /** @@ -86,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); @@ -107,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); @@ -156,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; @@ -174,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); } /** @@ -190,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 a5d4dad14e3..0c437f28a72 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; @@ -35,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) { @@ -45,12 +26,16 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . '/public.php/webdav' . $path; + $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path"); - $options['auth'] = [$token, '']; $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 { @@ -60,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) { @@ -73,17 +67,28 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . '/public.php/webdav/' . $folder; + $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder"); - $options['auth'] = [$token, '']; $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 e0315bce84e..986dced77a1 100644 --- a/build/integration/features/bootstrap/LDAPContext.php +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; @@ -104,7 +85,7 @@ class LDAPContext implements Context { $this->asAn('admin'); $this->creatingAnLDAPConfigurationAt('/apps/user_ldap/api/v1/config'); $data = new TableNode([ - ['configData[ldapHost]', 'openldap'], + ['configData[ldapHost]', getenv('LDAP_HOST') ?: 'openldap'], ['configData[ldapPort]', '389'], ['configData[ldapBase]', 'dc=nextcloud,dc=ci'], ['configData[ldapAgentName]', 'cn=admin,dc=nextcloud,dc=ci'], @@ -141,6 +122,9 @@ class LDAPContext implements Context { $this->asAn('admin'); $configData = $table->getRows(); foreach ($configData as &$row) { + if (str_contains($row[0], 'Host') && getenv('LDAP_HOST')) { + $row[1] = str_replace('openldap', getenv('LDAP_HOST'), $row[1]); + } $row[0] = 'configData[' . $row[0] . ']'; } $this->settingTheLDAPConfigurationTo(new TableNode($configData)); diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index c2d9e86275c..d48ed6399c5 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com) - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ trait Mail { // CommandLine trait is expected to be used in the class that uses this @@ -37,7 +21,7 @@ trait Mail { return; } - exec("kill " . $this->fakeSmtpServerPid); + exec('kill ' . $this->fakeSmtpServerPid); $this->invokingTheCommand('config:system:delete mail_smtpport'); } @@ -50,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 7fb4188f919..935ad2a4a1d 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -1,36 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Jonas Meurer <jonas@freesources.org> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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; @@ -61,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; } @@ -78,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; } @@ -99,7 +75,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->currentUser = $previous_user; try { @@ -151,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"; @@ -172,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]); } } } @@ -186,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"; @@ -218,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"; @@ -248,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 = []; @@ -277,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; @@ -285,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; @@ -293,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; @@ -301,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; @@ -370,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); @@ -549,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; } @@ -570,7 +577,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->currentUser = $previous_user; try { @@ -651,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); @@ -664,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); @@ -677,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); @@ -691,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); @@ -704,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); @@ -717,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); @@ -730,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); @@ -802,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') { @@ -823,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') { @@ -847,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') { @@ -900,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); } /** @@ -909,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); } /** @@ -977,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 a3e5e1b5007..6102f686ea7 100644 --- a/build/integration/features/bootstrap/RemoteContext.php +++ b/build/integration/features/bootstrap/RemoteContext.php @@ -1,28 +1,11 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; +use OCP\Http\Client\IClientService; use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; @@ -50,7 +33,7 @@ class RemoteContext implements Context { } protected function getApiClient() { - return new \OC\Remote\Api\OCS($this->remoteInstance, $this->credentails, \OC::$server->getHTTPClientService()); + return new \OC\Remote\Api\OCS($this->remoteInstance, $this->credentails, \OC::$server->get(IClientService::class)); } /** @@ -59,14 +42,14 @@ 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'; } $this->lastException = null; try { - $this->remoteInstance = new \OC\Remote\Instance($baseUri, \OC::$server->getMemCacheFactory()->createLocal(), \OC::$server->getHTTPClientService()); + $this->remoteInstance = new \OC\Remote\Instance($baseUri, \OC::$server->getMemCacheFactory()->createLocal(), \OC::$server->get(IClientService::class)); // trigger the status request $this->remoteInstance->getProtocol(); } catch (\Exception $e) { 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 c9d5f75a1d3..49a4fe92822 100644 --- a/build/integration/features/bootstrap/Search.php +++ b/build/integration/features/bootstrap/Search.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.com) - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Gherkin\Node\TableNode; use PHPUnit\Framework\Assert; diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php index 5abdb22ccfc..aa131cec597 100644 --- a/build/integration/features/bootstrap/SetupContext.php +++ b/build/integration/features/bootstrap/SetupContext.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Morris Jobke - * - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index 70e78e24929..37e0e63e547 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 2a6a509d65f..0cc490ff110 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -1,35 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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; @@ -81,13 +55,19 @@ trait Sharing { $fd = $body->getRowsHash(); if (array_key_exists('expireDate', $fd)) { $dateModification = $fd['expireDate']; - $fd['expireDate'] = date('Y-m-d', strtotime($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(); } @@ -123,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'); } @@ -143,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; @@ -159,7 +139,7 @@ trait Sharing { } else { $url = $this->lastShareData->data->url; } - $fullUrl = $url . "/download"; + $fullUrl = $url . '/download'; $this->checkDownload($fullUrl, null, 'text/plain'); } @@ -173,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'); } @@ -187,8 +167,8 @@ trait Sharing { $token = $this->lastShareData->data->token; } - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav"; - $this->checkDownload($fullUrl, [$token, $password], 'text/plain'); + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/"; + $this->checkDownload($fullUrl, ['', $password], 'text/plain'); } private function checkDownload($url, $auth = null, $mimeType = null) { @@ -219,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 = []; @@ -228,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()); } @@ -239,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 = [ @@ -263,20 +243,20 @@ 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(); } } public function createShare($user, - $path = null, - $shareType = null, - $shareWith = null, - $publicUpload = null, - $password = null, - $permissions = null, - $viewOnly = false) { + $path = null, + $shareType = null, + $shareWith = null, + $publicUpload = null, + $password = null, + $permissions = null, + $viewOnly = false) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares"; $client = new Client(); $options = [ @@ -311,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(); @@ -328,16 +308,18 @@ trait Sharing { public function isFieldInResponse($field, $contentExpected) { $data = simplexml_load_string($this->response->getBody())->data[0]; if ((string)$field == 'expiration') { - $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 { @@ -347,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 ($data->$field == $contentExpected) { + } 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; } @@ -478,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); } /** @@ -487,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); } /** @@ -519,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"); @@ -580,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 { @@ -626,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 f187e89f08f..a9dd99108a9 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; @@ -36,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'); @@ -46,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 ecef9c08b1e..c64626de68d 100644 --- a/build/integration/features/bootstrap/TagsContext.php +++ b/build/integration/features/bootstrap/TagsContext.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Phil Davis <phil.davis@inf.org> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ require __DIR__ . '/../../vendor/autoload.php'; @@ -267,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) ) @@ -277,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 5417c22a058..6f351c30ccf 100644 --- a/build/integration/features/bootstrap/TalkContext.php +++ b/build/integration/features/bootstrap/TalkContext.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com) - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ use Behat\Behat\Context\Context; 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 19e9b57c3fb..dfcc23289a7 100644 --- a/build/integration/features/bootstrap/Trashbin.php +++ b/build/integration/features/bootstrap/Trashbin.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH. - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ use DMS\PHPUnitExtensions\ArraySubset\Assert as AssertArraySubset; use PHPUnit\Framework\Assert; @@ -115,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 680db01a260..2cb37002ac0 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -1,39 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016 Sergio Bertolin <sbertolin@solidgear.es> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author David Toledo <dtoledo@solidgear.es> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolin <sbertolin@solidgear.es> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ + use GuzzleHttp\Client as GClient; -use GuzzleHttp\Message\ResponseInterface; use PHPUnit\Framework\Assert; +use Psr\Http\Message\ResponseInterface; use Sabre\DAV\Client as SClient; use Sabre\DAV\Xml\Property\ResourceType; @@ -43,16 +18,17 @@ require __DIR__ . '/../../vendor/autoload.php'; trait WebDav { use Sharing; - /** @var string */ - private $davPath = "remote.php/webdav"; - /** @var boolean */ - private $usingOldDavPath = true; + 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; /** @var ResponseInterface */ private $response; - /** @var array map with user as key and another map as value, which has path as key and etag as value */ - private $storedETAG = null; - /** @var int */ - private $storedFileID = null; + private array $parsedResponse = []; + private string $s3MultipartDestination; + private string $uploadId; + /** @var string[] */ + private array $parts = []; /** * @Given /^using dav path "([^"]*)"$/ @@ -65,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; } @@ -73,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; } @@ -85,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 = [ @@ -100,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); @@ -115,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()); } @@ -129,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(); } @@ -159,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); } /** @@ -168,16 +152,15 @@ trait WebDav { */ public function downloadPublicFileWithRange($range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav"; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token"; $client = new GClient(); $options = []; - $options['auth'] = [$token, ""]; $options['headers'] = [ 'Range' => $range ]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -186,7 +169,7 @@ trait WebDav { */ public function downloadPublicFileInsideAFolderWithRange($path, $range) { $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav" . "$path"; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$path"; $client = new GClient(); $options = [ @@ -194,9 +177,8 @@ trait WebDav { 'Range' => $range ] ]; - $options['auth'] = [$token, ""]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -216,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); } @@ -229,6 +211,24 @@ trait WebDav { } /** + * @Then /^Favorite search should work$/ + */ + public function searchFavorite(): void { + $this->searchFile( + $this->currentUser, + '<oc:favorite/>', + null, + '<d:eq> + <d:prop> + <oc:favorite/> + </d:prop> + <d:literal>yes</d:literal> + </d:eq>' + ); + Assert::assertEquals(207, $this->response->getStatusCode()); + } + + /** * @Then /^Downloaded content when downloading file "([^"]*)" with range "([^"]*)" should be "([^"]*)"$/ * @param string $fileSource * @param string $range @@ -240,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 */ @@ -252,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 @@ -313,18 +380,31 @@ trait WebDav { } /** + * @Then the response should be empty + * @throws \Exception + */ + public function theResponseShouldBeEmpty(): void { + $response = ($this->response instanceof ResponseInterface) ? $this->convertResponseToDavEntries() : $this->response; + if ($response === []) { + return; + } + + throw new \Exception('response is not empty'); + } + + /** * @Then the single response should contain a property :key with value :value * @param string $key * @param string $expectedValue * @throws \Exception */ public function theSingleResponseShouldContainAPropertyWithValue($key, $expectedValue) { - $keys = $this->response; - if (!array_key_exists($key, $keys)) { + $response = ($this->response instanceof ResponseInterface) ? $this->convertResponseToDavSingleEntry() : $this->response; + if (!array_key_exists($key, $response)) { throw new \Exception("Cannot find property \"$key\" with \"$expectedValue\""); } - $value = $keys[$key]; + $value = $response[$key]; if ($value instanceof ResourceType) { $value = $value->getValue(); if (empty($value)) { @@ -344,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 = []; @@ -445,28 +525,28 @@ trait WebDav { </d:prop> <d:literal>image/png</d:literal> </d:eq> - + <d:eq> <d:prop> <d:getcontenttype/> </d:prop> <d:literal>image/jpeg</d:literal> </d:eq> - + <d:eq> <d:prop> <d:getcontenttype/> </d:prop> <d:literal>image/heic</d:literal> </d:eq> - + <d:eq> <d:prop> <d:getcontenttype/> </d:prop> <d:literal>video/mp4</d:literal> </d:eq> - + <d:eq> <d:prop> <d:getcontenttype/> @@ -509,9 +589,10 @@ trait WebDav { </d:searchrequest>'; try { - $this->response = $this->makeDavRequest($user, "SEARCH", '', [ + $this->response = $this->makeDavRequest($user, 'SEARCH', '', [ 'Content-Type' => 'text/xml' ], $body, ''); + var_dump((string)$this->response->getBody()); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception @@ -550,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); } } @@ -583,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'); } } } @@ -600,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(); @@ -617,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); } @@ -632,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(); @@ -668,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(); @@ -679,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 @@ -704,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); @@ -738,21 +804,22 @@ 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); } /** * @Given user :user creates a new chunking upload with id :id */ 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'); } /** @@ -761,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'); } /** @@ -772,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'); } /** @@ -786,12 +853,66 @@ 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(); } } + + /** + * @Given user :user creates a new chunking v2 upload with id :id and destination :targetDestination + */ + public function userCreatesANewChunkingv2UploadWithIdAndDestination($user, $id, $targetDestination) { + $this->s3MultipartDestination = $this->getTargetDestination($user, $targetDestination); + $this->newUploadId(); + $destination = '/uploads/' . $user . '/' . $this->getUploadId($id); + $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [ + 'Destination' => $this->s3MultipartDestination, + ], null, 'uploads'); + } + + /** + * @Given user :user uploads new chunk v2 file :num to id :id + */ + public function userUploadsNewChunkv2FileToIdAndDestination($user, $num, $id) { + $data = \GuzzleHttp\Psr7\Utils::streamFor(fopen('/tmp/part-upload-' . $num, 'r')); + $destination = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/' . $num; + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [ + 'Destination' => $this->s3MultipartDestination + ], $data, 'uploads'); + } + + /** + * @Given user :user moves new chunk v2 file with id :id + */ + public function userMovesNewChunkv2FileWithIdToMychunkedfileAndDestination($user, $id) { + $source = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/.file'; + try { + $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ + 'Destination' => $this->s3MultipartDestination, + ], null, 'uploads'); + } catch (\GuzzleHttp\Exception\ServerException $e) { + // 5xx responses cause a server exception + $this->response = $e->getResponse(); + } catch (\GuzzleHttp\Exception\ClientException $e) { + // 4xx responses cause a client exception + $this->response = $e->getResponse(); + } + } + + private function getTargetDestination(string $user, string $destination): string { + return substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $destination; + } + + private function getUploadId(string $id): string { + return $id . '-' . $this->uploadId; + } + + private function newUploadId() { + $this->uploadId = (string)time(); + } + /** * @Given /^Downloading file "([^"]*)" as "([^"]*)"$/ */ @@ -897,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() { @@ -924,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'); } } } @@ -941,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); } } @@ -957,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']; } @@ -980,4 +1118,88 @@ trait WebDav { $currentFileID = $this->getFileIdForPath($user, $path); Assert::assertEquals($currentFileID, $this->storedFileID); } + + /** + * @Given /^user "([^"]*)" creates a file locally with "([^"]*)" x 5 MB chunks$/ + */ + public function userCreatesAFileLocallyWithChunks($arg1, $chunks) { + $this->parts = []; + for ($i = 1;$i <= (int)$chunks;$i++) { + $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; + } + } + + /** + * @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); + file_put_contents('/tmp/part-upload-' . $id, str_repeat($randomletter, (int)$size * 1024 * 1024)); + $this->parts[] = '/tmp/part-upload-' . $id; + } + + /** + * @Then /^Downloaded content should be the created file$/ + */ + public function downloadedContentShouldBeTheCreatedFile() { + $content = ''; + sort($this->parts); + foreach ($this->parts as $part) { + $content .= file_get_contents($part); + } + Assert::assertEquals($content, (string)$this->response->getBody()); + } + + /** + * @Then /^the S3 multipart upload was successful with status "([^"]*)"$/ + */ + public function theSmultipartUploadWasSuccessful($status) { + Assert::assertEquals((int)$status, $this->response->getStatusCode()); + } + + /** + * @Then /^the upload should fail on object storage$/ + */ + public function theUploadShouldFailOnObjectStorage() { + $descriptor = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open('php occ config:system:get objectstore --no-ansi', $descriptor, $pipes, '../../'); + $lastCode = proc_close($process); + if ($lastCode === 0) { + $this->theHTTPStatusCodeShouldBe(500); + } + } + + /** + * @return array + * @throws Exception + */ + private function convertResponseToDavSingleEntry(): array { + $results = $this->convertResponseToDavEntries(); + if (count($results) > 1) { + throw new \Exception('result is empty or contain more than one (1) entry'); + } + + return array_shift($results); + } + + /** + * @return array + */ + private function convertResponseToDavEntries(): array { + $client = $this->getSabreClient($this->currentUser); + $parsedResponse = $client->parseMultiStatus((string)$this->response->getBody()); + + $results = []; + foreach ($parsedResponse as $href => $statusList) { + $results[$href] = $statusList[200] ?? []; + } + + return $results; + } } |