diff options
Diffstat (limited to 'build')
57 files changed, 745 insertions, 302 deletions
diff --git a/build/ca-bundle-etag.txt b/build/ca-bundle-etag.txt index f09d49137b5..a3148994b07 100644 --- a/build/ca-bundle-etag.txt +++ b/build/ca-bundle-etag.txt @@ -1 +1 @@ -"38f2f-62eefa8c2e7b3" +"366fb-635889cca07d9" diff --git a/build/files-checker.php b/build/files-checker.php index 4cbd0874247..4dfc7267e23 100644 --- a/build/files-checker.php +++ b/build/files-checker.php @@ -11,6 +11,7 @@ $expectedFiles = [ '..', '.devcontainer', '.editorconfig', + '.envrc', '.eslintignore', '.eslintrc.js', '.git', @@ -61,6 +62,8 @@ $expectedFiles = [ 'cypress.d.ts', 'cypress', 'dist', + 'flake.lock', + 'flake.nix', 'index.html', 'index.php', 'lib', diff --git a/build/gen-coverage-badge.php b/build/gen-coverage-badge.php index 73d2a37e025..602ab83b281 100644 --- a/build/gen-coverage-badge.php +++ b/build/gen-coverage-badge.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 45db5105838..0a3fe4fd823 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -80,6 +80,8 @@ default: - CommandLineContext: baseUrl: http://localhost:8080 ocPath: ../../ + - PrincipalPropertySearchContext: + baseUrl: http://localhost:8080 federation: paths: - "%paths.base%/../federation_features" @@ -253,4 +255,24 @@ default: admin: - admin - admin - regular_user_password: 123456
\ No newline at end of file + regular_user_password: 123456 + routing: + paths: + - "%paths.base%/../routing_features" + contexts: + - RoutingContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 + theming: + paths: + - "%paths.base%/../theming_features" + contexts: + - FeatureContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 diff --git a/build/integration/dav_features/carddav.feature b/build/integration/dav_features/carddav.feature index f1ea46af886..35e85639817 100644 --- a/build/integration/dav_features/carddav.feature +++ b/build/integration/dav_features/carddav.feature @@ -49,7 +49,6 @@ Feature: carddav |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| Scenario: Exporting the picture of ones own contact Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" @@ -63,7 +62,6 @@ Feature: carddav |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| Scenario: Create addressbook request for non-existing addressbook of another user Given user "user0" exists diff --git a/build/integration/dav_features/dav-v2.feature b/build/integration/dav_features/dav-v2.feature index 9eae9a1b5fd..dbd2295497f 100644 --- a/build/integration/dav_features/dav-v2.feature +++ b/build/integration/dav_features/dav-v2.feature @@ -41,7 +41,6 @@ Feature: dav-v2 |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| And Downloaded content should start with "Welcome to your Nextcloud account!" Scenario: Doing a GET with a web login should work without CSRF token on the new backend diff --git a/build/integration/dav_features/principal-property-search.feature b/build/integration/dav_features/principal-property-search.feature new file mode 100644 index 00000000000..b2195489263 --- /dev/null +++ b/build/integration/dav_features/principal-property-search.feature @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: principal-property-search + Background: + Given user "user0" exists + Given As an "admin" + Given invoking occ with "app:enable --force testing" + + Scenario: Find a principal by a given displayname + When searching for a principal matching "user0" + Then The search HTTP status code should be "207" + And The search response should contain "<d:href>/remote.php/dav/principals/users/user0/</d:href>" diff --git a/build/integration/dav_features/webdav-related.feature b/build/integration/dav_features/webdav-related.feature index f439330838c..12fd3d44c4f 100644 --- a/build/integration/dav_features/webdav-related.feature +++ b/build/integration/dav_features/webdav-related.feature @@ -291,7 +291,6 @@ Feature: webdav-related |X-Frame-Options|SAMEORIGIN| |X-Permitted-Cross-Domain-Policies|none| |X-Robots-Tag|noindex, nofollow| - |X-XSS-Protection|1; mode=block| And Downloaded content should start with "Welcome to your Nextcloud account!" Scenario: Doing a GET with a web login should work without CSRF token on the old backend diff --git a/build/integration/features/bootstrap/AppConfiguration.php b/build/integration/features/bootstrap/AppConfiguration.php index 5f39c58ffeb..e8580ed537b 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php index ffc391c0d45..beebf1c024a 100644 --- a/build/integration/features/bootstrap/Avatar.php +++ b/build/integration/features/bootstrap/Avatar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -240,10 +241,10 @@ trait Avatar { } private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { - if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && - $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && - $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && - $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) + && $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) + && $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) + && $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { return true; } @@ -251,8 +252,8 @@ trait Avatar { } private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { - if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && - $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) + && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { return true; } diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index a8c232d6fe7..59a4312913e 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -19,6 +20,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index 80f8c53fc4e..459c35089fa 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index ae32056e17f..7d09ab6ddcf 100644 --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ b/build/integration/features/bootstrap/CapabilitiesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php index a59f0d56f96..733c98dca02 100644 --- a/build/integration/features/bootstrap/CardDavContext.php +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index 3392f8545d9..c8abf91127e 100644 --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ b/build/integration/features/bootstrap/ChecksumsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php index 84b3dfd447f..924d723daa6 100644 --- a/build/integration/features/bootstrap/CommandLine.php +++ b/build/integration/features/bootstrap/CommandLine.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php index 5ea8d12a970..e7764356270 100644 --- a/build/integration/features/bootstrap/CommandLineContext.php +++ b/build/integration/features/bootstrap/CommandLineContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -109,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 17795a48fb4..53001b1c204 100644 --- a/build/integration/features/bootstrap/CommentsContext.php +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/ContactsMenu.php b/build/integration/features/bootstrap/ContactsMenu.php index 4fc3c03c5e9..f6bf6b9422b 100644 --- a/build/integration/features/bootstrap/ContactsMenu.php +++ b/build/integration/features/bootstrap/ContactsMenu.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ConversionsContext.php b/build/integration/features/bootstrap/ConversionsContext.php index 099a2263630..ccd14c460f8 100644 --- a/build/integration/features/bootstrap/ConversionsContext.php +++ b/build/integration/features/bootstrap/ConversionsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/DavFeatureContext.php b/build/integration/features/bootstrap/DavFeatureContext.php index acca52ccafc..ec6085cff98 100644 --- a/build/integration/features/bootstrap/DavFeatureContext.php +++ b/build/integration/features/bootstrap/DavFeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Download.php b/build/integration/features/bootstrap/Download.php index 2a66f7c3d89..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ExternalStorage.php b/build/integration/features/bootstrap/ExternalStorage.php index b1e4c92810b..8fe2653a026 100644 --- a/build/integration/features/bootstrap/ExternalStorage.php +++ b/build/integration/features/bootstrap/ExternalStorage.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php index caf2139faab..32387869edd 100644 --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index c91c5e7cfa3..ab37556f931 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index 38fb6dd6b39..95dc8119ad6 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index 00c2cd346df..0c437f28a72 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -43,8 +44,8 @@ class FilesDropContext implements Context, SnippetAcceptingContext { $this->response = $e->getResponse(); } } - - + + /** * @When Dropping file :path with :content as :nickName */ diff --git a/build/integration/features/bootstrap/LDAPContext.php b/build/integration/features/bootstrap/LDAPContext.php index f0181b36c71..986dced77a1 100644 --- a/build/integration/features/bootstrap/LDAPContext.php +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index d28c24730ba..d48ed6399c5 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/MetadataContext.php b/build/integration/features/bootstrap/MetadataContext.php index 893c08a5467..32042590c86 100644 --- a/build/integration/features/bootstrap/MetadataContext.php +++ b/build/integration/features/bootstrap/MetadataContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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 446ffc0318e..935ad2a4a1d 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/RateLimitingContext.php b/build/integration/features/bootstrap/RateLimitingContext.php index ca198dc5514..15c8c5c8379 100644 --- a/build/integration/features/bootstrap/RateLimitingContext.php +++ b/build/integration/features/bootstrap/RateLimitingContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/RemoteContext.php b/build/integration/features/bootstrap/RemoteContext.php index ae9da4b3614..6102f686ea7 100644 --- a/build/integration/features/bootstrap/RemoteContext.php +++ b/build/integration/features/bootstrap/RemoteContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/RoutingContext.php b/build/integration/features/bootstrap/RoutingContext.php new file mode 100644 index 00000000000..762570547e0 --- /dev/null +++ b/build/integration/features/bootstrap/RoutingContext.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +require __DIR__ . '/../../vendor/autoload.php'; + +class RoutingContext implements Context, SnippetAcceptingContext { + use Provisioning; + use AppConfiguration; + use CommandLine; + + protected function resetAppConfigs(): void { + } +} diff --git a/build/integration/features/bootstrap/Search.php b/build/integration/features/bootstrap/Search.php index 47259be769c..49a4fe92822 100644 --- a/build/integration/features/bootstrap/Search.php +++ b/build/integration/features/bootstrap/Search.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php index 96cb00d8601..aa131cec597 100644 --- a/build/integration/features/bootstrap/SetupContext.php +++ b/build/integration/features/bootstrap/SetupContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index e152a749bfa..37e0e63e547 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 2ea7de3c6e0..039ee7d1121 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -561,18 +562,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 { diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index 8ef617adbfb..fe4d3bb6bf1 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -29,6 +30,7 @@ class SharingContext implements Context, SnippetAcceptingContext { $this->deleteServerConfig('core', 'shareapi_expire_after_n_days'); $this->deleteServerConfig('core', 'link_defaultExpDays'); $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 262271e3710..c64626de68d 100644 --- a/build/integration/features/bootstrap/TagsContext.php +++ b/build/integration/features/bootstrap/TagsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -255,9 +256,9 @@ class TagsContext implements \Behat\Behat\Context\Context { foreach ($table->getRowsHash() as $rowDisplayName => $row) { foreach ($tags as $key => $tag) { if ( - $tag['display-name'] === $rowDisplayName && - $tag['user-visible'] === $row[0] && - $tag['user-assignable'] === $row[1] + $tag['display-name'] === $rowDisplayName + && $tag['user-visible'] === $row[0] + && $tag['user-assignable'] === $row[1] ) { unset($tags[$key]); } diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php index fe248e1af7c..6f351c30ccf 100644 --- a/build/integration/features/bootstrap/TalkContext.php +++ b/build/integration/features/bootstrap/TalkContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 00000000000..f44a6533a1b --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +trait Theming { + + private bool $undoAllThemingChangesAfterScenario = false; + + /** + * @AfterScenario + */ + public function undoAllThemingChanges() { + if (!$this->undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/features/bootstrap/Trashbin.php b/build/integration/features/bootstrap/Trashbin.php index 6c8fd5e4fb6..dfcc23289a7 100644 --- a/build/integration/features/bootstrap/Trashbin.php +++ b/build/integration/features/bootstrap/Trashbin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index e9e08844767..2cb37002ac0 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index bffe7a5887f..6c35e5a68f1 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -187,6 +187,18 @@ Feature: provisioning | timezoneOffset | 0 | | pronouns | NULL | + Scenario: Edit a user with mixed case emails + Given As an "admin" + And user "brand-new-user" exists + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | email | + | value | mixed-CASE@Nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | id | brand-new-user | + | email | mixed-case@nextcloud.com | + Scenario: Edit a user account properties scopes Given user "brand-new-user" exists And As an "brand-new-user" @@ -452,6 +464,7 @@ Feature: provisioning Then groups returned are | España | | admin | + | hidden_group | | new-group | Scenario: create a subadmin diff --git a/build/integration/files_features/transfer-ownership.feature b/build/integration/files_features/transfer-ownership.feature index 4e5407cadbb..6f7a7944166 100644 --- a/build/integration/files_features/transfer-ownership.feature +++ b/build/integration/files_features/transfer-ownership.feature @@ -184,10 +184,10 @@ Feature: transfer-ownership And As an "user2" Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user0" + And as "user1" the folder "/test" exists + And As an "user1" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -210,13 +210,12 @@ Feature: transfer-ownership And user "user1" accepts last share When transferring ownership from "user0" to "user1" And the command was successful - And As an "user1" - Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + Then as "user0" the folder "/test" does not exist + When As an "user1" And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user1" + Then as "user1" the folder "/test" exists + And Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -242,10 +241,10 @@ Feature: transfer-ownership And As an "user2" Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is" And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And using received transfer folder of "user1" as dav path - And as "user1" the folder "/test" does not exist - And As an "user0" + And as "user1" the folder "/test" exists + And As an "user1" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with @@ -253,7 +252,7 @@ Feature: transfer-ownership | uid_file_owner | user3 | | share_with | group1 | - Scenario: transferring ownership does not transfer received shares + Scenario: transferring ownership transfers received shares Given user "user0" exists And user "user1" exists And user "user2" exists @@ -264,16 +263,16 @@ Feature: transfer-ownership And the command was successful And As an "user1" And using received transfer folder of "user1" as dav path - Then as "user1" the folder "/test" does not exist + Then as "user1" the folder "/test" exists And using old dav path - And as "user0" the folder "/test" exists + And as "user0" the folder "/test" does not exist And As an "user2" And Getting info of last share And the OCS status code should be "100" And Share fields of last share match with | uid_owner | user2 | | uid_file_owner | user2 | - | share_with | user0 | + | share_with | user1 | @local_storage Scenario: transferring ownership does not transfer external storage @@ -516,26 +515,6 @@ Feature: transfer-ownership Then the command failed with exit code 1 And the command error output contains the text "Moving a storage (user0/files/test) into another storage (user1) is not allowed" - Scenario: transferring ownership does not transfer received shares - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And User "user2" created a folder "/test" - And User "user0" created a folder "/sub" - And folder "/test" of user "user2" is shared with user "user0" with permissions 31 - And user "user0" accepts last share - And User "user0" moved folder "/test" to "/sub/test" - When transferring ownership of path "sub" from "user0" to "user1" - And the command was successful - And As an "user1" - And using received transfer folder of "user1" as dav path - Then as "user1" the folder "/sub" exists - And as "user1" the folder "/sub/test" does not exist - And using old dav path - And as "user0" the folder "/sub" does not exist - And Getting info of last share - And the OCS status code should be "404" - Scenario: transferring ownership transfers received shares into subdir when requested Given user "user0" exists And user "user1" exists @@ -548,7 +527,7 @@ Feature: transfer-ownership And User "user0" moved folder "/transfer-share" to "/sub/transfer-share" And folder "/do-not-transfer" of user "user2" is shared with user "user0" with permissions 31 And user "user0" accepts last share - When transferring ownership of path "sub" from "user0" to "user1" with received shares + When transferring ownership of path "sub" from "user0" to "user1" And the command was successful And As an "user1" And using received transfer folder of "user1" as dav path diff --git a/build/integration/files_features/windows_compatibility.feature b/build/integration/files_features/windows_compatibility.feature index 2fdd37d67a1..feaaca1ed3a 100644 --- a/build/integration/files_features/windows_compatibility.feature +++ b/build/integration/files_features/windows_compatibility.feature @@ -54,7 +54,7 @@ Feature: Windows compatible filenames And invoking occ with "files:windows-compatible-filenames --enable" And invoking occ with "files:sanitize-filenames user0" Then as "user0" the file "/2*2=4.txt" does not exist - And as "user0" the file "/2 2=4.txt" exists + And as "user0" the file "/2_2=4.txt" exists Scenario: renaming a file with invalid character and replacement setup Given As an "admin" diff --git a/build/integration/filesdrop_features/filesdrop.feature b/build/integration/filesdrop_features/filesdrop.feature index eacd19a4b13..7618a31a1d0 100644 --- a/build/integration/filesdrop_features/filesdrop.feature +++ b/build/integration/filesdrop_features/filesdrop.feature @@ -44,7 +44,7 @@ Feature: FilesDrop And Updating last share with | permissions | 4 | When Dropping file "/folder/a.txt" with "abc" - Then the HTTP status code should be "405" + Then the HTTP status code should be "400" Scenario: Files drop forbid MKCOL without a nickname Given user "user0" exists @@ -57,7 +57,7 @@ Feature: FilesDrop And Updating last share with | permissions | 4 | When Creating folder "folder" in drop - Then the HTTP status code should be "405" + Then the HTTP status code should be "400" Scenario: Files drop allows MKCOL with a nickname Given user "user0" exists @@ -83,7 +83,7 @@ Feature: FilesDrop And Updating last share with | permissions | 4 | When dropping file "/folder/a.txt" with "abc" - Then the HTTP status code should be "405" + Then the HTTP status code should be "400" Scenario: Files request drop Given user "user0" exists @@ -99,6 +99,47 @@ Feature: FilesDrop And Downloading file "/drop/Alice/folder/a.txt" Then Downloaded content should be "abc" + Scenario: File drop uploading folder with name of file + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder" with "its a file" as "Alice" + Then the HTTP status code should be "201" + When Dropping file "/folder/a.txt" with "abc" as "Alice" + Then the HTTP status code should be "201" + When Downloading file "/drop/Alice/folder" + Then the HTTP status code should be "200" + And Downloaded content should be "its a file" + When Downloading file "/drop/Alice/folder (2)/a.txt" + Then Downloaded content should be "abc" + + Scenario: File drop uploading file with name of folder + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as "Alice" + Then the HTTP status code should be "201" + When Dropping file "/folder" with "its a file" as "Alice" + Then the HTTP status code should be "201" + When Downloading file "/drop/Alice/folder/a.txt" + Then the HTTP status code should be "200" + And Downloaded content should be "abc" + When Downloading file "/drop/Alice/folder (2)" + Then the HTTP status code should be "200" + And Downloaded content should be "its a file" + Scenario: Put file same file multiple times via files drop Given user "user0" exists And As an "user0" @@ -154,4 +195,43 @@ Feature: FilesDrop | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | | shareWith | | When Dropping file "/folder/a.txt" with "abc" - Then the HTTP status code should be "405" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with slashes + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as "Alice/Bob/Mallory" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with forbidden characters + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as ".htaccess" + Then the HTTP status code should be "400" + + Scenario: Files request drop with invalid nickname with forbidden characters + Given user "user0" exists + And As an "user0" + And user "user0" created a folder "/drop" + And as "user0" creating a share with + | path | drop | + | shareType | 4 | + | permissions | 4 | + | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] | + | shareWith | | + When Dropping file "/folder/a.txt" with "abc" as ".Mallory" + Then the HTTP status code should be "400" diff --git a/build/integration/routing_features/apps-and-routes.feature b/build/integration/routing_features/apps-and-routes.feature new file mode 100644 index 00000000000..954ea73bfac --- /dev/null +++ b/build/integration/routing_features/apps-and-routes.feature @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: appmanagement + Background: + Given using api version "2" + And user "user1" exists + And user "user2" exists + And group "group1" exists + And user "user1" belongs to group "group1" + + Scenario: Enable app and test route + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "998" + And the HTTP status code should be "404" + When sending "POST" to "/cloud/apps/weather_status" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And app "weather_status" is enabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "user1" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "user2" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + + Scenario: Enable app only for some groups + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "998" + And the HTTP status code should be "404" + Given invoking occ with "app:enable weather_status --groups group1" + Then the command was successful + Given As an "user2" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the HTTP status code should be "412" + Given As an "user1" + When sending "GET" to "/apps/weather_status/api/v1/location" + Then the OCS status code should be "200" + And the HTTP status code should be "200" + Given As an "admin" + And sending "DELETE" to "/cloud/apps/weather_status" + And app "weather_status" is disabled diff --git a/build/integration/run.sh b/build/integration/run.sh index cbd3cceb3d1..30dd0646b10 100755 --- a/build/integration/run.sh +++ b/build/integration/run.sh @@ -18,6 +18,8 @@ HIDE_OC_LOGS=$2 INSTALLED=$($OCC status | grep installed: | cut -d " " -f 5) if [ "$INSTALLED" == "true" ]; then + # Disable appstore to avoid spamming from CI + $OCC config:system:set appstoreenabled --value=false --type=boolean # Disable bruteforce protection because the integration tests do trigger them $OCC config:system:set auth.bruteforce.protection.enabled --value false --type bool # Disable rate limit protection because the integration tests do trigger them diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature index 46bcdf486d2..a6e4c67165a 100644 --- a/build/integration/sharing_features/sharing-v1-part2.feature +++ b/build/integration/sharing_features/sharing-v1-part2.feature @@ -1265,7 +1265,9 @@ Feature: sharing |{http://open-collaboration-services.org/ns}share-permissions | Then the single response should contain a property "{http://open-collaboration-services.org/ns}share-permissions" with value "19" - Scenario: Cannot download a file when it's shared view-only + Scenario: Cannot download a file when it's shared view-only without shareapi_allow_view_without_download + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" moves file "/textfile0.txt" to "/document.odt" @@ -1274,8 +1276,15 @@ Feature: sharing When As an "user1" And Downloading file "/document.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And Downloading file "/document.odt" + Then the HTTP status code should be "200" - Scenario: Cannot download a file when its parent is shared view-only + Scenario: Cannot download a file when its parent is shared view-only without shareapi_allow_view_without_download + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" created a folder "/sharedviewonly" @@ -1285,8 +1294,15 @@ Feature: sharing When As an "user1" And Downloading file "/sharedviewonly/document.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And Downloading file "/sharedviewonly/document.odt" + Then the HTTP status code should be "200" - Scenario: Cannot copy a file when it's shared view-only + Scenario: Cannot copy a file when it's shared view-only even with shareapi_allow_view_without_download enabled + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" moves file "/textfile0.txt" to "/document.odt" @@ -1294,8 +1310,15 @@ Feature: sharing And user "user1" accepts last share When User "user1" copies file "/document.odt" to "/copyforbidden.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And User "user1" copies file "/document.odt" to "/copyforbidden.odt" + Then the HTTP status code should be "403" Scenario: Cannot copy a file when its parent is shared view-only + Given As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "no" Given user "user0" exists And user "user1" exists And User "user0" created a folder "/sharedviewonly" @@ -1304,5 +1327,10 @@ Feature: sharing And user "user1" accepts last share When User "user1" copies file "/sharedviewonly/document.odt" to "/copyforbidden.odt" Then the HTTP status code should be "403" + Then As an "admin" + And parameter "shareapi_allow_view_without_download" of app "core" is set to "yes" + Then As an "user1" + And User "user1" copies file "/sharedviewonly/document.odt" to "/copyforbidden.odt" + Then the HTTP status code should be "403" # See sharing-v1-part3.feature diff --git a/build/integration/theming_features/theming.feature b/build/integration/theming_features/theming.feature new file mode 100644 index 00000000000..2ae5d4f75c3 --- /dev/null +++ b/build/integration/theming_features/theming.feature @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +Feature: theming + + Background: + Given user "user0" exists + + Scenario: themed stylesheets are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + # Themes that can not be explicitly set by a guest could have been + # globally set too through "enforce_theme". + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed stylesheets are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/theme/default.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/light-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/dark-highcontrast.css" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/theme/opendyslexic.css" + Then the HTTP status code should be "200" + + Scenario: themed images are available for users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for guests + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed images are available for disabled users + Given Logging in using web as "admin" + And logged in admin uploads theming image for "background" from file "data/clouds.jpg" + And logged in admin uploads theming image for "logo" from file "data/coloured-pattern-non-square.png" + And logged in admin uploads theming image for "logoheader" from file "data/coloured-pattern-non-square.png" + And As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/image/background" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logo" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/image/logoheader" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for users + Given As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for guests + Given As an "anonymous" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" + + Scenario: themed icons are available for disabled users + Given As an "admin" + And assure user "user0" is disabled + And As an "user0" + When sending "GET" with exact url to "/index.php/apps/theming/favicon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/favicon/dashboard" + Then the HTTP status code should be "200" + When sending "GET" with exact url to "/index.php/apps/theming/icon/dashboard" + Then the HTTP status code should be "200" diff --git a/build/license.php b/build/license.php index b21f5b263fc..bb6351181c6 100644 --- a/build/license.php +++ b/build/license.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -220,7 +221,7 @@ With help from many libraries and frameworks including: $index++; continue; } - + if (strpos($line, '<?php declare(strict_types') !== false) { $isStrict = true; array_splice($lines, $index, 1); @@ -262,7 +263,7 @@ With help from many libraries and frameworks including: private function getCopyrightNotices($path, $file) { $licenseHeaderCopyrightAtLines = trim(shell_exec("grep -ni 'copyright' $path | cut -d ':' -f 1")); $lineByLine = explode(PHP_EOL, $file); - + $copyrightNotice = []; if (trim($licenseHeaderCopyrightAtLines !== '')) { $copyrightNotice = array_map(function ($line) use ($lineByLine) { @@ -355,7 +356,7 @@ With help from many libraries and frameworks including: } $authors = $this->filterAuthors($authors); - + if ($gitRoot) { $authors = array_map([$this, 'checkCoreMailMap'], $authors); $authors = array_unique($authors); diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 8d409314d4d..7172c6f33f4 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -209,7 +209,6 @@ <code><![CDATA[IAppContainer]]></code> </DeprecatedInterface> <DeprecatedMethod> - <code><![CDATA[\OC_App::loadApps(['dav'])]]></code> <code><![CDATA[getServer]]></code> <code><![CDATA[getServer]]></code> <code><![CDATA[getURLGenerator]]></code> @@ -675,6 +674,12 @@ </ParamNameMismatch> </file> <file src="apps/dav/lib/Connector/Sabre/File.php"> + <DeprecatedClass> + <code><![CDATA[Files::streamCopy($data, $target, true)]]></code> + </DeprecatedClass> + <DeprecatedMethod> + <code><![CDATA[Files::streamCopy($data, $target, true)]]></code> + </DeprecatedMethod> <LessSpecificReturnStatement> <code><![CDATA[$this->node]]></code> </LessSpecificReturnStatement> @@ -776,12 +781,6 @@ <code><![CDATA[setAppValue]]></code> </DeprecatedMethod> </file> - <file src="apps/dav/lib/Controller/ExampleContentController.php"> - <DeprecatedMethod> - <code><![CDATA[getAppValue]]></code> - <code><![CDATA[setAppValue]]></code> - </DeprecatedMethod> - </file> <file src="apps/dav/lib/Controller/InvitationResponseController.php"> <UndefinedPropertyAssignment> <code><![CDATA[$vEvent->DTSTAMP]]></code> @@ -1015,11 +1014,6 @@ <code><![CDATA[getAppValue]]></code> </DeprecatedMethod> </file> - <file src="apps/dav/lib/Settings/ExampleContentSettings.php"> - <DeprecatedMethod> - <code><![CDATA[getAppValue]]></code> - </DeprecatedMethod> - </file> <file src="apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php"> <DeprecatedMethod> <code><![CDATA[getAppValue]]></code> @@ -1344,7 +1338,6 @@ </DeprecatedInterface> <DeprecatedMethod> <code><![CDATA[Util::connectHook('\OCP\Config', 'js', '\OCA\Files\App', 'extendJsConfig')]]></code> - <code><![CDATA[\OC_Helper::getFileTemplateManager()]]></code> <code><![CDATA[getUserFolder]]></code> <code><![CDATA[getUserFolder]]></code> </DeprecatedMethod> @@ -1396,7 +1389,6 @@ </file> <file src="apps/files/lib/Helper.php"> <UndefinedInterfaceMethod> - <code><![CDATA[$file]]></code> <code><![CDATA[$i]]></code> <code><![CDATA[$i]]></code> <code><![CDATA[$i]]></code> @@ -1469,11 +1461,6 @@ <code><![CDATA[class-string<IStorage>]]></code> </MoreSpecificReturnType> </file> - <file src="apps/files_external/lib/Lib/Backend/SMB.php"> - <DeprecatedClass> - <code><![CDATA[new KerberosApacheAuth()]]></code> - </DeprecatedClass> - </file> <file src="apps/files_external/lib/Lib/Storage/SFTP.php"> <InternalMethod> <code><![CDATA[put]]></code> @@ -1588,9 +1575,6 @@ <code><![CDATA[new QueryException()]]></code> <code><![CDATA[new QueryException()]]></code> </DeprecatedClass> - <DeprecatedInterface> - <code><![CDATA[private]]></code> - </DeprecatedInterface> </file> <file src="apps/files_sharing/lib/Controller/ShareAPIController.php"> <DeprecatedClass> @@ -1965,14 +1949,13 @@ </file> <file src="apps/files_versions/lib/Storage.php"> <DeprecatedClass> + <code><![CDATA[Files::streamCopy($source, $target, true)]]></code> <code><![CDATA[\OC_Util::setupFS($uid)]]></code> </DeprecatedClass> <DeprecatedMethod> + <code><![CDATA[Files::streamCopy($source, $target, true)]]></code> <code><![CDATA[dispatch]]></code> </DeprecatedMethod> - <RedundantCondition> - <code><![CDATA[$storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')]]></code> - </RedundantCondition> </file> <file src="apps/oauth2/lib/Controller/OauthApiController.php"> <NoInterfaceProperties> @@ -2003,8 +1986,6 @@ <code><![CDATA[implementsActions]]></code> <code><![CDATA[implementsActions]]></code> <code><![CDATA[implementsActions]]></code> - <code><![CDATA[setEMailAddress]]></code> - <code><![CDATA[setEMailAddress]]></code> </DeprecatedMethod> <TypeDoesNotContainNull> <code><![CDATA[$groupid === null]]></code> @@ -2594,14 +2575,6 @@ <code><![CDATA[$gid]]></code> </ParamNameMismatch> </file> - <file src="apps/user_ldap/lib/Helper.php"> - <DeprecatedMethod> - <code><![CDATA[execute]]></code> - <code><![CDATA[getAppKeys]]></code> - <code><![CDATA[getAppValue]]></code> - <code><![CDATA[getAppValue]]></code> - </DeprecatedMethod> - </file> <file src="apps/user_ldap/lib/Jobs/CleanUp.php"> <DeprecatedMethod> <code><![CDATA[getAppValue]]></code> @@ -2870,12 +2843,6 @@ <code><![CDATA[dispatch]]></code> </DeprecatedMethod> </file> - <file src="core/Application.php"> - <DeprecatedMethod> - <code><![CDATA[getServer]]></code> - <code><![CDATA[registerService]]></code> - </DeprecatedMethod> - </file> <file src="core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php"> <DeprecatedClass> <code><![CDATA[Files::rmdirr($dir)]]></code> @@ -2950,8 +2917,6 @@ </file> <file src="core/Command/Db/ConvertType.php"> <DeprecatedMethod> - <code><![CDATA[\OC_App::getAllApps()]]></code> - <code><![CDATA[\OC_App::loadApp($app)]]></code> <code><![CDATA[execute]]></code> <code><![CDATA[getName]]></code> <code><![CDATA[getPrimaryKeyColumns]]></code> @@ -3363,18 +3328,6 @@ <code><![CDATA[version_compare($first, $second, $operator)]]></code> </NullableReturnStatement> </file> - <file src="lib/private/App/InfoParser.php"> - <InvalidArrayOffset> - <code><![CDATA[$array[$element][]]]></code> - <code><![CDATA[$array[$element][]]]></code> - </InvalidArrayOffset> - <InvalidReturnStatement> - <code><![CDATA[(string)$xml]]></code> - </InvalidReturnStatement> - <InvalidReturnType> - <code><![CDATA[array]]></code> - </InvalidReturnType> - </file> <file src="lib/private/AppConfig.php"> <NullableReturnStatement> <code><![CDATA[$this->fastCache[$app][$key] ?? $default]]></code> @@ -3386,18 +3339,12 @@ </UndefinedMethod> </file> <file src="lib/private/AppFramework/DependencyInjection/DIContainer.php"> - <ImplementedReturnTypeMismatch> - <code><![CDATA[boolean|null]]></code> - </ImplementedReturnTypeMismatch> <InvalidReturnStatement> <code><![CDATA[$this->server]]></code> </InvalidReturnStatement> <InvalidReturnType> <code><![CDATA[\OCP\IServerContainer]]></code> </InvalidReturnType> - <UndefinedInterfaceMethod> - <code><![CDATA[getAppDataDir]]></code> - </UndefinedInterfaceMethod> </file> <file src="lib/private/AppFramework/Http/Dispatcher.php"> <NullArgument> @@ -3453,53 +3400,9 @@ </MoreSpecificImplementedParamType> </file> <file src="lib/private/AppFramework/Utility/SimpleContainer.php"> - <LessSpecificReturnStatement> - <code><![CDATA[$class->newInstance()]]></code> - <code><![CDATA[$class->newInstanceArgs(array_map(function (ReflectionParameter $parameter) { - $parameterType = $parameter->getType(); - - $resolveName = $parameter->getName(); - - // try to find out if it is a class or a simple parameter - if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { - $resolveName = $parameterType->getName(); - } - - try { - $builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType) - && $parameter->getType()->isBuiltin(); - return $this->query($resolveName, !$builtIn); - } catch (QueryException $e) { - // Service not found, use the default value when available - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { - $resolveName = $parameter->getName(); - try { - return $this->query($resolveName); - } catch (QueryException $e2) { - // Pass null if typed and nullable - if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) { - return null; - } - - // don't lose the error we got while trying to query by type - throw new QueryException($e->getMessage(), (int)$e->getCode(), $e); - } - } - - throw $e; - } - }, $constructor->getParameters()))]]></code> - </LessSpecificReturnStatement> <MissingTemplateParam> <code><![CDATA[ArrayAccess]]></code> </MissingTemplateParam> - <MoreSpecificReturnType> - <code><![CDATA[\stdClass]]></code> - </MoreSpecificReturnType> <RedundantCast> <code><![CDATA[(int)$e->getCode()]]></code> </RedundantCast> @@ -3882,14 +3785,6 @@ <code><![CDATA[wrap]]></code> </UndefinedInterfaceMethod> </file> - <file src="lib/private/Files/Mount/ObjectHomeMountProvider.php"> - <InvalidNullableReturnType> - <code><![CDATA[\OCP\Files\Mount\IMountPoint]]></code> - </InvalidNullableReturnType> - <NullableReturnStatement> - <code><![CDATA[null]]></code> - </NullableReturnStatement> - </file> <file src="lib/private/Files/Node/File.php"> <InvalidReturnStatement> <code><![CDATA[$this->view->hash($type, $this->path, $raw)]]></code> @@ -4017,12 +3912,6 @@ <code><![CDATA[ArrayCache]]></code> <code><![CDATA[ArrayCache]]></code> </InvalidClass> - <InvalidReturnStatement> - <code><![CDATA[$response->getBody()]]></code> - </InvalidReturnStatement> - <InvalidReturnType> - <code><![CDATA[fopen]]></code> - </InvalidReturnType> </file> <file src="lib/private/Files/Storage/Local.php"> <TypeDoesNotContainNull> @@ -4099,12 +3988,6 @@ </TypeDoesNotContainNull> </file> <file src="lib/private/Group/Group.php"> - <InvalidArgument> - <code><![CDATA[bool]]></code> - </InvalidArgument> - <InvalidOperand> - <code><![CDATA[$hide]]></code> - </InvalidOperand> <LessSpecificReturnStatement> <code><![CDATA[$users]]></code> </LessSpecificReturnStatement> @@ -4139,16 +4022,6 @@ <code><![CDATA[isAdmin]]></code> </UndefinedInterfaceMethod> </file> - <file src="lib/private/Http/Client/Response.php"> - <InvalidNullableReturnType> - <code><![CDATA[string|resource]]></code> - </InvalidNullableReturnType> - <NullableReturnStatement> - <code><![CDATA[$this->stream ? - $this->response->getBody()->detach(): - $this->response->getBody()->getContents()]]></code> - </NullableReturnStatement> - </file> <file src="lib/private/Installer.php"> <InvalidArgument> <code><![CDATA[false]]></code> @@ -4192,31 +4065,12 @@ </InvalidOperand> </file> <file src="lib/private/Lockdown/Filesystem/NullCache.php"> - <InvalidNullableReturnType> - <code><![CDATA[get]]></code> - </InvalidNullableReturnType> <InvalidReturnStatement> <code><![CDATA[[]]]></code> </InvalidReturnStatement> <InvalidReturnType> <code><![CDATA[getIncomplete]]></code> </InvalidReturnType> - <NullableReturnStatement> - <code><![CDATA[$file !== '' ? null : - new CacheEntry([ - 'fileid' => -1, - 'parent' => -1, - 'name' => '', - 'path' => '', - 'size' => '0', - 'mtime' => time(), - 'storage_mtime' => time(), - 'etag' => '', - 'mimetype' => FileInfo::MIMETYPE_FOLDER, - 'mimepart' => 'httpd', - 'permissions' => Constants::PERMISSION_READ - ])]]></code> - </NullableReturnStatement> </file> <file src="lib/private/Lockdown/Filesystem/NullStorage.php"> <TooManyArguments> @@ -4306,12 +4160,6 @@ </ImplementedReturnTypeMismatch> </file> <file src="lib/private/Remote/Instance.php"> - <InvalidReturnStatement> - <code><![CDATA[$request->getBody()]]></code> - </InvalidReturnStatement> - <InvalidReturnType> - <code><![CDATA[bool|string]]></code> - </InvalidReturnType> <InvalidScalarArgument> <code><![CDATA[$response]]></code> </InvalidScalarArgument> @@ -4347,9 +4195,6 @@ </ParamNameMismatch> </file> <file src="lib/private/Route/Router.php"> - <InvalidClass> - <code><![CDATA[\OC_APP]]></code> - </InvalidClass> <InvalidNullableReturnType> <code><![CDATA[string]]></code> </InvalidNullableReturnType> @@ -4379,14 +4224,11 @@ </file> <file src="lib/private/Server.php"> <ImplementedReturnTypeMismatch> - <code><![CDATA[\OCP\Calendar\Resource\IManager]]></code> - <code><![CDATA[\OCP\Calendar\Room\IManager]]></code> <code><![CDATA[\OCP\Files\Folder|null]]></code> </ImplementedReturnTypeMismatch> <LessSpecificReturnStatement> <code><![CDATA[$this->get(IFile::class)]]></code> <code><![CDATA[$this->get(IGroupManager::class)]]></code> - <code><![CDATA[$this->get(INavigationManager::class)]]></code> <code><![CDATA[$this->get(IUserManager::class)]]></code> <code><![CDATA[$this->get(IUserSession::class)]]></code> <code><![CDATA[$this->get(\OCP\Encryption\IManager::class)]]></code> @@ -4395,13 +4237,9 @@ <code><![CDATA[\OC\Encryption\File]]></code> <code><![CDATA[\OC\Encryption\Manager]]></code> <code><![CDATA[\OC\Group\Manager]]></code> - <code><![CDATA[\OC\NavigationManager]]></code> <code><![CDATA[\OC\User\Manager]]></code> <code><![CDATA[\OC\User\Session]]></code> </MoreSpecificReturnType> - <UndefinedDocblockClass> - <code><![CDATA[\OC\OCSClient]]></code> - </UndefinedDocblockClass> </file> <file src="lib/private/ServerContainer.php"> <InvalidPropertyAssignmentValue> @@ -4468,39 +4306,13 @@ </UndefinedInterfaceMethod> </file> <file src="lib/private/Share20/ProviderFactory.php"> - <InvalidNullableReturnType> - <code><![CDATA[FederatedShareProvider]]></code> - <code><![CDATA[ShareByMailProvider]]></code> - </InvalidNullableReturnType> <InvalidReturnStatement> <code><![CDATA[$provider]]></code> <code><![CDATA[$provider]]></code> - <code><![CDATA[$this->shareByCircleProvider]]></code> </InvalidReturnStatement> <InvalidReturnType> <code><![CDATA[getProviderForType]]></code> </InvalidReturnType> - <NullableReturnStatement> - <code><![CDATA[null]]></code> - <code><![CDATA[null]]></code> - <code><![CDATA[null]]></code> - <code><![CDATA[null]]></code> - <code><![CDATA[null]]></code> - <code><![CDATA[null]]></code> - </NullableReturnStatement> - <ParamNameMismatch> - <code><![CDATA[$shareProviderClass]]></code> - </ParamNameMismatch> - <UndefinedClass> - <code><![CDATA[\OCA\Circles\ShareByCircleProvider]]></code> - </UndefinedClass> - <UndefinedDocblockClass> - <code><![CDATA[RoomShareProvider]]></code> - <code><![CDATA[\OCA\Circles\ShareByCircleProvider]]></code> - <code><![CDATA[\OCA\Talk\Share\RoomShareProvider]]></code> - <code><![CDATA[private $roomShareProvider = null;]]></code> - <code><![CDATA[private $shareByCircleProvider = null;]]></code> - </UndefinedDocblockClass> </file> <file src="lib/private/Share20/Share.php"> <LessSpecificReturnStatement> @@ -4620,9 +4432,6 @@ <code><![CDATA[$matches[0][$last_match]]]></code> <code><![CDATA[$matches[1][$last_match]]]></code> </InvalidArrayOffset> - <InvalidScalarArgument> - <code><![CDATA[$path]]></code> - </InvalidScalarArgument> <UndefinedInterfaceMethod> <code><![CDATA[getQuota]]></code> </UndefinedInterfaceMethod> @@ -4640,14 +4449,6 @@ <code><![CDATA[array{X-Request-Id: string, Cache-Control: string, Content-Security-Policy: string, Feature-Policy: string, X-Robots-Tag: string, Last-Modified?: string, ETag?: string, ...H}]]></code> </MoreSpecificReturnType> </file> - <file src="lib/public/Color.php"> - <LessSpecificReturnStatement> - <code><![CDATA[$step]]></code> - </LessSpecificReturnStatement> - <MoreSpecificReturnType> - <code><![CDATA[array{0: int, 1: int, 2: int}]]></code> - </MoreSpecificReturnType> - </file> <file src="lib/public/Preview/BeforePreviewFetchedEvent.php"> <LessSpecificReturnStatement> <code><![CDATA[$this->mode]]></code> @@ -4662,14 +4463,6 @@ <code><![CDATA[getRequest]]></code> </DeprecatedMethod> </file> - <file src="ocs/v1.php"> - <DeprecatedMethod> - <code><![CDATA[OC_App::loadApps()]]></code> - <code><![CDATA[OC_App::loadApps(['authentication'])]]></code> - <code><![CDATA[OC_App::loadApps(['extended_authentication'])]]></code> - <code><![CDATA[OC_App::loadApps(['session'])]]></code> - </DeprecatedMethod> - </file> <file src="public.php"> <DeprecatedMethod> <code><![CDATA[getAppValue]]></code> diff --git a/build/psalm/OcpSinceChecker.php b/build/psalm/OcpSinceChecker.php index 959e70e0c4c..c533e944550 100644 --- a/build/psalm/OcpSinceChecker.php +++ b/build/psalm/OcpSinceChecker.php @@ -20,7 +20,18 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn $classLike = $event->getStmt(); $statementsSource = $event->getStatementsSource(); - self::checkClassComment($classLike, $statementsSource); + if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) { + return; + } + + $isTesting = str_contains($statementsSource->getFilePath(), '/lib/public/Notification/') + || str_contains($statementsSource->getFilePath(), 'CalendarEventStatus'); + + if ($isTesting) { + self::checkStatementAttributes($classLike, $statementsSource); + } else { + self::checkClassComment($classLike, $statementsSource); + } foreach ($classLike->stmts as $stmt) { if ($stmt instanceof ClassConst) { @@ -32,11 +43,64 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn } if ($stmt instanceof EnumCase) { - self::checkStatementComment($stmt, $statementsSource, 'enum'); + if ($isTesting) { + self::checkStatementAttributes($classLike, $statementsSource); + } else { + self::checkStatementComment($stmt, $statementsSource, 'enum'); + } } } } + private static function checkStatementAttributes(ClassLike $stmt, FileSource $statementsSource): void { + $hasAppFrameworkAttribute = false; + $mustBeConsumable = false; + $isConsumable = false; + foreach ($stmt->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if (in_array($attr->name->getLast(), [ + 'Catchable', + 'Consumable', + 'Dispatchable', + 'Implementable', + 'Listenable', + 'Throwable', + ], true)) { + $hasAppFrameworkAttribute = true; + self::checkAttributeHasValidSinceVersion($attr, $statementsSource); + } + if (in_array($attr->name->getLast(), [ + 'Catchable', + 'Consumable', + 'Listenable', + ], true)) { + $isConsumable = true; + } + if ($attr->name->getLast() === 'ExceptionalImplementable') { + $mustBeConsumable = true; + } + } + } + + if ($mustBeConsumable && !$isConsumable) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Attribute OCP\\AppFramework\\Attribute\\ExceptionalImplementable is only valid on classes that also have OCP\\AppFramework\\Attribute\\Consumable', + new CodeLocation($statementsSource, $stmt) + ) + ); + } + + if (!$hasAppFrameworkAttribute) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'At least one of the OCP\\AppFramework\\Attribute attributes is required', + new CodeLocation($statementsSource, $stmt) + ) + ); + } + } + private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void { $docblock = $stmt->getDocComment(); @@ -124,4 +188,28 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn ); } } + + private static function checkAttributeHasValidSinceVersion(\PhpParser\Node\Attribute $stmt, FileSource $statementsSource): void { + foreach ($stmt->args as $arg) { + if ($arg->name?->name === 'since') { + if (!$arg->value instanceof \PhpParser\Node\Scalar\String_) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Attribute since argument is not a valid version string', + new CodeLocation($statementsSource, $stmt) + ) + ); + } else { + if (!preg_match('/^[1-9][0-9]*(\.[0-9]+){0,3}$/', $arg->value->value)) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Attribute since argument is not a valid version string', + new CodeLocation($statementsSource, $stmt) + ) + ); + } + } + } + } + } } diff --git a/build/rector.php b/build/rector.php index 95fcc8d961e..246741bb34f 100644 --- a/build/rector.php +++ b/build/rector.php @@ -12,6 +12,9 @@ use PhpParser\Node; use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface; use Rector\Config\RectorConfig; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; +use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DataProviderAnnotationToAttributeRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWillMethodRector; +use Rector\PHPUnit\PHPUnit100\Rector\Class_\StaticDataProviderClassMethodRector; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; use Rector\ValueObject\Application\File; @@ -63,9 +66,10 @@ $config = RectorConfig::configure() $nextcloudDir . '/remote.php', $nextcloudDir . '/status.php', $nextcloudDir . '/version.php', + $nextcloudDir . '/lib/private/Share20/ProviderFactory.php', + $nextcloudDir . '/tests', // $nextcloudDir . '/config', // $nextcloudDir . '/lib', - // $nextcloudDir . '/tests', // $nextcloudDir . '/themes', ]) ->withSkip([ @@ -78,6 +82,11 @@ $config = RectorConfig::configure() // ->withPhpSets() ->withImportNames(importShortClasses:false) ->withTypeCoverageLevel(0) + ->withRules([ + UseSpecificWillMethodRector::class, + StaticDataProviderClassMethodRector::class, + DataProviderAnnotationToAttributeRector::class, + ]) ->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [ 'inline_public' => true, 'rename_property' => true, |