aboutsummaryrefslogtreecommitdiffstats
path: root/build/integration
diff options
context:
space:
mode:
Diffstat (limited to 'build/integration')
-rw-r--r--build/integration/.gitignore3
-rw-r--r--build/integration/capabilities_features/capabilities.feature2
-rw-r--r--build/integration/collaboration_features/autocomplete.feature262
-rw-r--r--build/integration/collaboration_features/user_status.feature22
-rw-r--r--build/integration/comments_features/comments.feature (renamed from build/integration/features/comments.feature)2
-rw-r--r--build/integration/composer.json22
-rw-r--r--build/integration/composer.lock4590
-rw-r--r--build/integration/config/behat.yml188
-rw-r--r--build/integration/data/bjoern.vcf.license2
-rw-r--r--build/integration/data/clouds.jpgbin0 -> 538205 bytes
-rw-r--r--build/integration/data/clouds.jpg.license2
-rw-r--r--build/integration/data/coloured-pattern-non-square.png (renamed from build/integration/data/coloured-pattern.png)bin2447 -> 2447 bytes
-rw-r--r--build/integration/data/coloured-pattern-non-square.png.license2
-rw-r--r--build/integration/data/green-square-256.png.license2
-rw-r--r--build/integration/data/textfile.txt.license2
-rw-r--r--build/integration/dav_features/caldav.feature (renamed from build/integration/features/caldav.feature)39
-rw-r--r--build/integration/dav_features/carddav.feature (renamed from build/integration/features/carddav.feature)29
-rw-r--r--build/integration/dav_features/dav-v2-public.feature78
-rw-r--r--build/integration/dav_features/dav-v2.feature162
-rw-r--r--build/integration/dav_features/principal-property-search.feature13
-rw-r--r--build/integration/dav_features/webdav-related.feature (renamed from build/integration/features/webdav-related.feature)199
-rw-r--r--build/integration/features/auth.feature3
-rw-r--r--build/integration/features/avatar.feature92
-rw-r--r--build/integration/features/bootstrap/Activity.php32
-rw-r--r--build/integration/features/bootstrap/AppConfiguration.php32
-rw-r--r--build/integration/features/bootstrap/Auth.php37
-rw-r--r--build/integration/features/bootstrap/Avatar.php53
-rw-r--r--build/integration/features/bootstrap/BasicStructure.php134
-rw-r--r--build/integration/features/bootstrap/CalDavContext.php212
-rw-r--r--build/integration/features/bootstrap/CapabilitiesContext.php37
-rw-r--r--build/integration/features/bootstrap/CardDavContext.php100
-rw-r--r--build/integration/features/bootstrap/ChecksumsContext.php68
-rw-r--r--build/integration/features/bootstrap/CollaborationContext.php206
-rw-r--r--build/integration/features/bootstrap/CommandLine.php31
-rw-r--r--build/integration/features/bootstrap/CommandLineContext.php48
-rw-r--r--build/integration/features/bootstrap/CommentsContext.php72
-rw-r--r--build/integration/features/bootstrap/ContactsMenu.php51
-rw-r--r--build/integration/features/bootstrap/ConversionsContext.php60
-rw-r--r--build/integration/features/bootstrap/DavFeatureContext.php24
-rw-r--r--build/integration/features/bootstrap/Download.php74
-rw-r--r--build/integration/features/bootstrap/ExternalStorage.php123
-rw-r--r--build/integration/features/bootstrap/FakeSMTPHelper.php37
-rw-r--r--build/integration/features/bootstrap/FeatureContext.php36
-rw-r--r--build/integration/features/bootstrap/FederationContext.php173
-rw-r--r--build/integration/features/bootstrap/FilesDropContext.php67
-rw-r--r--build/integration/features/bootstrap/LDAPContext.php31
-rw-r--r--build/integration/features/bootstrap/Mail.php28
-rw-r--r--build/integration/features/bootstrap/MetadataContext.php124
-rw-r--r--build/integration/features/bootstrap/PrincipalPropertySearchContext.php141
-rw-r--r--build/integration/features/bootstrap/Provisioning.php248
-rw-r--r--build/integration/features/bootstrap/RateLimitingContext.php31
-rw-r--r--build/integration/features/bootstrap/RemoteContext.php39
-rw-r--r--build/integration/features/bootstrap/RoutingContext.php19
-rw-r--r--build/integration/features/bootstrap/Search.php24
-rw-r--r--build/integration/features/bootstrap/SetupContext.php22
-rw-r--r--build/integration/features/bootstrap/ShareesContext.php25
-rw-r--r--build/integration/features/bootstrap/Sharing.php167
-rw-r--r--build/integration/features/bootstrap/SharingContext.php32
-rw-r--r--build/integration/features/bootstrap/TagsContext.php38
-rw-r--r--build/integration/features/bootstrap/TalkContext.php24
-rw-r--r--build/integration/features/bootstrap/Theming.php49
-rw-r--r--build/integration/features/bootstrap/Trashbin.php32
-rw-r--r--build/integration/features/bootstrap/WebDav.php623
-rw-r--r--build/integration/features/comments-search.feature271
-rw-r--r--build/integration/features/contacts-menu.feature194
-rw-r--r--build/integration/features/dav-v2.feature83
-rw-r--r--build/integration/features/external-storage.feature62
-rw-r--r--build/integration/features/log-condition.feature39
-rw-r--r--build/integration/features/maintenance-mode.feature8
-rw-r--r--build/integration/features/ocs-v1.feature2
-rw-r--r--build/integration/features/provisioning-v1.feature1505
-rw-r--r--build/integration/features/provisioning-v2.feature3
-rw-r--r--build/integration/federation_features/cleanup-remote-storage.feature76
-rw-r--r--build/integration/federation_features/federated.feature435
-rw-r--r--build/integration/file_conversions/file_conversions.feature122
-rw-r--r--build/integration/files_features/checksums.feature (renamed from build/integration/features/checksums.feature)19
-rw-r--r--build/integration/files_features/download.feature (renamed from build/integration/features/download.feature)58
-rw-r--r--build/integration/files_features/external-storage.feature127
-rw-r--r--build/integration/files_features/favorites.feature (renamed from build/integration/features/favorites.feature)3
-rw-r--r--build/integration/files_features/metadata.feature16
-rw-r--r--build/integration/files_features/tags.feature (renamed from build/integration/features/tags.feature)3
-rw-r--r--build/integration/files_features/transfer-ownership.feature (renamed from build/integration/features/transfer-ownership.feature)57
-rw-r--r--build/integration/files_features/trashbin.feature (renamed from build/integration/features/trashbin.feature)3
-rw-r--r--build/integration/files_features/windows_compatibility.feature68
-rw-r--r--build/integration/filesdrop_features/filesdrop.feature186
-rw-r--r--build/integration/ldap_features/ldap-ocs.feature2
-rw-r--r--build/integration/ldap_features/ldap-openldap.feature2
-rw-r--r--build/integration/openldap_features/openldap-uid-username.feature (renamed from build/integration/ldap_features/openldap-uid-username.feature)4
-rw-r--r--build/integration/openldap_numerical_features/openldap-numerical-id.feature (renamed from build/integration/ldap_features/openldap-numerical-id.feature)30
-rw-r--r--build/integration/ratelimiting_features/ratelimiting.feature (renamed from build/integration/features/ratelimiting.feature)7
-rw-r--r--build/integration/remoteapi_features/remote.feature5
-rw-r--r--build/integration/routing_features/apps-and-routes.feature52
-rwxr-xr-xbuild/integration/run-docker.sh33
-rwxr-xr-xbuild/integration/run.sh42
-rw-r--r--build/integration/setup_features/setup.feature2
-rw-r--r--build/integration/sharees_features/sharees.feature3
-rw-r--r--build/integration/sharees_features/sharees_provisioningapiv2.feature3
-rw-r--r--build/integration/sharing_features/sharing-activity.feature46
-rw-r--r--build/integration/sharing_features/sharing-v1-part2.feature168
-rw-r--r--build/integration/sharing_features/sharing-v1-part3.feature68
-rw-r--r--build/integration/sharing_features/sharing-v1-part4.feature184
-rw-r--r--build/integration/sharing_features/sharing-v1.feature63
-rw-r--r--build/integration/theming_features/theming.feature131
-rw-r--r--build/integration/videoverification_features/sharing-v1-video-verification.feature (renamed from build/integration/sharing_features/sharing-v1-video-verification.feature)5
104 files changed, 11018 insertions, 2292 deletions
diff --git a/build/integration/.gitignore b/build/integration/.gitignore
index 18b981bf7ed..f181a23b4c0 100644
--- a/build/integration/.gitignore
+++ b/build/integration/.gitignore
@@ -1,3 +1,4 @@
+# SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
vendor
output
-composer.lock
diff --git a/build/integration/capabilities_features/capabilities.feature b/build/integration/capabilities_features/capabilities.feature
index 500ea0297a6..2c664e24e1e 100644
--- a/build/integration/capabilities_features/capabilities.feature
+++ b/build/integration/capabilities_features/capabilities.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: capabilities
Background:
Given using api version "1"
diff --git a/build/integration/collaboration_features/autocomplete.feature b/build/integration/collaboration_features/autocomplete.feature
new file mode 100644
index 00000000000..7efc646f08d
--- /dev/null
+++ b/build/integration/collaboration_features/autocomplete.feature
@@ -0,0 +1,262 @@
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: autocomplete
+ Background:
+ Given using api version "2"
+ And group "commongroup" exists
+ And user "admin" belongs to group "commongroup"
+ And user "auto" exists
+ And user "autocomplete" exists
+ And user "autocomplete2" exists
+ And user "autocomplete2" belongs to group "commongroup"
+
+ Scenario: getting autocomplete
+ Given As an "admin"
+ When get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+ | autocomplete2 | users |
+ And user "autocomplete" has status "dnd"
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get autocomplete for "auto"
+ | id | source | status |
+ | auto | users | "" |
+ | autocomplete | users | {"status":"dnd","message":null,"icon":null,"clearAt":null} |
+ | autocomplete2 | users | "" |
+
+
+ Scenario: getting autocomplete without enumeration
+ Given As an "admin"
+ When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ Then get autocomplete for "autocomplete"
+ | id | source |
+ | autocomplete | users |
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get autocomplete for "auto"
+ | id | source |
+ Then get autocomplete for "autocomplete"
+ | id | source |
+
+ Scenario: getting autocomplete emails from address book with enumeration
+ Given As an "admin"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | email |
+ | value | autocomplete@example.com |
+ And there is a contact in an addressbook
+ Then get email autocomplete for "example"
+ | id | source |
+ | autocomplete | users |
+ | user@example.com | emails |
+ Then get email autocomplete for "auto"
+ | id | source |
+ | autocomplete | users |
+ Then get email autocomplete for "example"
+ | id | source |
+ | autocomplete | users |
+ | user@example.com | emails |
+ Then get email autocomplete for "autocomplete@example.com"
+ | id | source |
+ | autocomplete | users |
+
+ Scenario: getting autocomplete emails from address book without enumeration
+ Given As an "admin"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | email |
+ | value | autocomplete@example.com |
+ And there is a contact in an addressbook
+ And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get email autocomplete for "example"
+ | id | source |
+ | user@example.com | emails |
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "yes"
+ Then get email autocomplete for "auto"
+ | id | source |
+ Then get email autocomplete for "example"
+ | id | source |
+ | user@example.com | emails |
+ Then get email autocomplete for "autocomplete@example.com"
+ | id | source |
+ | autocomplete | users |
+
+ Scenario: getting autocomplete with limited enumeration by group
+ Given As an "admin"
+ When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete2 | users |
+ Then get autocomplete for "autocomplete"
+ | id | source |
+ | autocomplete | users |
+ | autocomplete2 | users |
+ Then get autocomplete for "autocomplete2"
+ | id | source |
+ | autocomplete2 | users |
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get autocomplete for "autocomplete"
+ | id | source |
+ | autocomplete2 | users |
+ Then get autocomplete for "autocomplete2"
+ | id | source |
+ | autocomplete2 | users |
+
+
+ Scenario: getting autocomplete with limited enumeration by phone
+ Given As an "admin"
+ When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+
+ # autocomplete stores their phone number
+ Given As an "autocomplete"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the HTTP status code should be "200"
+ And the OCS status code should be "200"
+
+ Given As an "admin"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+
+ # admin populates they have the phone number
+ When search users by phone for region "DE" with
+ | random-string1 | 0711 / 252 428-90 |
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get autocomplete for "auto"
+ | id | source |
+ | autocomplete | users |
+
+
+ Scenario: getting autocomplete with limited enumeration by group or phone
+ Given As an "admin"
+ When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
+ And parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
+
+ # autocomplete stores their phone number
+ Given As an "autocomplete"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the HTTP status code should be "200"
+ And the OCS status code should be "200"
+ # admin populates they have the phone number
+ Given As an "admin"
+ When search users by phone for region "DE" with
+ | random-string1 | 0711 / 252 428-90 |
+
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+ | autocomplete2 | users |
+
+ When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no"
+ Then get autocomplete for "auto"
+ | id | source |
+ | autocomplete | users |
+ | autocomplete2 | users |
+
+
+ Scenario: getting autocomplete with limited enumeration but sharing is group restricted
+ Given As an "admin"
+ When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
+ And parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
+
+ # autocomplete stores their phone number
+ Given As an "autocomplete"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the HTTP status code should be "200"
+ And the OCS status code should be "200"
+ # admin populates they have the phone number
+ Given As an "admin"
+ When search users by phone for region "DE" with
+ | random-string1 | 0711 / 252 428-90 |
+
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+ | autocomplete2 | users |
+ When parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes"
+ Then get autocomplete for "auto"
+ | id | source |
+ | autocomplete2 | users |
+
+
+ Scenario: getting autocomplete with limited enumeration by phone but user changes it
+ Given As an "admin"
+ When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+
+ # autocomplete stores their phone number
+ Given As an "autocomplete"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the HTTP status code should be "200"
+ And the OCS status code should be "200"
+
+ Given As an "admin"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+
+ # admin populates they have the phone number
+ When search users by phone for region "DE" with
+ | random-string1 | 0711 / 252 428-90 |
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+
+ # autocomplete changes their phone number
+ Given As an "autocomplete"
+ And sending "PUT" to "/cloud/users/autocomplete" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-91 |
+ And the HTTP status code should be "200"
+ And the OCS status code should be "200"
+
+ Given As an "admin"
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+
+ # admin populates they have the new phone number
+ When search users by phone for region "DE" with
+ | random-string1 | 0711 / 252 428-91 |
+ Then get autocomplete for "auto"
+ | id | source |
+ | auto | users |
+ | autocomplete | users |
+
+
+ Scenario: getting autocomplete without enumeration and sharing is group restricted
+ Given As an "admin"
+ When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
+ And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes"
+
+ Then get autocomplete for "auto"
+ | id | source |
+ Then get autocomplete for "autocomplete"
+ | id | source |
+ Then get autocomplete for "autocomplete2"
+ | id | source |
+ | autocomplete2 | users |
diff --git a/build/integration/collaboration_features/user_status.feature b/build/integration/collaboration_features/user_status.feature
new file mode 100644
index 00000000000..f620b50a416
--- /dev/null
+++ b/build/integration/collaboration_features/user_status.feature
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: user_status
+ Background:
+ Given using api version "2"
+ And user "user0" exists
+ And user "user0" has status "dnd"
+
+ Scenario: listing recent user statuses with default settings
+ Then user statuses for "admin" list "user0" with status "dnd"
+
+ Scenario: empty recent user statuses with disabled/limited user enumeration
+ When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no"
+ Then user statuses for "admin" are empty
+ When parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "yes"
+ When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes"
+ Then user statuses for "admin" are empty
+ When parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "no"
+ When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "yes"
+ Then user statuses for "admin" are empty
+ When parameter "shareapi_restrict_user_enumeration_to_phone" of app "core" is set to "no"
+ Then user statuses for "admin" list "user0" with status "dnd"
diff --git a/build/integration/features/comments.feature b/build/integration/comments_features/comments.feature
index 0f3a4cc75cf..33eb154b147 100644
--- a/build/integration/features/comments.feature
+++ b/build/integration/comments_features/comments.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: comments
Scenario: Creating a comment on a file belonging to myself
Given user "user0" exists
diff --git a/build/integration/composer.json b/build/integration/composer.json
index ba90c632b1b..98e093a7e69 100644
--- a/build/integration/composer.json
+++ b/build/integration/composer.json
@@ -1,10 +1,16 @@
{
- "require-dev": {
- "phpunit/phpunit": "~6.5",
- "behat/behat": "~3.8.0",
- "guzzlehttp/guzzle": "6.5.2",
- "jarnaiz/behat-junit-formatter": "^1.3",
- "sabre/dav": "3.2.3",
- "symfony/event-dispatcher": "~4.4"
- }
+ "config": {
+ "platform": {
+ "php": "8.1"
+ },
+ "sort-packages": true
+ },
+ "require-dev": {
+ "behat/behat": "^3.15",
+ "dms/phpunit-arraysubset-asserts": "^0.5",
+ "guzzlehttp/guzzle": "^7.9",
+ "phpunit/phpunit": "^9.6",
+ "sabre/dav": "^4.7",
+ "symfony/event-dispatcher": "^6.4"
+ }
}
diff --git a/build/integration/composer.lock b/build/integration/composer.lock
new file mode 100644
index 00000000000..cc8427c78eb
--- /dev/null
+++ b/build/integration/composer.lock
@@ -0,0 +1,4590 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "922eb11ee2ecd8426779897121fad75d",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "behat/behat",
+ "version": "v3.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Behat.git",
+ "reference": "c465af8756adaaa6d962c3176a0a6c594361809b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Behat/zipball/c465af8756adaaa6d962c3176a0a6c594361809b",
+ "reference": "c465af8756adaaa6d962c3176a0a6c594361809b",
+ "shasum": ""
+ },
+ "require": {
+ "behat/gherkin": "^4.12.0",
+ "composer-runtime-api": "^2.2",
+ "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0",
+ "ext-mbstring": "*",
+ "nikic/php-parser": "^4.19.2 || ^5.2",
+ "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/config": "^5.4 || ^6.4 || ^7.0",
+ "symfony/console": "^5.4 || ^6.4 || ^7.0",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
+ "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
+ "symfony/translation": "^5.4 || ^6.4 || ^7.0",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.68",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "rector/rector": "^2.0",
+ "sebastian/diff": "^4.0",
+ "symfony/polyfill-php84": "^1.31",
+ "symfony/process": "^5.4 || ^6.4 || ^7.0"
+ },
+ "suggest": {
+ "ext-dom": "Needed to output test results in JUnit format."
+ },
+ "bin": [
+ "bin/behat"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Hook\\": "src/Behat/Hook/",
+ "Behat\\Step\\": "src/Behat/Step/",
+ "Behat\\Behat\\": "src/Behat/Behat/",
+ "Behat\\Config\\": "src/Behat/Config/",
+ "Behat\\Testwork\\": "src/Behat/Testwork/",
+ "Behat\\Transformation\\": "src/Behat/Transformation/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Scenario-oriented BDD framework for PHP",
+ "homepage": "https://behat.org/",
+ "keywords": [
+ "Agile",
+ "BDD",
+ "ScenarioBDD",
+ "Scrum",
+ "StoryBDD",
+ "User story",
+ "business",
+ "development",
+ "documentation",
+ "examples",
+ "symfony",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Behat/issues",
+ "source": "https://github.com/Behat/Behat/tree/v3.23.0"
+ },
+ "time": "2025-07-15T16:58:54+00:00"
+ },
+ {
+ "name": "behat/gherkin",
+ "version": "v4.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Gherkin.git",
+ "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4",
+ "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.2",
+ "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*"
+ },
+ "require-dev": {
+ "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1",
+ "friendsofphp/php-cs-fixer": "^3.65",
+ "mikey179/vfsstream": "^1.6",
+ "phpstan/extension-installer": "^1",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpunit/phpunit": "^10.5",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.0"
+ },
+ "suggest": {
+ "symfony/yaml": "If you want to parse features, represented in YAML files"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Gherkin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "https://everzet.com"
+ }
+ ],
+ "description": "Gherkin DSL parser for PHP",
+ "homepage": "https://behat.org/",
+ "keywords": [
+ "BDD",
+ "Behat",
+ "Cucumber",
+ "DSL",
+ "gherkin",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Gherkin/issues",
+ "source": "https://github.com/Behat/Gherkin/tree/v4.14.0"
+ },
+ "time": "2025-05-23T15:06:40+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "dms/phpunit-arraysubset-asserts",
+ "version": "v0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rdohms/phpunit-arraysubset-asserts.git",
+ "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rdohms/phpunit-arraysubset-asserts/zipball/aa6b9e858414e91cca361cac3b2035ee57d212e0",
+ "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.4 || ^7.0 || ^8.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0"
+ },
+ "require-dev": {
+ "dms/coding-standard": "^9"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "assertarraysubset-autoload.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Rafael Dohms",
+ "email": "rdohms@gmail.com"
+ }
+ ],
+ "description": "This package provides ArraySubset and related asserts once deprecated in PHPUnit 8",
+ "support": {
+ "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues",
+ "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/v0.5.0"
+ },
+ "time": "2023-06-02T17:33:53+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^1.9.4",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5.27",
+ "vimeo/psalm": "^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:23:10+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.9.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T13:37:11+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T13:27:01+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-03-27T12:30:47+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-06-12T14:39:25+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
+ "reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
+ },
+ "time": "2025-05-31T08:24:38+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "sebastian/code-unit-reverse-lookup": "^2.0.3",
+ "sebastian/complexity": "^2.0.3",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/lines-of-code": "^1.0.4",
+ "sebastian/version": "^3.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:23:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.5.0 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.12.0",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.32",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.6",
+ "sebastian/global-state": "^5.0.7",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-19T10:50:18+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "sabre/dav",
+ "version": "4.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/dav.git",
+ "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
+ "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-dom": "*",
+ "ext-iconv": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-pcre": "*",
+ "ext-simplexml": "*",
+ "ext-spl": "*",
+ "lib-libxml": ">=2.7.0",
+ "php": "^7.1.0 || ^8.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "sabre/event": "^5.0",
+ "sabre/http": "^5.0.5",
+ "sabre/uri": "^2.0",
+ "sabre/vobject": "^4.2.1",
+ "sabre/xml": "^2.0.1"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.19",
+ "monolog/monolog": "^1.27 || ^2.0",
+ "phpstan/phpstan": "^0.12 || ^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest": {
+ "ext-curl": "*",
+ "ext-imap": "*",
+ "ext-pdo": "*"
+ },
+ "bin": [
+ "bin/sabredav",
+ "bin/naturalselection"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Sabre\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "WebDAV Framework for PHP",
+ "homepage": "http://sabre.io/",
+ "keywords": [
+ "CalDAV",
+ "CardDAV",
+ "WebDAV",
+ "framework",
+ "iCalendar"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/dav/issues",
+ "source": "https://github.com/fruux/sabre-dav"
+ },
+ "time": "2024-10-29T11:46:02+00:00"
+ },
+ {
+ "name": "sabre/event",
+ "version": "5.1.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/event.git",
+ "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2",
+ "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/coroutine.php",
+ "lib/Loop/functions.php",
+ "lib/Promise/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Event\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/event is a library for lightweight event-based programming",
+ "homepage": "http://sabre.io/event/",
+ "keywords": [
+ "EventEmitter",
+ "async",
+ "coroutine",
+ "eventloop",
+ "events",
+ "hooks",
+ "plugin",
+ "promise",
+ "reactor",
+ "signal"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/event/issues",
+ "source": "https://github.com/fruux/sabre-event"
+ },
+ "time": "2024-08-27T11:23:05+00:00"
+ },
+ {
+ "name": "sabre/http",
+ "version": "5.1.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/http.git",
+ "reference": "dedff73f3995578bc942fa4c8484190cac14f139"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139",
+ "reference": "dedff73f3995578bc942fa4c8484190cac14f139",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-curl": "*",
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0",
+ "sabre/event": ">=4.0 <6.0",
+ "sabre/uri": "^2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest": {
+ "ext-curl": " to make http requests with the Client class"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\HTTP\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
+ "homepage": "https://github.com/fruux/sabre-http",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/http/issues",
+ "source": "https://github.com/fruux/sabre-http"
+ },
+ "time": "2024-08-27T16:07:41+00:00"
+ },
+ {
+ "name": "sabre/uri",
+ "version": "2.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/uri.git",
+ "reference": "b76524c22de90d80ca73143680a8e77b1266c291"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
+ "reference": "b76524c22de90d80ca73143680a8e77b1266c291",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.63",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Uri\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Functions for making sense out of URIs.",
+ "homepage": "http://sabre.io/uri/",
+ "keywords": [
+ "rfc3986",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/uri/issues",
+ "source": "https://github.com/fruux/sabre-uri"
+ },
+ "time": "2024-08-27T12:18:16+00:00"
+ },
+ {
+ "name": "sabre/vobject",
+ "version": "4.5.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/vobject.git",
+ "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/vobject/zipball/900266bb3bd448a9f7f41f82344ad0aba237cb27",
+ "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0",
+ "sabre/xml": "^2.1 || ^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1",
+ "phpstan/phpstan": "^0.12 || ^1.11",
+ "phpunit/php-invoker": "^2.0 || ^3.1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest": {
+ "hoa/bench": "If you would like to run the benchmark scripts"
+ },
+ "bin": [
+ "bin/vobject",
+ "bin/generate_vcards"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sabre\\VObject\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Dominik Tobschall",
+ "email": "dominik@fruux.com",
+ "homepage": "http://tobschall.de/",
+ "role": "Developer"
+ },
+ {
+ "name": "Ivan Enderlin",
+ "email": "ivan.enderlin@hoa-project.net",
+ "homepage": "http://mnt.io/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "http://sabre.io/vobject/",
+ "keywords": [
+ "availability",
+ "freebusy",
+ "iCalendar",
+ "ical",
+ "ics",
+ "jCal",
+ "jCard",
+ "recurrence",
+ "rfc2425",
+ "rfc2426",
+ "rfc2739",
+ "rfc4770",
+ "rfc5545",
+ "rfc5546",
+ "rfc6321",
+ "rfc6350",
+ "rfc6351",
+ "rfc6474",
+ "rfc6638",
+ "rfc6715",
+ "rfc6868",
+ "vCalendar",
+ "vCard",
+ "vcf",
+ "xCal",
+ "xCard"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/vobject/issues",
+ "source": "https://github.com/fruux/sabre-vobject"
+ },
+ "time": "2024-10-14T11:53:54+00:00"
+ },
+ {
+ "name": "sabre/xml",
+ "version": "2.2.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/xml.git",
+ "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
+ "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "lib-libxml": ">=2.6.20",
+ "php": "^7.1 || ^8.0",
+ "sabre/uri": ">=1.0,<3.0.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Deserializer/functions.php",
+ "lib/Serializer/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Xml\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Markus Staab",
+ "email": "markus.staab@redaxo.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/xml is an XML library that you may not hate.",
+ "homepage": "https://sabre.io/xml/",
+ "keywords": [
+ "XMLReader",
+ "XMLWriter",
+ "dom",
+ "xml"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/xml/issues",
+ "source": "https://github.com/fruux/sabre-xml"
+ },
+ "time": "2024-09-06T07:37:46+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:33:00+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:35:11+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:07:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v6.4.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/af5917a3b1571f54689e56677a3f06440d2fe4c7",
+ "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/finder": "<5.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v6.4.22"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-14T06:00:01+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v6.4.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93",
+ "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/dotenv": "<5.4",
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/lock": "<5.4",
+ "symfony/process": "<5.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/lock": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v6.4.23"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-27T19:37:22+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v6.4.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0d9f24f3de0a83573fce5c9ed025d6306c6e166b",
+ "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^6.4.20|^7.2.5"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<6.1",
+ "symfony/finder": "<5.4",
+ "symfony/proxy-manager-bridge": "<6.3",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0",
+ "symfony/service-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "symfony/config": "^6.1|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v6.4.23"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-23T06:49:06+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v6.4.13",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e",
+ "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/event-dispatcher-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:18:03+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v6.4.13",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3",
+ "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "require-dev": {
+ "symfony/process": "^5.4|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.13"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-10-25T15:07:50+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.32.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.32.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.32.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.32.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-23T08:48:59+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-25T09:37:31+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v6.4.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "73e2c6966a5aef1d4892873ed5322245295370c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6",
+ "reference": "73e2c6966a5aef1d4892873ed5322245295370c6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^6.2|^7.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v6.4.21"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-18T15:23:29+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v6.4.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/de8afa521e04a5220e9e58a1dc99971ab7cac643",
+ "reference": "de8afa521e04a5220e9e58a1dc99971ab7cac643",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^2.5|^3.0"
+ },
+ "conflict": {
+ "symfony/config": "<5.4",
+ "symfony/console": "<5.4",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/http-kernel": "<5.4",
+ "symfony/service-contracts": "<2.5",
+ "symfony/twig-bundle": "<5.4",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3|3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^4.18|^5.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-client-contracts": "^2.5|^3.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to internationalize your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v6.4.23"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-26T21:24:02+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
+ "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-27T08:32:26+00:00"
+ },
+ {
+ "name": "symfony/var-exporter",
+ "version": "v6.4.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/f28cf841f5654955c9f88ceaf4b9dc29571988a9",
+ "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/property-access": "^6.4|^7.0",
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\VarExporter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows exporting any serializable PHP data structure to plain PHP code",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clone",
+ "construct",
+ "export",
+ "hydrate",
+ "instantiate",
+ "lazy-loading",
+ "proxy",
+ "serialize"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-exporter/tree/v6.4.22"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-14T13:00:13+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v6.4.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/93e29e0deb5f1b2e360adfb389a20d25eb81a27b",
+ "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v6.4.23"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-03T06:46:12+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.1"
+ },
+ "plugin-api-version": "2.6.0"
+}
diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml
index 79ffe58f6b6..0a3fe4fd823 100644
--- a/build/integration/config/behat.yml
+++ b/build/integration/config/behat.yml
@@ -1,13 +1,22 @@
+# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
default:
autoload:
'': "%paths.base%/../features/bootstrap"
+ formatters:
+ junit:
+ output_path: '%paths.base%/../output'
+ pretty:
+ output_styles:
+ comment: [ 'bright-blue' ]
suites:
default:
paths:
- "%paths.base%/../features"
contexts:
- FeatureContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
@@ -25,12 +34,99 @@ default:
- CommandLineContext:
baseUrl: http://localhost:8080
ocPath: ../../
+ comments:
+ paths:
+ - "%paths.base%/../comments_features"
+ contexts:
+ - FeatureContext:
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ - CommentsContext:
+ baseUrl: http://localhost:8080
+ - TagsContext:
+ baseUrl: http://localhost:8080
+ - CardDavContext:
+ baseUrl: http://localhost:8080
+ - CalDavContext:
+ baseUrl: http://localhost:8080
+ - ChecksumsContext:
+ baseUrl: http://localhost:8080
+ - CommandLineContext:
+ baseUrl: http://localhost:8080
+ ocPath: ../../
+ dav:
+ paths:
+ - "%paths.base%/../dav_features"
+ contexts:
+ - DavFeatureContext:
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ - CommentsContext:
+ baseUrl: http://localhost:8080
+ - TagsContext:
+ baseUrl: http://localhost:8080
+ - CardDavContext:
+ baseUrl: http://localhost:8080
+ - CalDavContext:
+ baseUrl: http://localhost:8080
+ - ChecksumsContext:
+ baseUrl: http://localhost:8080
+ - CommandLineContext:
+ baseUrl: http://localhost:8080
+ ocPath: ../../
+ - PrincipalPropertySearchContext:
+ baseUrl: http://localhost:8080
federation:
paths:
- "%paths.base%/../federation_features"
contexts:
- FederationContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ files:
+ paths:
+ - "%paths.base%/../files_features"
+ contexts:
+ - FeatureContext:
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ - CommentsContext:
+ baseUrl: http://localhost:8080
+ - TagsContext:
+ baseUrl: http://localhost:8080
+ - CardDavContext:
+ baseUrl: http://localhost:8080
+ - CalDavContext:
+ baseUrl: http://localhost:8080
+ - ChecksumsContext:
+ baseUrl: http://localhost:8080
+ - CommandLineContext:
+ baseUrl: http://localhost:8080
+ ocPath: ../../
+ - MetadataContext:
+ baseUrl: http://localhost:8080
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ files_conversion:
+ paths:
+ - "%paths.base%/../file_conversions"
+ contexts:
+ - ConversionsContext:
+ baseUrl: http://localhost:8080
admin:
- admin
- admin
@@ -40,7 +136,17 @@ default:
- "%paths.base%/../capabilities_features"
contexts:
- CapabilitiesContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ collaboration:
+ paths:
+ - "%paths.base%/../collaboration_features"
+ contexts:
+ - CollaborationContext:
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
@@ -50,7 +156,7 @@ default:
- "%paths.base%/../sharees_features"
contexts:
- ShareesContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
@@ -60,7 +166,18 @@ default:
- "%paths.base%/../sharing_features"
contexts:
- SharingContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ - TalkContext
+ videoverification:
+ paths:
+ - "%paths.base%/../videoverification_features"
+ contexts:
+ - SharingContext:
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
@@ -71,7 +188,7 @@ default:
- "%paths.base%/../setup_features"
contexts:
- SetupContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
@@ -96,19 +213,66 @@ default:
- admin
- admin
regular_user_password: 123456
+ openldap_numerical:
+ paths:
+ - "%paths.base%/../openldap_numerical_features"
+ contexts:
+ - LDAPContext:
+ baseUrl: http://localhost:8080
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
+ openldap:
+ paths:
+ - "%paths.base%/../openldap_features"
+ contexts:
+ - LDAPContext:
+ baseUrl: http://localhost:8080
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
remoteapi:
paths:
- "%paths.base%/../remoteapi_features"
contexts:
- FeatureContext:
- baseUrl: http://localhost:8080/ocs/
+ baseUrl: http://localhost:8080/ocs/
admin:
- admin
- admin
regular_user_password: 123456
- RemoteContext:
- remote: http://localhost:8080
- extensions:
- jarnaiz\JUnitFormatter\JUnitFormatterExtension:
- filename: report.xml
- outputDir: "%paths.base%/../output/"
+ remote: http://localhost:8080
+
+ ratelimiting:
+ paths:
+ - "%paths.base%/../ratelimiting_features"
+ contexts:
+ - RateLimitingContext:
+ baseUrl: http://localhost:8080
+ admin:
+ - admin
+ - admin
+ 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/data/bjoern.vcf.license b/build/integration/data/bjoern.vcf.license
new file mode 100644
index 00000000000..314119db8d2
--- /dev/null
+++ b/build/integration/data/bjoern.vcf.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/build/integration/data/clouds.jpg b/build/integration/data/clouds.jpg
new file mode 100644
index 00000000000..2433b140766
--- /dev/null
+++ b/build/integration/data/clouds.jpg
Binary files differ
diff --git a/build/integration/data/clouds.jpg.license b/build/integration/data/clouds.jpg.license
new file mode 100644
index 00000000000..d7c54c39d02
--- /dev/null
+++ b/build/integration/data/clouds.jpg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2019 CHUTTERSNAP <https://unsplash.com/@chuttersnap> <https://unsplash.com/photos/blue-clouds-under-white-sky-9AqIdzEc9pY>"
+SPDX-License-Identifier: LicenseRef-Unsplash
diff --git a/build/integration/data/coloured-pattern.png b/build/integration/data/coloured-pattern-non-square.png
index cf43787f3fd..cf43787f3fd 100644
--- a/build/integration/data/coloured-pattern.png
+++ b/build/integration/data/coloured-pattern-non-square.png
Binary files differ
diff --git a/build/integration/data/coloured-pattern-non-square.png.license b/build/integration/data/coloured-pattern-non-square.png.license
new file mode 100644
index 00000000000..39ee0d6eacc
--- /dev/null
+++ b/build/integration/data/coloured-pattern-non-square.png.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/build/integration/data/green-square-256.png.license b/build/integration/data/green-square-256.png.license
new file mode 100644
index 00000000000..3811666d42a
--- /dev/null
+++ b/build/integration/data/green-square-256.png.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/build/integration/data/textfile.txt.license b/build/integration/data/textfile.txt.license
new file mode 100644
index 00000000000..f7804ddc385
--- /dev/null
+++ b/build/integration/data/textfile.txt.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+SPDX-License-Identifier: AGPL-3.0-only \ No newline at end of file
diff --git a/build/integration/features/caldav.feature b/build/integration/dav_features/caldav.feature
index 2bddbc3e9e4..f324f720bbd 100644
--- a/build/integration/features/caldav.feature
+++ b/build/integration/dav_features/caldav.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: caldav
Scenario: Accessing a not existing calendar of another user
Given user "user0" exists
@@ -13,7 +15,7 @@ Feature: caldav
When "user0" requests calendar "admin/MyCalendar" on the endpoint "/remote.php/dav/calendars/"
Then The CalDAV HTTP status code should be "404"
And The exception is "Sabre\DAV\Exception\NotFound"
- And The error message is "Node with name 'MyCalendar' could not be found"
+ And The error message is "Calendar with name 'MyCalendar' could not be found"
Scenario: Accessing a not shared calendar of another user via the legacy endpoint
Given user "user0" exists
@@ -22,7 +24,7 @@ Feature: caldav
When "user0" requests calendar "admin/MyCalendar" on the endpoint "/remote.php/caldav/calendars/"
Then The CalDAV HTTP status code should be "404"
And The exception is "Sabre\DAV\Exception\NotFound"
- And The error message is "Node with name 'MyCalendar' could not be found"
+ And The error message is "Calendar with name 'MyCalendar' could not be found"
Scenario: Accessing a not existing calendar of another user
Given user "user0" exists
@@ -58,4 +60,35 @@ Feature: caldav
Then The CalDAV HTTP status code should be "202"
When "admin" requests calendar "/" on the endpoint "/remote.php/dav/public-calendars"
Then The CalDAV HTTP status code should be "207"
- Then There should be "0" calendars in the response body \ No newline at end of file
+ Then There should be "0" calendars in the response body
+
+ Scenario: Create calendar request for non-existing calendar of another user
+ Given user "user0" exists
+ When "user0" sends a create calendar request to "admin/MyCalendar2" on the endpoint "/remote.php/dav/calendars/"
+ Then The CalDAV HTTP status code should be "404"
+ And The exception is "Sabre\DAV\Exception\NotFound"
+ And The error message is "Node with name 'admin' could not be found"
+
+ Scenario: Create calendar request for existing calendar of another user
+ Given user "user0" exists
+ When "admin" creates a calendar named "MyCalendar2"
+ Then The CalDAV HTTP status code should be "201"
+ When "user0" sends a create calendar request to "admin/MyCalendar2" on the endpoint "/remote.php/dav/calendars/"
+ Then The CalDAV HTTP status code should be "404"
+ And The exception is "Sabre\DAV\Exception\NotFound"
+ And The error message is "Node with name 'admin' could not be found"
+
+ Scenario: Update a principal's schedule-default-calendar-URL
+ Given user "user0" exists
+ And "user0" creates a calendar named "MyCalendar2"
+ When "user0" updates property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" to href "/remote.php/dav/calendars/user0/MyCalendar2/" of principal "users/user0" on the endpoint "/remote.php/dav/principals/"
+ Then The CalDAV response should be multi status
+ And The CalDAV response should contain a property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL"
+ When "user0" requests principal "users/user0" on the endpoint "/remote.php/dav/principals/"
+ Then The CalDAV response should be multi status
+ And The CalDAV response should contain a property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" with a href value "/remote.php/dav/calendars/user0/MyCalendar2/"
+
+ Scenario: Should create default calendar on first login
+ Given user "first-login" exists
+ When "first-login" requests calendar "first-login/personal" on the endpoint "/remote.php/dav/calendars/"
+ Then The CalDAV HTTP status code should be "207"
diff --git a/build/integration/features/carddav.feature b/build/integration/dav_features/carddav.feature
index 16c165b6bab..35e85639817 100644
--- a/build/integration/features/carddav.feature
+++ b/build/integration/dav_features/carddav.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: carddav
Scenario: Accessing a not existing addressbook of another user
Given user "user0" exists
@@ -44,11 +46,9 @@ Feature: carddav
|Content-Type|text/vcard; charset=utf-8|
|Content-Security-Policy|default-src 'none';|
|X-Content-Type-Options |nosniff|
- |X-Download-Options|noopen|
|X-Frame-Options|SAMEORIGIN|
|X-Permitted-Cross-Domain-Policies|none|
- |X-Robots-Tag|none|
- |X-XSS-Protection|1; mode=block|
+ |X-Robots-Tag|noindex, nofollow|
Scenario: Exporting the picture of ones own contact
Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201"
@@ -59,8 +59,25 @@ Feature: carddav
|Content-Type|image/jpeg|
|Content-Security-Policy|default-src 'none';|
|X-Content-Type-Options |nosniff|
- |X-Download-Options|noopen|
|X-Frame-Options|SAMEORIGIN|
|X-Permitted-Cross-Domain-Policies|none|
- |X-Robots-Tag|none|
- |X-XSS-Protection|1; mode=block|
+ |X-Robots-Tag|noindex, nofollow|
+
+ Scenario: Create addressbook request for non-existing addressbook of another user
+ Given user "user0" exists
+ When "user0" sends a create addressbook request to "admin/MyAddressbook2" on the endpoint "/remote.php/dav/addressbooks/"
+ Then The CardDAV HTTP status code should be "404"
+ And The CardDAV exception is "Sabre\DAV\Exception\NotFound"
+ And The CardDAV error message is "File not found: admin in 'addressbooks'"
+
+ Scenario: Create addressbook request for existing addressbook of another user
+ Given user "user0" exists
+ When "admin" creates an addressbook named "MyAddressbook2" with statuscode "201"
+ When "user0" sends a create addressbook request to "admin/MyAddressbook2" on the endpoint "/remote.php/dav/addressbooks/"
+ Then The CardDAV HTTP status code should be "404"
+ And The CardDAV exception is "Sabre\DAV\Exception\NotFound"
+ And The CardDAV error message is "File not found: admin in 'addressbooks'"
+
+ Scenario: Should create default addressbook on first login
+ Given user "first-login" exists
+ Then "first-login" requests addressbook "first-login/contacts" with statuscode "207" on the endpoint "/remote.php/dav/addressbooks/users/"
diff --git a/build/integration/dav_features/dav-v2-public.feature b/build/integration/dav_features/dav-v2-public.feature
new file mode 100644
index 00000000000..a1ff85dc77b
--- /dev/null
+++ b/build/integration/dav_features/dav-v2-public.feature
@@ -0,0 +1,78 @@
+# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: dav-v2-public
+ Background:
+ Given using api version "1"
+
+ Scenario: See note to recipient in public shares
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And As an "user1"
+ And user "user1" created a folder "/testshare"
+ And as "user1" creating a share with
+ | path | testshare |
+ | shareType | 3 |
+ | permissions | 1 |
+ | note | Hello |
+ And As an "user0"
+ Given using new public dav path
+ When Requesting share note on dav endpoint
+ Then the single response should contain a property "{http://nextcloud.org/ns}note" with value "Hello"
+
+ Scenario: Downloading a file from public share with Ajax header
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And As an "user1"
+ And user "user1" created a folder "/testshare"
+ When User "user1" uploads file "data/green-square-256.png" to "/testshare/image.png"
+ And as "user1" creating a share with
+ | path | testshare |
+ | shareType | 3 |
+ | permissions | 1 |
+ And As an "user0"
+ Given using new public dav path
+ When Downloading public file "/image.png"
+ Then the downloaded file has the content of "/testshare/image.png" from "user1" data
+
+ # Test that downloading files work to ensure e.g. the viewer works or files can be downloaded
+ Scenario: Downloading a file from public share without Ajax header and disabled s2s share
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And As an "user1"
+ And user "user1" created a folder "/testshare"
+ When User "user1" uploads file "data/green-square-256.png" to "/testshare/image.png"
+ And as "user1" creating a share with
+ | path | testshare |
+ | shareType | 3 |
+ | permissions | 1 |
+ And As an "user0"
+ Given parameter "outgoing_server2server_share_enabled" of app "files_sharing" is set to "no"
+ Given using new public dav path
+ When Downloading public file "/image.png" without ajax header
+ Then the downloaded file has the content of "/testshare/image.png" from "user1" data
+
+ Scenario: Download a folder
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" created a folder "/testshare"
+ And user "user0" created a folder "/testshare/testFolder"
+ When User "user0" uploads file "data/textfile.txt" to "/testshare/testFolder/text.txt"
+ When User "user0" uploads file "data/green-square-256.png" to "/testshare/testFolder/image.png"
+ And as "user0" creating a share with
+ | path | testshare |
+ | shareType | 3 |
+ | permissions | 1 |
+ And As an "user1"
+ Given using new public dav path
+ When Downloading public folder "testFolder"
+ Then the downloaded file is a zip file
+ Then the downloaded zip file contains a folder named "testFolder/"
+ And the downloaded zip file contains a file named "testFolder/text.txt" with the contents of "/testshare/testFolder/text.txt" from "user0" data
+ And the downloaded zip file contains a file named "testFolder/image.png" with the contents of "/testshare/testFolder/image.png" from "user0" data
diff --git a/build/integration/dav_features/dav-v2.feature b/build/integration/dav_features/dav-v2.feature
new file mode 100644
index 00000000000..dbd2295497f
--- /dev/null
+++ b/build/integration/dav_features/dav-v2.feature
@@ -0,0 +1,162 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+Feature: dav-v2
+ Background:
+ Given using api version "1"
+
+ Scenario: moving a file new endpoint way
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ When User "user0" moves file "/textfile0.txt" to "/FOLDER/textfile0.txt"
+ Then the HTTP status code should be "201"
+
+ Scenario: Moving and overwriting it's parent
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ And user "user0" created a folder "/test"
+ And user "user0" created a folder "/test/test"
+ When User "user0" moves file "/test/test" to "/test"
+ Then the HTTP status code should be "403"
+
+ Scenario: download a file with range using new endpoint
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When Downloading file "/welcome.txt" with range "bytes=52-78"
+ Then Downloaded content should be "example file for developers"
+
+ Scenario: Downloading a file on the new endpoint should serve security headers
+ Given using new dav path
+ And As an "admin"
+ When Downloading file "/welcome.txt"
+ Then The following headers should be set
+ |Content-Disposition|attachment; filename*=UTF-8''welcome.txt; filename="welcome.txt"|
+ |Content-Security-Policy|default-src 'none';|
+ |X-Content-Type-Options |nosniff|
+ |X-Frame-Options|SAMEORIGIN|
+ |X-Permitted-Cross-Domain-Policies|none|
+ |X-Robots-Tag|noindex, nofollow|
+ 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
+ Given Logging in using web as "admin"
+ When Sending a "GET" to "/remote.php/dav/files/admin/welcome.txt" without requesttoken
+ Then Downloaded content should start with "Welcome to your Nextcloud account!"
+ Then the HTTP status code should be "200"
+
+ Scenario: Doing a GET with a web login should work with CSRF token on the new backend
+ Given Logging in using web as "admin"
+ When Sending a "GET" to "/remote.php/dav/files/admin/welcome.txt" with requesttoken
+ Then Downloaded content should start with "Welcome to your Nextcloud account!"
+ Then the HTTP status code should be "200"
+
+ Scenario: Download a folder
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" created a folder "/testFolder"
+ When User "user0" uploads file "data/textfile.txt" to "/testFolder/text.txt"
+ When User "user0" uploads file "data/green-square-256.png" to "/testFolder/image.png"
+ And As an "user0"
+ When Downloading folder "/testFolder"
+ Then the downloaded file is a zip file
+ Then the downloaded zip file contains a folder named "testFolder/"
+ And the downloaded zip file contains a file named "testFolder/text.txt" with the contents of "/testFolder/text.txt" from "user0" data
+ And the downloaded zip file contains a file named "testFolder/image.png" with the contents of "/testFolder/image.png" from "user0" data
+
+ Scenario: Doing a PROPFIND with a web login should not work without CSRF token on the new backend
+ Given Logging in using web as "admin"
+ When Sending a "PROPFIND" to "/remote.php/dav/files/admin/welcome.txt" without requesttoken
+ Then the HTTP status code should be "401"
+
+ Scenario: Doing a PROPFIND with a web login should work with CSRF token on the new backend
+ Given Logging in using web as "admin"
+ When Sending a "PROPFIND" to "/remote.php/dav/files/admin/welcome.txt" with requesttoken
+ Then the HTTP status code should be "207"
+
+ Scenario: Uploading a file having 0B as quota
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" has a quota of "0 B"
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/asdf.txt"
+ Then the HTTP status code should be "507"
+
+ Scenario: Uploading a file as recipient using webdav new endpoint having quota
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And user "user0" has a quota of "10 MB"
+ And user "user1" has a quota of "10 MB"
+ And As an "user1"
+ And user "user1" created a folder "/testquota"
+ And as "user1" creating a share with
+ | path | testquota |
+ | shareType | 0 |
+ | permissions | 31 |
+ | shareWith | user0 |
+ And user "user0" accepts last share
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt"
+ Then the HTTP status code should be "201"
+
+ Scenario: Uploading a file with very long filename
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" has a quota of "10 MB"
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/long-filename-with-250-characters-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.txt"
+ Then the HTTP status code should be "201"
+
+ Scenario: Uploading a file with a too long filename
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" has a quota of "10 MB"
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/long-filename-with-251-characters-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.txt"
+ Then the HTTP status code should be "400"
+
+ Scenario: Create a search query on image
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt"
+ Then Image search should work
+ And the response should be empty
+ When User "user0" uploads file "data/green-square-256.png" to "/image.png"
+ Then Image search should work
+ And the single response should contain a property "{DAV:}getcontenttype" with value "image/png"
+
+ Scenario: Create a search query on favorite
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When User "user0" uploads file "data/green-square-256.png" to "/fav_image.png"
+ Then Favorite search should work
+ And the response should be empty
+ When user "user0" favorites element "/fav_image.png"
+ Then Favorite search should work
+ And the single response should contain a property "{http://owncloud.org/ns}favorite" with value "1"
+
+ Scenario: Create a search query on favorite
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When User "user0" uploads file "data/green-square-256.png" to "/fav_image.png"
+ Then Favorite search should work
+ And the response should be empty
+ When user "user0" favorites element "/fav_image.png"
+ Then Favorite search should work
+ And the single response should contain a property "{http://owncloud.org/ns}favorite" with value "1"
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/features/webdav-related.feature b/build/integration/dav_features/webdav-related.feature
index 66652e6fa26..12fd3d44c4f 100644
--- a/build/integration/features/webdav-related.feature
+++ b/build/integration/dav_features/webdav-related.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: webdav-related
Background:
Given using api version "1"
@@ -36,6 +38,43 @@ Feature: webdav-related
Then the HTTP status code should be "204"
And Downloaded content when downloading file "/textfile0.txt" with range "bytes=0-6" should be "Welcome"
+ Scenario: Moving and overwriting it's parent
+ Given using old dav path
+ And As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ And user "user0" created a folder "/test"
+ And user "user0" created a folder "/test/test"
+ When User "user0" moves file "/test/test" to "/test"
+ Then the HTTP status code should be "403"
+
+ Scenario: Moving a file from shared folder to root folder
+ Given using old dav path
+ And user "user0" exists
+ And user "user1" exists
+ And user "user0" created a folder "/testshare"
+ And User "user0" copies file "/welcome.txt" to "/testshare/welcome.txt"
+ And as "user0" creating a share with
+ | path | testshare |
+ | shareType | 0 |
+ | shareWith | user1 |
+ When User "user1" moves file "/testshare/welcome.txt" to "/movedwelcome.txt"
+ Then As an "user1"
+ And Downloaded content when downloading file "/movedwelcome.txt" with range "bytes=0-6" should be "Welcome"
+
+ Scenario: Moving a file from root folder to shared folder
+ Given using old dav path
+ And user "user0" exists
+ And user "user1" exists
+ And user "user0" created a folder "/testshare"
+ And as "user0" creating a share with
+ | path | testshare |
+ | shareType | 0 |
+ | shareWith | user1 |
+ When User "user1" moves file "/welcome.txt" to "/testshare/movedwelcome.txt"
+ Then As an "user1"
+ And Downloaded content when downloading file "/testshare/movedwelcome.txt" with range "bytes=0-6" should be "Welcome"
+
Scenario: Moving a file to a folder with no permissions
Given using old dav path
And As an "admin"
@@ -53,7 +92,7 @@ Feature: webdav-related
And User "user0" moves file "/textfile0.txt" to "/testshare/textfile0.txt"
And the HTTP status code should be "403"
When Downloading file "/testshare/textfile0.txt"
- Then the HTTP status code should be "404"
+ Then the HTTP status code should be "404"
Scenario: Moving a file to overwrite a file in a folder with no permissions
Given using old dav path
@@ -191,10 +230,10 @@ Feature: webdav-related
And As an "user1"
And user "user1" created a folder "/testquota"
And as "user1" creating a share with
- | path | testquota |
- | shareType | 0 |
- | permissions | 31 |
- | shareWith | user0 |
+ | path | testquota |
+ | shareType | 0 |
+ | permissions | 31 |
+ | shareWith | user0 |
And user "user0" accepts last share
And As an "user0"
When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt"
@@ -249,11 +288,9 @@ Feature: webdav-related
|Content-Disposition|attachment; filename*=UTF-8''welcome.txt; filename="welcome.txt"|
|Content-Security-Policy|default-src 'none';|
|X-Content-Type-Options |nosniff|
- |X-Download-Options|noopen|
|X-Frame-Options|SAMEORIGIN|
|X-Permitted-Cross-Domain-Policies|none|
- |X-Robots-Tag|none|
- |X-XSS-Protection|1; mode=block|
+ |X-Robots-Tag|noindex, nofollow|
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
@@ -278,33 +315,6 @@ Feature: webdav-related
When Sending a "PROPFIND" to "/remote.php/webdav/welcome.txt" with requesttoken
Then the HTTP status code should be "207"
- Scenario: Upload chunked file asc
- Given user "user0" exists
- And user "user0" uploads chunk file "1" of "3" with "AAAAA" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "2" of "3" with "BBBBB" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "3" of "3" with "CCCCC" to "/myChunkedFile.txt"
- When As an "user0"
- And Downloading file "/myChunkedFile.txt"
- Then Downloaded content should be "AAAAABBBBBCCCCC"
-
- Scenario: Upload chunked file desc
- Given user "user0" exists
- And user "user0" uploads chunk file "3" of "3" with "CCCCC" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "2" of "3" with "BBBBB" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "1" of "3" with "AAAAA" to "/myChunkedFile.txt"
- When As an "user0"
- And Downloading file "/myChunkedFile.txt"
- Then Downloaded content should be "AAAAABBBBBCCCCC"
-
- Scenario: Upload chunked file random
- Given user "user0" exists
- And user "user0" uploads chunk file "2" of "3" with "BBBBB" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "3" of "3" with "CCCCC" to "/myChunkedFile.txt"
- And user "user0" uploads chunk file "1" of "3" with "AAAAA" to "/myChunkedFile.txt"
- When As an "user0"
- And Downloading file "/myChunkedFile.txt"
- Then Downloaded content should be "AAAAABBBBBCCCCC"
-
Scenario: A file that is not shared does not have a share-types property
Given user "user0" exists
And user "user0" created a folder "/test"
@@ -608,3 +618,122 @@ Feature: webdav-related
And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42"
When user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt" with size 15
Then the HTTP status code should be "201"
+
+ Scenario: Upload bulked files
+ Given user "user0" exists
+ And user "user0" uploads bulked files "A.txt" with "AAAAA" and "B.txt" with "BBBBB" and "C.txt" with "CCCCC"
+ When As an "user0"
+ Then Downloading file "/A.txt"
+ And Downloaded content should be "AAAAA"
+ And File "/A.txt" should have prop "d:getlastmodified" equal to "Fri, 18 Mar 2005 01:58:31 GMT"
+ And Downloading file "/B.txt"
+ And Downloaded content should be "BBBBB"
+ And File "/B.txt" should have prop "d:getlastmodified" equal to "Sat, 02 Jun 2040 03:57:02 GMT"
+ And Downloading file "/C.txt"
+ And Downloaded content should be "CCCCC"
+ And File "/C.txt" should have prop "d:getlastmodified" equal to "Sun, 18 Aug 2075 05:55:33 GMT"
+
+ Scenario: Creating a folder with invalid characters
+ Given using new dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And As an "user1"
+ And user "user1" created a folder "/testshare "
+ Then the HTTP status code should be "400"
+
+ @s3-multipart
+ Scenario: Upload chunked file asc with new chunking v2
+ Given using new dav path
+ And user "user0" exists
+ And user "user0" creates a file locally with "3" x 5 MB chunks
+ And user "user0" creates a new chunking v2 upload with id "chunking-42" and destination "/myChunkedFile1.txt"
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-42"
+ And user "user0" moves new chunk v2 file with id "chunking-42"
+ Then the S3 multipart upload was successful with status "201"
+ When As an "user0"
+ And Downloading file "/myChunkedFile1.txt"
+ Then Downloaded content should be the created file
+
+ @s3-multipart
+ Scenario: Upload chunked file desc with new chunking v2
+ Given using new dav path
+ And user "user0" exists
+ And user "user0" creates a file locally with "3" x 5 MB chunks
+ And user "user0" creates a new chunking v2 upload with id "chunking-42" and destination "/myChunkedFile.txt"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-42"
+ And user "user0" moves new chunk v2 file with id "chunking-42"
+ Then the S3 multipart upload was successful with status "201"
+ When As an "user0"
+ And Downloading file "/myChunkedFile.txt"
+ Then Downloaded content should be the created file
+
+ @s3-multipart
+ Scenario: Upload chunked file with random chunk sizes
+ Given using new dav path
+ And user "user0" exists
+ And user "user0" creates a new chunking v2 upload with id "chunking-random" and destination "/myChunkedFile.txt"
+ And user user0 creates the chunk 1 with a size of 5 MB
+ And user user0 creates the chunk 2 with a size of 7 MB
+ And user user0 creates the chunk 3 with a size of 9 MB
+ And user user0 creates the chunk 4 with a size of 1 MB
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "4" to id "chunking-random"
+ And user "user0" moves new chunk v2 file with id "chunking-random"
+ Then the S3 multipart upload was successful with status "201"
+ When As an "user0"
+ And Downloading file "/myChunkedFile.txt"
+ Then Downloaded content should be the created file
+
+ @s3-multipart
+ Scenario: Upload chunked file with too low chunk sizes
+ Given using new dav path
+ And user "user0" exists
+ And user "user0" creates a new chunking v2 upload with id "chunking-random" and destination "/myChunkedFile.txt"
+ And user user0 creates the chunk 1 with a size of 5 MB
+ And user user0 creates the chunk 2 with a size of 2 MB
+ And user user0 creates the chunk 3 with a size of 5 MB
+ And user user0 creates the chunk 4 with a size of 1 MB
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-random"
+ And user "user0" uploads new chunk v2 file "4" to id "chunking-random"
+ And user "user0" moves new chunk v2 file with id "chunking-random"
+ Then the upload should fail on object storage
+
+ @s3-multipart
+ Scenario: Upload chunked file with special characters with new chunking v2
+ Given using new dav path
+ And user "user0" exists
+ And user "user0" creates a file locally with "3" x 5 MB chunks
+ And user "user0" creates a new chunking v2 upload with id "chunking-42" and destination "/äöü.txt"
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-42"
+ And user "user0" moves new chunk v2 file with id "chunking-42"
+ Then the S3 multipart upload was successful with status "201"
+ When As an "user0"
+ And Downloading file "/äöü.txt"
+ Then Downloaded content should be the created file
+
+ @s3-multipart
+ Scenario: Upload chunked file with special characters in path with new chunking v2
+ Given using new dav path
+ And user "user0" exists
+ And User "user0" created a folder "üäöé"
+ And user "user0" creates a file locally with "3" x 5 MB chunks
+ And user "user0" creates a new chunking v2 upload with id "chunking-42" and destination "/üäöé/äöü.txt"
+ And user "user0" uploads new chunk v2 file "1" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "2" to id "chunking-42"
+ And user "user0" uploads new chunk v2 file "3" to id "chunking-42"
+ And user "user0" moves new chunk v2 file with id "chunking-42"
+ Then the S3 multipart upload was successful with status "201"
+ When As an "user0"
+ And Downloading file "/üäöé/äöü.txt"
+ Then Downloaded content should be the created file
diff --git a/build/integration/features/auth.feature b/build/integration/features/auth.feature
index 679b2465659..f9c8b7d0e46 100644
--- a/build/integration/features/auth.feature
+++ b/build/integration/features/auth.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: auth
Background:
diff --git a/build/integration/features/avatar.feature b/build/integration/features/avatar.feature
index f7926615c01..4c8c37fb98c 100644
--- a/build/integration/features/avatar.feature
+++ b/build/integration/features/avatar.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: avatar
Background:
@@ -8,7 +10,7 @@ Feature: avatar
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
Scenario: get default user avatar as an anonymous user
@@ -16,37 +18,69 @@ Feature: avatar
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
- Scenario: get temporary user avatar before cropping it
+ Scenario: get temporary non-square user avatar before cropping it
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/green-square-256.png"
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user gets temporary avatar
Then The following headers should be set
| Content-Type | image/png |
# "last avatar" also includes the last temporary avatar
- And last avatar is a square of size 256
- And last avatar is a single "#00FF00" color
+ And last avatar is not a square
+ And last avatar is not a single color
- Scenario: get user avatar before cropping it
+ Scenario: get non-square user avatar before cropping it
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/green-square-256.png"
- # Avatar needs to be cropped to finish setting it even if it is squared
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
+ # Avatar needs to be cropped to finish setting it
When user "user0" gets avatar for user "user0"
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
+ Scenario: set square user avatar from file
+ Given Logging in using web as "user0"
+ When logged in user posts temporary avatar from file "data/green-square-256.png"
+ And user "user0" gets avatar for user "user0"
+ And The following headers should be set
+ | Content-Type | image/png |
+ | X-NC-IsCustomAvatar | 1 |
+ # Last avatar size is 512 by default when getting avatar without size parameter
+ And last avatar is a square of size 512
+ And last avatar is a single "#00FF00" color
+ And user "anonymous" gets avatar for user "user0"
+ And The following headers should be set
+ | Content-Type | image/png |
+ | X-NC-IsCustomAvatar | 1 |
+ And last avatar is a square of size 512
+ And last avatar is a single "#00FF00" color
+ Scenario: set square user avatar from internal path
+ Given user "user0" uploads file "data/green-square-256.png" to "/internal-green-square-256.png"
+ And Logging in using web as "user0"
+ When logged in user posts temporary avatar from internal path "internal-green-square-256.png"
+ And user "user0" gets avatar for user "user0" with size "64"
+ And The following headers should be set
+ | Content-Type | image/png |
+ | X-NC-IsCustomAvatar | 1 |
+ And last avatar is a square of size 64
+ And last avatar is a single "#00FF00" color
+ And user "anonymous" gets avatar for user "user0" with size "64"
+ And The following headers should be set
+ | Content-Type | image/png |
+ | X-NC-IsCustomAvatar | 1 |
+ And last avatar is a square of size 64
+ And last avatar is a single "#00FF00" color
- Scenario: set user avatar from file
+ Scenario: set non-square user avatar from file
Given Logging in using web as "user0"
- When logged in user posts temporary avatar from file "data/coloured-pattern.png"
+ When logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
@@ -57,19 +91,19 @@ Feature: avatar
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
- Scenario: set user avatar from internal path
- Given user "user0" uploads file "data/coloured-pattern.png" to "/internal-coloured-pattern.png"
+ Scenario: set non-square user avatar from internal path
+ Given user "user0" uploads file "data/coloured-pattern-non-square.png" to "/internal-coloured-pattern-non-square.png"
And Logging in using web as "user0"
- When logged in user posts temporary avatar from internal path "internal-coloured-pattern.png"
+ When logged in user posts temporary avatar from internal path "internal-coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 704 |
| y | 320 |
@@ -91,7 +125,7 @@ Feature: avatar
Scenario: cropped user avatar needs to be squared
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/coloured-pattern.png"
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
When logged in user crops temporary avatar with 400
| x | 384 |
| y | 256 |
@@ -102,7 +136,7 @@ Feature: avatar
Scenario: delete user avatar
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/coloured-pattern.png"
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
@@ -112,33 +146,33 @@ Feature: avatar
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
When logged in user deletes the user avatar
Then user "user0" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
And user "anonymous" gets avatar for user "user0"
And The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 0 |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
Scenario: get user avatar with a larger size than the original one
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/coloured-pattern.png"
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
@@ -148,12 +182,12 @@ Feature: avatar
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 192
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
Scenario: get user avatar with a smaller size than the original one
Given Logging in using web as "user0"
- And logged in user posts temporary avatar from file "data/coloured-pattern.png"
+ And logged in user posts temporary avatar from file "data/coloured-pattern-non-square.png"
And logged in user crops temporary avatar
| x | 384 |
| y | 256 |
@@ -163,7 +197,7 @@ Feature: avatar
Then The following headers should be set
| Content-Type | image/png |
| X-NC-IsCustomAvatar | 1 |
- And last avatar is a square of size 96
+ And last avatar is a square of size 512
And last avatar is a single "#FF0000" color
@@ -172,12 +206,12 @@ Feature: avatar
When user "user0" gets avatar for guest "guest0"
Then The following headers should be set
| Content-Type | image/png |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
Scenario: get default guest avatar as an anonymous user
When user "anonymous" gets avatar for guest "guest0"
Then The following headers should be set
| Content-Type | image/png |
- And last avatar is a square of size 128
+ And last avatar is a square of size 512
And last avatar is not a single color
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 772370a85ba..e8580ed537b 100644
--- a/build/integration/features/bootstrap/AppConfiguration.php
+++ b/build/integration/features/bootstrap/AppConfiguration.php
@@ -1,29 +1,9 @@
<?php
+
/**
- *
- *
- * @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) <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;
@@ -96,9 +76,9 @@ trait AppConfiguration {
$this->sendingTo('get', '/cloud/apps?filter=enabled');
$this->theHTTPStatusCodeShouldBe('200');
if ($enabled) {
- Assert::assertContains('testing', $this->response->getBody()->getContents());
+ Assert::assertStringContainsString('testing', $this->response->getBody()->getContents());
} else {
- Assert::assertNotContains('testing', $this->response->getBody()->getContents());
+ Assert::assertStringNotContainsString('testing', $this->response->getBody()->getContents());
}
}
diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php
index c621ef3572d..aeaade85383 100644
--- a/build/integration/features/bootstrap/Auth.php
+++ b/build/integration/features/bootstrap/Auth.php
@@ -1,35 +1,14 @@
<?php
+
/**
- *
- *
- * @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';
@@ -225,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, [
@@ -244,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 388715340c6..beebf1c024a 100644
--- a/build/integration/features/bootstrap/Avatar.php
+++ b/build/integration/features/bootstrap/Avatar.php
@@ -1,34 +1,16 @@
<?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;
require __DIR__ . '/../../vendor/autoload.php';
trait Avatar {
-
- /** @var string **/
+ /** @var string * */
private $lastAvatar;
/** @AfterScenario **/
@@ -110,7 +92,7 @@ trait Avatar {
* @param string $source
*/
public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
- $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r'));
+ $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
$this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
[
@@ -173,13 +155,22 @@ trait Avatar {
* @param string size
*/
public function lastAvatarIsASquareOfSize(string $size) {
- list($width, $height) = getimagesizefromstring($this->lastAvatar);
+ [$width, $height] = getimagesizefromstring($this->lastAvatar);
- Assert::assertEquals($width, $height, 'Avatar is not a square');
+ Assert::assertEquals($width, $height, 'Expected avatar to be a square');
Assert::assertEquals($size, $width);
}
/**
+ * @Then last avatar is not a square
+ */
+ public function lastAvatarIsNotASquare() {
+ [$width, $height] = getimagesizefromstring($this->lastAvatar);
+
+ Assert::assertNotEquals($width, $height, 'Expected avatar to not be a square');
+ }
+
+ /**
* @Then last avatar is not a single color
*/
public function lastAvatarIsNotASingleColor() {
@@ -250,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;
}
@@ -261,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 5b01e80707d..59a4312913e 100644
--- a/build/integration/features/bootstrap/BasicStructure.php
+++ b/build/integration/features/bootstrap/BasicStructure.php
@@ -1,41 +1,15 @@
<?php
+
/**
- *
- *
- * @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) <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;
@@ -46,6 +20,7 @@ trait BasicStructure {
use Avatar;
use Download;
use Mail;
+ use Theming;
/** @var string */
private $currentUser = '';
@@ -74,7 +49,6 @@ trait BasicStructure {
protected $remoteBaseUrl;
public function __construct($baseUrl, $admin, $regular_user_password) {
-
// Initialize your context here
$this->baseUrl = $baseUrl;
$this->adminUser = $admin;
@@ -149,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;
}
/**
@@ -181,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();
@@ -199,6 +177,42 @@ trait BasicStructure {
$this->response = $client->request($verb, $fullUrl, $options);
} catch (ClientException $ex) {
$this->response = $ex->getResponse();
+ } catch (ServerException $ex) {
+ $this->response = $ex->getResponse();
+ }
+ }
+
+ /**
+ * @param string $verb
+ * @param string $url
+ * @param TableNode|array|null $body
+ * @param array $headers
+ */
+ protected function sendRequestForJSON(string $verb, string $url, $body = null, array $headers = []): void {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = ['admin', 'admin'];
+ } elseif (strpos($this->currentUser, 'anonymous') !== 0) {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
+ if ($body instanceof TableNode) {
+ $fd = $body->getRowsHash();
+ $options['form_params'] = $fd;
+ } elseif (is_array($body)) {
+ $options['form_params'] = $body;
+ }
+
+ $options['headers'] = array_merge($headers, [
+ 'OCS-ApiRequest' => 'true',
+ 'Accept' => 'application/json',
+ ]);
+
+ try {
+ $this->response = $client->{$verb}($fullUrl, $options);
+ } catch (ClientException $ex) {
+ $this->response = $ex->getResponse();
}
}
@@ -274,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(
@@ -297,6 +312,9 @@ trait BasicStructure {
'requesttoken' => $this->requestToken,
],
'cookies' => $this->cookieJar,
+ 'headers' => [
+ 'Origin' => $baseUrl,
+ ],
]
);
$this->extracRequestTokenFromResponse($response);
@@ -322,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();
@@ -410,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);
}
@@ -453,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");
}
/**
@@ -473,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');
}
}
@@ -492,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) {
@@ -504,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 b1981568e44..459c35089fa 100644
--- a/build/integration/features/bootstrap/CalDavContext.php
+++ b/build/integration/features/bootstrap/CalDavContext.php
@@ -1,37 +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;
@@ -61,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,
@@ -70,6 +51,9 @@ class CalDavContext implements \Behat\Behat\Context\Context {
'admin',
'admin',
],
+ 'headers' => [
+ 'X-NC-CalDAV-No-Trashbin' => '1',
+ ]
]
);
} catch (\GuzzleHttp\Exception\ClientException $e) {
@@ -103,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
@@ -170,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(
@@ -193,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(
@@ -231,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 aa5a040161b..7d09ab6ddcf 100644
--- a/build/integration/features/bootstrap/CapabilitiesContext.php
+++ b/build/integration/features/bootstrap/CapabilitiesContext.php
@@ -1,30 +1,9 @@
<?php
+
/**
- *
- *
- * @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) <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 1f555ed8140..733c98dca02 100644
--- a/build/integration/features/bootstrap/CardDavContext.php
+++ b/build/integration/features/bootstrap/CardDavContext.php
@@ -1,36 +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;
@@ -129,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(
@@ -142,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>',
@@ -209,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(
@@ -242,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 {
@@ -268,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 {
@@ -312,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 e8cfdee2eea..c8abf91127e 100644
--- a/build/integration/features/bootstrap/ChecksumsContext.php
+++ b/build/integration/features/bootstrap/ChecksumsContext.php
@@ -1,28 +1,9 @@
<?php
+
/**
- *
- *
- * @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;
@@ -79,7 +60,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context {
* @param string $checksum
*/
public function userUploadsFileToWithChecksum($user, $source, $destination, $checksum) {
- $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r'));
+ $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
try {
$this->response = $this->client->put(
$this->baseUrl . '/remote.php/webdav' . $destination,
@@ -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
new file mode 100644
index 00000000000..27fa1795c5d
--- /dev/null
+++ b/build/integration/features/bootstrap/CollaborationContext.php
@@ -0,0 +1,206 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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;
+use GuzzleHttp\Client;
+use PHPUnit\Framework\Assert;
+
+require __DIR__ . '/../../vendor/autoload.php';
+
+class CollaborationContext implements Context {
+ use Provisioning;
+ use AppConfiguration;
+ use WebDav;
+
+ /**
+ * @Then /^get autocomplete for "([^"]*)"$/
+ * @param TableNode|null $formData
+ */
+ public function getAutocompleteForUser(string $search, TableNode $formData): void {
+ $this->getAutocompleteWithType(0, $search, $formData);
+ }
+
+ /**
+ * @Then /^get email autocomplete for "([^"]*)"$/
+ * @param TableNode|null $formData
+ */
+ public function getAutocompleteForEmail(string $search, TableNode $formData): void {
+ $this->getAutocompleteWithType(4, $search, $formData);
+ }
+
+ private function getAutocompleteWithType(int $type, string $search, TableNode $formData): void {
+ $query = $search === 'null' ? null : $search;
+
+ $this->sendRequestForJSON('GET', '/core/autocomplete/get?itemType=files&itemId=123&shareTypes[]=' . $type . '&search=' . $query, [
+ 'itemType' => 'files',
+ 'itemId' => '123',
+ 'search' => $query,
+ ]);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $data = json_decode($this->response->getBody()->getContents(), true);
+ $suggestions = $data['ocs']['data'];
+
+ Assert::assertCount(count($formData->getHash()), $suggestions, 'Suggestion count does not match');
+ Assert::assertEquals($formData->getHash(), array_map(static function ($suggestion, $expected) {
+ $data = [];
+ if (isset($expected['id'])) {
+ $data['id'] = $suggestion['id'];
+ }
+ if (isset($expected['source'])) {
+ $data['source'] = $suggestion['source'];
+ }
+ if (isset($expected['status'])) {
+ $data['status'] = json_encode($suggestion['status']);
+ }
+ return $data;
+ }, $suggestions, $formData->getHash()));
+ }
+
+ /**
+ * @Given /^there is a contact in an addressbook$/
+ */
+ public function thereIsAContactInAnAddressbook() {
+ $this->usingNewDavPath();
+ 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');
+ } 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();
+ }
+
+ try {
+ $destination = '/users/admin/myaddressbook/contact1.vcf';
+ $data = <<<EOF
+BEGIN:VCARD
+VERSION:4.0
+PRODID:-//Nextcloud Contacts v4.0.2
+UID:a0f4088a-4dca-4308-9b63-09a1ebcf78f3
+FN:A person
+ADR;TYPE=HOME:;;;;;;
+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');
+ } 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();
+ }
+ }
+
+ protected function resetAppConfigs(): void {
+ $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_userid');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_email');
+ $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn');
+ $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members');
+ }
+
+ /**
+ * @Given /^user "([^"]*)" has status "([^"]*)"$/
+ * @param string $user
+ * @param string $status
+ */
+ public function assureUserHasStatus($user, $status) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status/status";
+ $client = new Client();
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
+ if ($user === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$user, $this->regularUser];
+ }
+
+ $options['form_params'] = [
+ 'statusType' => $status
+ ];
+
+ $this->response = $client->put($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/user_status";
+ unset($options['form_params']);
+ $this->response = $client->get($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $returnedStatus = json_decode(json_encode(simplexml_load_string($this->response->getBody()->getContents())->data), true)['status'];
+ Assert::assertEquals($status, $returnedStatus);
+ }
+
+ /**
+ * @param string $user
+ * @return null|array
+ */
+ public function getStatusList(string $user): ?array {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/user_status/api/v1/statuses";
+ $client = new Client();
+ $options = [
+ 'headers' => [
+ 'OCS-APIREQUEST' => 'true',
+ ],
+ ];
+ if ($user === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$user, $this->regularUser];
+ }
+
+ $this->response = $client->get($fullUrl, $options);
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $contents = $this->response->getBody()->getContents();
+ return json_decode(json_encode(simplexml_load_string($contents)->data), true);
+ }
+
+ /**
+ * @Given /^user statuses for "([^"]*)" list "([^"]*)" with status "([^"]*)"$/
+ * @param string $user
+ * @param string $statusUser
+ * @param string $status
+ */
+ public function assertStatusesList(string $user, string $statusUser, string $status): void {
+ $statusList = $this->getStatusList($user);
+ Assert::assertArrayHasKey('element', $statusList, 'Returned status list empty or broken');
+ if (array_key_exists('userId', $statusList['element'])) {
+ // If only one user has a status set, the API returns their status directly
+ Assert::assertArrayHasKey('status', $statusList['element'], 'Returned status list empty or broken');
+ $filteredStatusList = [ $statusList['element']['userId'] => $statusList['element']['status'] ];
+ } else {
+ // If more than one user have their status set, the API returns an array of their statuses
+ $filteredStatusList = array_column($statusList['element'], 'status', 'userId');
+ }
+ Assert::assertArrayHasKey($statusUser, $filteredStatusList, 'User not listed in statuses: ' . $statusUser);
+ Assert::assertEquals($status, $filteredStatusList[$statusUser]);
+ }
+
+ /**
+ * @Given /^user statuses for "([^"]*)" are empty$/
+ * @param string $user
+ */
+ public function assertStatusesEmpty(string $user): void {
+ $statusList = $this->getStatusList($user);
+ Assert::assertEmpty($statusList);
+ }
+}
diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php
index fa773ea91cd..924d723daa6 100644
--- a/build/integration/features/bootstrap/CommandLine.php
+++ b/build/integration/features/bootstrap/CommandLine.php
@@ -1,29 +1,10 @@
<?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;
require __DIR__ . '/../../vendor/autoload.php';
@@ -142,13 +123,13 @@ trait CommandLine {
* @Then /^the command output contains the text "([^"]*)"$/
*/
public function theCommandOutputContainsTheText($text) {
- Assert::assertContains($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
+ Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
}
/**
* @Then /^the command error output contains the text "([^"]*)"$/
*/
public function theCommandErrorOutputContainsTheText($text) {
- Assert::assertContains($text, $this->lastStdErr, 'The command did not output the expected text on stderr');
+ Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stderr');
}
}
diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php
index 2c434a1b89f..e7764356270 100644
--- a/build/integration/features/bootstrap/CommandLineContext.php
+++ b/build/integration/features/bootstrap/CommandLineContext.php
@@ -1,32 +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;
@@ -36,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, '/') . '/';
@@ -60,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) {
@@ -70,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) {
@@ -98,7 +85,7 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
/**
- * @When /^transferring ownership from "([^"]+)" to "([^"]+)"/
+ * @When /^transferring ownership from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnership($user1, $user2) {
if ($this->runOcc(['files:transfer-ownership', $user1, $user2]) === 0) {
@@ -110,7 +97,7 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
/**
- * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"/
+ * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"$/
*/
public function transferringOwnershipPath($path, $user1, $user2) {
$path = '--path=' . $path;
@@ -122,7 +109,6 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
}
}
-
/**
* @When /^using received transfer folder of "([^"]+)" as dav path$/
*/
@@ -136,6 +122,6 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
* @Then /^transfer folder name contains "([^"]+)"$/
*/
public function transferFolderNameContains($text) {
- Assert::assertContains($text, $this->lastTransferPath);
+ Assert::assertStringContainsString($text, $this->lastTransferPath);
}
}
diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php
index ebd7d5697e5..53001b1c204 100644
--- a/build/integration/features/bootstrap/CommentsContext.php
+++ b/build/integration/features/bootstrap/CommentsContext.php
@@ -1,30 +1,10 @@
<?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';
class CommentsContext implements \Behat\Behat\Context\Context {
@@ -50,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();
@@ -128,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() . ')');
}
}
@@ -170,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'];
+ $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0);
}
}
@@ -228,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() . ')');
}
}
@@ -239,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)) {
@@ -264,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 . ')');
}
}
@@ -294,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
new file mode 100644
index 00000000000..f6bf6b9422b
--- /dev/null
+++ b/build/integration/features/bootstrap/ContactsMenu.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+use PHPUnit\Framework\Assert;
+
+trait ContactsMenu {
+ // BasicStructure trait is expected to be used in the class that uses this
+ // trait.
+
+ /**
+ * @When /^searching for contacts matching with "([^"]*)"$/
+ *
+ * @param string $filter
+ */
+ public function searchingForContactsMatchingWith(string $filter) {
+ $url = '/index.php/contactsmenu/contacts';
+
+ $parameters[] = 'filter=' . $filter;
+
+ $url .= '?' . implode('&', $parameters);
+
+ $this->sendingAToWithRequesttoken('POST', $url);
+ }
+
+ /**
+ * @Then /^the list of searched contacts has "(\d+)" contacts$/
+ */
+ public function theListOfSearchedContactsHasContacts(int $count) {
+ $this->theHTTPStatusCodeShouldBe(200);
+
+ $searchedContacts = json_decode($this->response->getBody(), $asAssociativeArray = true)['contacts'];
+
+ Assert::assertEquals($count, count($searchedContacts));
+ }
+
+ /**
+ * @Then /^searched contact "(\d+)" is named "([^"]*)"$/
+ *
+ * @param int $index
+ * @param string $expectedName
+ */
+ public function searchedContactXIsNamed(int $index, string $expectedName) {
+ $searchedContacts = json_decode($this->response->getBody(), $asAssociativeArray = true)['contacts'];
+ $searchedContact = $searchedContacts[$index];
+
+ Assert::assertEquals($expectedName, $searchedContact['fullName']);
+ }
+}
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 71a96eb585f..549a033346e 100644
--- a/build/integration/features/bootstrap/Download.php
+++ b/build/integration/features/bootstrap/Download.php
@@ -1,34 +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) <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 **/
@@ -40,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);
@@ -58,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'
);
}
@@ -73,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'
);
}
@@ -97,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'];
@@ -117,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'
);
}
@@ -137,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 1cc1a5fe7a7..32387869edd 100644
--- a/build/integration/features/bootstrap/FakeSMTPHelper.php
+++ b/build/integration/features/bootstrap/FakeSMTPHelper.php
@@ -1,25 +1,8 @@
<?php
+
/**
- *
- *
- * @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 e9c486daa4d..ab37556f931 100644
--- a/build/integration/features/bootstrap/FeatureContext.php
+++ b/build/integration/features/bootstrap/FeatureContext.php
@@ -1,39 +1,29 @@
<?php
+
/**
- *
- *
- * @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 41581110bdf..95dc8119ad6 100644
--- a/build/integration/features/bootstrap/FederationContext.php
+++ b/build/integration/features/bootstrap/FederationContext.php
@@ -1,33 +1,14 @@
<?php
+
/**
- *
- *
- * @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';
@@ -37,6 +18,43 @@ require __DIR__ . '/../../vendor/autoload.php';
class FederationContext implements Context, SnippetAcceptingContext {
use WebDav;
use AppConfiguration;
+ use CommandLine;
+
+ /** @var string */
+ private static $phpFederatedServerPid = '';
+
+ /** @var string */
+ private $lastAcceptedRemoteShareId;
+
+ /**
+ * @BeforeScenario
+ * @AfterScenario
+ *
+ * The server is started also after the scenarios to ensure that it is
+ * properly cleaned up if stopped.
+ */
+ public function startFederatedServer() {
+ if (self::$phpFederatedServerPid !== '') {
+ return;
+ }
+
+ $port = getenv('PORT_FED');
+
+ self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
+ }
+
+ /**
+ * @BeforeScenario
+ */
+ public function cleanupRemoteStorages() {
+ // Ensure that dangling remote storages from previous tests will not
+ // interfere with the current scenario.
+ // The storages must be cleaned before each scenario; they can not be
+ // cleaned after each scenario, as this hook is executed before the hook
+ // that removes the users, so the shares would be still valid and thus
+ // the storages would not be dangling yet.
+ $this->runOcc(['sharing:cleanup-remote-storages']);
+ }
/**
* @Given /^User "([^"]*)" from server "(LOCAL|REMOTE)" shares "([^"]*)" with user "([^"]*)" from server "(LOCAL|REMOTE)"$/
@@ -48,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);
@@ -69,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);
@@ -80,6 +98,37 @@ class FederationContext implements Context, SnippetAcceptingContext {
}
/**
+ * @Then remote share :count is returned with
+ *
+ * @param int $number
+ * @param TableNode $body
+ */
+ public function remoteShareXIsReturnedWith(int $number, TableNode $body) {
+ $this->theHTTPStatusCodeShouldBe('200');
+ $this->theOCSStatusCodeShouldBe('100');
+
+ if (!($body instanceof TableNode)) {
+ return;
+ }
+
+ $returnedShare = $this->getXmlResponse()->data[0];
+ if ($returnedShare->element) {
+ $returnedShare = $returnedShare->element[$number];
+ }
+
+ $defaultExpectedFields = [
+ 'id' => 'A_NUMBER',
+ 'remote_id' => 'A_NUMBER',
+ 'accepted' => '1',
+ ];
+ $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash());
+
+ foreach ($expectedFields as $field => $value) {
+ $this->assertFieldIsInReturnedShare($field, $value, $returnedShare);
+ }
+ }
+
+ /**
* @When /^User "([^"]*)" from server "(LOCAL|REMOTE)" accepts last pending share$/
* @param string $user
* @param string $server
@@ -87,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;
@@ -95,10 +144,78 @@ class FederationContext implements Context, SnippetAcceptingContext {
$this->theHTTPStatusCodeShouldBe('200');
$this->theOCSStatusCodeShouldBe('100');
$this->usingServer($previous);
+
+ $this->lastAcceptedRemoteShareId = $share_id;
+ }
+
+ /**
+ * @When /^user "([^"]*)" deletes last accepted remote share$/
+ * @param string $user
+ */
+ public function deleteLastAcceptedRemoteShare($user) {
+ $this->asAn($user);
+ $this->sendingToWith('DELETE', '/apps/files_sharing/api/v1/remote_shares/' . $this->lastAcceptedRemoteShareId, null);
+ }
+
+ /**
+ * @When /^remote server is stopped$/
+ */
+ public function remoteServerIsStopped() {
+ if (self::$phpFederatedServerPid === '') {
+ return;
+ }
+
+ exec('kill ' . self::$phpFederatedServerPid);
+
+ 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 a596cbf5be8..0c437f28a72 100644
--- a/build/integration/features/bootstrap/FilesDropContext.php
+++ b/build/integration/features/bootstrap/FilesDropContext.php
@@ -1,27 +1,8 @@
<?php
+
/**
- *
- *
- * @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,13 +26,17 @@ 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',
];
- $options['body'] = \GuzzleHttp\Psr7\stream_for($content);
+
+ if ($nickname) {
+ $options['headers']['X-NC-NICKNAME'] = $nickname;
+ }
+
+ $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content);
try {
$this->response = $client->request('PUT', $fullUrl, $options);
@@ -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 78276f13828..986dced77a1 100644
--- a/build/integration/features/bootstrap/LDAPContext.php
+++ b/build/integration/features/bootstrap/LDAPContext.php
@@ -1,29 +1,9 @@
<?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) <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;
use PHPUnit\Framework\Assert;
@@ -105,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'],
@@ -142,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 d347636c8aa..d48ed6399c5 100644
--- a/build/integration/features/bootstrap/Mail.php
+++ b/build/integration/features/bootstrap/Mail.php
@@ -1,28 +1,10 @@
<?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
// trait.
@@ -39,7 +21,7 @@ trait Mail {
return;
}
- exec("kill " . $this->fakeSmtpServerPid);
+ exec('kill ' . $this->fakeSmtpServerPid);
$this->invokingTheCommand('config:system:delete mail_smtpport');
}
@@ -52,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 0ec19f27c60..935ad2a4a1d 100644
--- a/build/integration/features/bootstrap/Provisioning.php
+++ b/build/integration/features/bootstrap/Provisioning.php
@@ -1,36 +1,12 @@
<?php
+
/**
- *
- *
- * @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) <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>
- *
- * @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,13 +127,17 @@ 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";
$client = new Client();
$options = [];
- $options['auth'] = $this->adminUser;
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
$options['headers'] = [
'OCS-APIREQUEST' => 'true',
];
@@ -165,21 +145,119 @@ trait Provisioning {
$response = $client->get($fullUrl, $options);
foreach ($settings->getRows() as $setting) {
$value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
+ 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), 'Data wrong for field: ' . $setting[0]);
+ }
+ } elseif (isset($value[0])) {
+ Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]);
+ } else {
+ 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]);
+ }
+ }
+ }
+
+ /**
+ * @Then /^group "([^"]*)" has$/
+ *
+ * @param string $user
+ * @param TableNode|null $settings
+ */
+ public function groupHasSetting($group, $settings) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group";
+ $client = new Client();
+ $options = [];
+ if ($this->currentUser === 'admin') {
+ $options['auth'] = $this->adminUser;
+ } else {
+ $options['auth'] = [$this->currentUser, $this->regularUser];
+ }
+ $options['headers'] = [
+ 'OCS-APIREQUEST' => 'true',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ $groupDetails = simplexml_load_string($response->getBody())->data[0]->groups[0]->element;
+ foreach ($settings->getRows() as $setting) {
+ $value = json_decode(json_encode($groupDetails->{$setting[0]}), 1);
if (isset($value[0])) {
- Assert::assertEquals($setting[1], $value[0], "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($setting[1], $value[0]);
} else {
Assert::assertEquals('', $setting[1]);
}
}
}
+
+ /**
+ * @Then /^user "([^"]*)" has editable fields$/
+ *
+ * @param string $user
+ * @param TableNode|null $fields
+ */
+ public function userHasEditableFields($user, $fields) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields";
+ if ($user !== 'self') {
+ $fullUrl .= '/' . $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',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ $fieldsArray = json_decode(json_encode(simplexml_load_string($response->getBody())->data->element), 1);
+
+ $expectedFields = $fields->getRows();
+ $expectedFields = $this->simplifyArray($expectedFields);
+ Assert::assertEquals($expectedFields, $fieldsArray);
+ }
+
/**
* @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 = [];
@@ -206,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;
@@ -214,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;
@@ -222,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;
@@ -230,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;
@@ -299,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);
@@ -328,7 +406,7 @@ trait Provisioning {
$this->response = $client->get($fullUrl, $options);
$groups = [$group];
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
- Assert::assertNotEquals($groups, $respondedArray, "", 0.0, 10, true);
+ Assert::assertNotEqualsCanonicalizing($groups, $respondedArray);
Assert::assertEquals(200, $this->response->getStatusCode());
}
@@ -478,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;
}
@@ -499,7 +577,7 @@ trait Provisioning {
return;
}
$previous_user = $this->currentUser;
- $this->currentUser = "admin";
+ $this->currentUser = 'admin';
$this->deletingTheGroup($group);
$this->currentUser = $previous_user;
try {
@@ -580,23 +658,23 @@ 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);
- Assert::assertEquals($usersSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($usersSimplified, $respondedArray);
}
}
/**
* @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);
@@ -606,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);
@@ -620,46 +698,46 @@ 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);
- Assert::assertEquals($groupsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray);
}
}
/**
* @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);
- Assert::assertEquals($groupsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray);
}
}
/**
* @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);
- Assert::assertEquals($appsSimplified, $respondedArray, "", 0.0, 10, true);
+ Assert::assertEqualsCanonicalizing($appsSimplified, $respondedArray);
}
}
/**
* @Then /^subadmin users returned are$/
- * @param \Behat\Gherkin\Node\TableNode|null $groupsList
+ * @param TableNode|null $groupsList
*/
public function theSubadminUsersShouldBe($groupsList) {
$this->theSubadminGroupsShouldBe($groupsList);
@@ -731,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') {
@@ -752,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') {
@@ -776,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') {
@@ -829,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);
}
/**
@@ -838,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);
}
/**
@@ -902,4 +980,38 @@ trait Provisioning {
}
$this->usingServer($previousServer);
}
+
+ /**
+ * @Then /^user "([^"]*)" has not$/
+ */
+ public function userHasNotSetting($user, TableNode $settings) {
+ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$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',
+ ];
+
+ $response = $client->get($fullUrl, $options);
+ foreach ($settings->getRows() as $setting) {
+ $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1);
+ if (isset($value[0])) {
+ if (in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
+ $expectedValues = explode(';', $setting[1]);
+ foreach ($expectedValues as $expected) {
+ Assert::assertFalse(in_array($expected, $value, true));
+ }
+ } else {
+ Assert::assertNotEqualsCanonicalizing($setting[1], $value[0]);
+ }
+ } else {
+ Assert::assertNotEquals('', $setting[1]);
+ }
+ }
+ }
}
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 38f3ab76487..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 John Molakvoæ (skjnldsv) <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) {
@@ -138,7 +121,13 @@ class RemoteContext implements Context {
* @param string $value
*/
public function hasCapability($key, $value) {
- $capabilities = $this->getApiClient()->getCapabilities();
+ try {
+ $capabilities = $this->getApiClient()->getCapabilities();
+ } catch (\Exception $e) {
+ Assert::assertInstanceOf($value, $e);
+ $this->lastException = $e;
+ return;
+ }
$current = $capabilities;
$parts = explode('.', $key);
foreach ($parts as $part) {
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 72a20a08ce8..49a4fe92822 100644
--- a/build/integration/features/bootstrap/Search.php
+++ b/build/integration/features/bootstrap/Search.php
@@ -1,31 +1,13 @@
<?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;
trait Search {
-
// BasicStructure trait is expected to be used in the class that uses this
// trait.
diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php
index 39925374ac4..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 c1db2d57e05..37e0e63e547 100644
--- a/build/integration/features/bootstrap/ShareesContext.php
+++ b/build/integration/features/bootstrap/ShareesContext.php
@@ -1,26 +1,9 @@
<?php
+
/**
- *
- *
- * @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 c2d16f19e75..0cc490ff110 100644
--- a/build/integration/features/bootstrap/Sharing.php
+++ b/build/integration/features/bootstrap/Sharing.php
@@ -1,35 +1,9 @@
<?php
+
/**
- *
- *
- * @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) <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,19 +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) {
+ $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 = [
@@ -309,10 +290,14 @@ trait Sharing {
$body['permissions'] = $permissions;
}
+ if ($viewOnly === true) {
+ $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();
@@ -323,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 {
@@ -342,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;
}
@@ -402,13 +391,17 @@ trait Sharing {
}
/**
- * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with user "([^"]*)"( with permissions ([\d]*))?$/
+ * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with user "([^"]*)"( with permissions ([\d]*))?( view-only)?$/
*
* @param string $filepath
* @param string $user1
* @param string $user2
*/
- public function assureFileIsShared($entry, $filepath, $user1, $user2, $withPerms = null, $permissions = null) {
+ public function assureFileIsShared($entry, $filepath, $user1, $user2, $withPerms = null, $permissions = null, $viewOnly = null) {
+ // when view-only is set, permissions is empty string instead of null...
+ if ($permissions === '') {
+ $permissions = null;
+ }
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares" . "?path=$filepath";
$client = new Client();
$options = [];
@@ -424,20 +417,24 @@ trait Sharing {
if ($this->isUserOrGroupInSharedData($user2, $permissions)) {
return;
} else {
- $this->createShare($user1, $filepath, 0, $user2, null, null, $permissions);
+ $this->createShare($user1, $filepath, 0, $user2, null, null, $permissions, $viewOnly !== null);
}
$this->response = $client->get($fullUrl, $options);
Assert::assertEquals(true, $this->isUserOrGroupInSharedData($user2, $permissions));
}
/**
- * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with group "([^"]*)"( with permissions ([\d]*))?$/
+ * @Given /^(file|folder|entry) "([^"]*)" of user "([^"]*)" is shared with group "([^"]*)"( with permissions ([\d]*))?( view-only)?$/
*
* @param string $filepath
* @param string $user
* @param string $group
*/
- public function assureFileIsSharedWithGroup($entry, $filepath, $user, $group, $withPerms = null, $permissions = null) {
+ public function assureFileIsSharedWithGroup($entry, $filepath, $user, $group, $withPerms = null, $permissions = null, $viewOnly = null) {
+ // when view-only is set, permissions is empty string instead of null...
+ if ($permissions === '') {
+ $permissions = null;
+ }
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares" . "?path=$filepath";
$client = new Client();
$options = [];
@@ -453,7 +450,7 @@ trait Sharing {
if ($this->isUserOrGroupInSharedData($group, $permissions)) {
return;
} else {
- $this->createShare($user, $filepath, 1, $group, null, null, $permissions);
+ $this->createShare($user, $filepath, 1, $group, null, null, $permissions, $viewOnly !== null);
}
$this->response = $client->get($fullUrl, $options);
Assert::assertEquals(true, $this->isUserOrGroupInSharedData($group, $permissions));
@@ -465,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);
}
/**
@@ -474,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);
}
/**
@@ -506,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");
@@ -567,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 {
@@ -608,12 +605,12 @@ trait Sharing {
return;
}
- if (!array_key_exists($field, $returnedShare)) {
+ if (!property_exists($returnedShare, $field)) {
Assert::fail("$field was not found in response");
}
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') {
@@ -679,14 +676,14 @@ trait Sharing {
* @When save last share id
*/
public function saveLastShareId() {
- $this->savedShareId = $this->lastShareData['data']['id'];
+ $this->savedShareId = ($this->lastShareData['data']['id'] ?? null);
}
/**
* @Then share ids should match
*/
public function shareIdsShouldMatch() {
- if ($this->savedShareId !== $this->lastShareData['data']['id']) {
+ if ($this->savedShareId !== ($this->lastShareData['data']['id'] ?? null)) {
throw new \Exception('Expected the same link share to be returned');
}
}
diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php
index be719cbf379..a9dd99108a9 100644
--- a/build/integration/features/bootstrap/SharingContext.php
+++ b/build/integration/features/bootstrap/SharingContext.php
@@ -1,26 +1,8 @@
<?php
+
/**
- *
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author John Molakvoæ (skjnldsv) <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,15 +18,21 @@ class SharingContext implements Context, SnippetAcceptingContext {
use Trashbin;
use AppConfiguration;
use CommandLine;
+ use Activity;
protected function resetAppConfigs() {
$this->deleteServerConfig('core', 'shareapi_default_permissions');
$this->deleteServerConfig('core', 'shareapi_default_internal_expire_date');
$this->deleteServerConfig('core', 'shareapi_internal_expire_after_n_days');
$this->deleteServerConfig('core', 'internal_defaultExpDays');
+ $this->deleteServerConfig('core', 'shareapi_enforce_links_password');
$this->deleteServerConfig('core', 'shareapi_default_expire_date');
$this->deleteServerConfig('core', 'shareapi_expire_after_n_days');
$this->deleteServerConfig('core', 'link_defaultExpDays');
- $this->deleteServerConfig('sharebymail', 'enforcePasswordProtection');
+ $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 a490a5e6701..c64626de68d 100644
--- a/build/integration/features/bootstrap/TagsContext.php
+++ b/build/integration/features/bootstrap/TagsContext.php
@@ -1,32 +1,10 @@
<?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';
use Behat\Gherkin\Node\TableNode;
@@ -268,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)
)
@@ -278,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 bc61c87ebab..6f351c30ccf 100644
--- a/build/integration/features/bootstrap/TalkContext.php
+++ b/build/integration/features/bootstrap/TalkContext.php
@@ -1,30 +1,12 @@
<?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;
class TalkContext implements Context {
-
/**
* @BeforeFeature @Talk
* @BeforeScenario @Talk
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 46eeabb4214..dfcc23289a7 100644
--- a/build/integration/features/bootstrap/Trashbin.php
+++ b/build/integration/features/bootstrap/Trashbin.php
@@ -1,28 +1,11 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, ownCloud GmbH.
- *
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author John Molakvoæ (skjnldsv) <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;
require __DIR__ . '/../../vendor/autoload.php';
@@ -31,7 +14,6 @@ require __DIR__ . '/../../vendor/autoload.php';
* Trashbin functions
*/
trait Trashbin {
-
// WebDav trait is expected to be used in the class that uses this trait.
/**
@@ -116,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');
}
}
}
@@ -139,7 +121,7 @@ trait Trashbin {
return $item['{http://nextcloud.org/ns}trashbin-filename'];
}, $elementList));
- Assert::assertArraySubset([$name], array_values($trashContent));
+ AssertArraySubset::assertArraySubset([$name], array_values($trashContent));
}
/**
diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php
index de277b2278d..2cb37002ac0 100644
--- a/build/integration/features/bootstrap/WebDav.php
+++ b/build/integration/features/bootstrap/WebDav.php
@@ -1,40 +1,14 @@
<?php
+
/**
- *
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author David Toledo <dtoledo@solidgear.es>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ (skjnldsv) <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;
@@ -44,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 "([^"]*)"$/
@@ -66,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;
}
@@ -74,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;
}
@@ -86,11 +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";
}
$client = new GClient();
$options = [
@@ -99,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);
@@ -114,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());
}
@@ -128,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();
}
@@ -158,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);
}
/**
@@ -167,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);
}
/**
@@ -185,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 = [
@@ -193,9 +177,8 @@ trait WebDav {
'Range' => $range
]
];
- $options['auth'] = [$token, ""];
- $this->response = $client->request("GET", $fullUrl, $options);
+ $this->response = $client->request('GET', $fullUrl, $options);
}
/**
@@ -207,6 +190,45 @@ trait WebDav {
}
/**
+ * @Then /^File "([^"]*)" should have prop "([^"]*):([^"]*)" equal to "([^"]*)"$/
+ * @param string $file
+ * @param string $prefix
+ * @param string $prop
+ * @param string $value
+ */
+ 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"];
+ Assert::assertEquals($property, $value);
+ }
+
+ /**
+ * @Then /^Image search should work$/
+ */
+ public function search(): void {
+ $this->searchFile($this->currentUser);
+ Assert::assertEquals(207, $this->response->getStatusCode());
+ }
+
+ /**
+ * @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
@@ -218,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
*/
@@ -230,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
@@ -291,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)) {
@@ -322,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 = [];
@@ -379,6 +481,128 @@ trait WebDav {
return $response;
}
+ /**
+ * Returns the elements of a profind command
+ * @param string $properties properties which needs to be included in the report
+ * @param string $filterRules filter-rules to choose what needs to appear in the report
+ */
+ public function propfindFile(string $user, string $path, string $properties = '') {
+ $client = $this->getSabreClient($user);
+
+ $body = '<?xml version="1.0" encoding="utf-8" ?>
+ <d:propfind xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:prop>
+ ' . $properties . '
+ </d:prop>
+ </d:propfind>';
+
+ $response = $client->request('PROPFIND', $this->makeSabrePath($user, $path), $body);
+ $parsedResponse = $client->parseMultistatus($response['body']);
+ return $parsedResponse;
+ }
+
+ /**
+ * Returns the elements of a searc command
+ * @param string $properties properties which needs to be included in the report
+ * @param string $filterRules filter-rules to choose what needs to appear in the report
+ */
+ public function searchFile(string $user, ?string $properties = null, ?string $scope = null, ?string $condition = null) {
+ $client = $this->getSabreClient($user);
+
+ if ($properties === null) {
+ $properties = '<oc:fileid /> <d:getlastmodified /> <d:getetag /> <d:getcontenttype /> <d:getcontentlength /> <nc:has-preview /> <oc:favorite /> <d:resourcetype />';
+ }
+
+ if ($condition === null) {
+ $condition = '<d:and>
+ <d:or>
+ <d:eq>
+ <d:prop>
+ <d:getcontenttype/>
+ </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/>
+ </d:prop>
+ <d:literal>video/quicktime</d:literal>
+ </d:eq>
+ </d:or>
+ <d:eq>
+ <d:prop>
+ <oc:owner-id/>
+ </d:prop>
+ <d:literal>' . $user . '</d:literal>
+ </d:eq>
+</d:and>';
+ }
+
+ if ($scope === null) {
+ $scope = '<d:href>/files/' . $user . '</d:href><d:depth>infinity</d:depth>';
+ }
+
+ $body = '<?xml version="1.0" encoding="UTF-8"?>
+<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns" xmlns:ns="https://github.com/icewind1991/SearchDAV/ns" xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:basicsearch>
+ <d:select>
+ <d:prop>' . $properties . '</d:prop>
+ </d:select>
+ <d:from><d:scope>' . $scope . '</d:scope></d:from>
+ <d:where>' . $condition . '</d:where>
+ <d:orderby>
+ <d:order>
+ <d:prop><d:getlastmodified/></d:prop>
+ <d:descending/>
+ </d:order>
+ </d:orderby>
+ <d:limit>
+ <d:nresults>35</d:nresults>
+ <ns:firstresult>0</ns:firstresult>
+ </d:limit>
+ </d:basicsearch>
+</d:searchrequest>';
+
+ try {
+ $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
+ $this->response = $e->getResponse();
+ } catch (\GuzzleHttp\Exception\ClientException $e) {
+ // 4xx responses cause a client exception
+ $this->response = $e->getResponse();
+ }
+ }
+
/* Returns the elements of a report command
* @param string $user
* @param string $path
@@ -407,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);
}
}
@@ -440,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');
}
}
}
@@ -455,11 +679,14 @@ trait WebDav {
* @param string $destination
*/
public function userUploadsAFileTo($user, $source, $destination) {
- $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r'));
+ $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) {
- // 4xx and 5xx responses cause an exception
+ // 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();
}
}
@@ -471,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);
}
@@ -484,11 +711,14 @@ trait WebDav {
* @When User :user uploads file with content :content to :destination
*/
public function userUploadsAFileWithContentTo($user, $content, $destination) {
- $file = \GuzzleHttp\Psr7\stream_for($content);
+ $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) {
- // 4xx and 5xx responses cause an exception
+ // 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();
}
}
@@ -503,7 +733,10 @@ trait WebDav {
try {
$this->response = $this->makeDavRequest($user, 'DELETE', $file, []);
} catch (\GuzzleHttp\Exception\ServerException $e) {
- // 4xx and 5xx responses cause an exception
+ // 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();
}
}
@@ -516,43 +749,86 @@ 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) {
- // 4xx and 5xx responses cause an exception
+ // 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();
}
}
/**
- * @Given user :user uploads chunk file :num of :total with :data to :destination
+ * @Given user :user uploads bulked files :name1 with :content1 and :name2 with :content2 and :name3 with :content3
* @param string $user
- * @param int $num
- * @param int $total
- * @param string $data
- * @param string $destination
+ * @param string $name1
+ * @param string $content1
+ * @param string $name2
+ * @param string $content2
+ * @param string $name3
+ * @param string $content3
*/
- public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination) {
- $num -= 1;
- $data = \GuzzleHttp\Psr7\stream_for($data);
- $file = $destination . '-chunking-42-' . $total . '-' . $num;
- $this->makeDavRequest($user, 'PUT', $file, ['OC-Chunked' => '1'], $data, "uploads");
+ public function userUploadsBulkedFiles($user, $name1, $content1, $name2, $content2, $name3, $content3) {
+ $boundary = 'boundary_azertyuiop';
+
+ $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 .= "\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 .= "\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 .= "\r\n";
+ $body .= $content3 . "\r\n";
+ $body .= '--' . $boundary . "--\r\n";
+
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $body);
+ rewind($stream);
+
+ $client = new GClient();
+ $options = [
+ 'auth' => [$user, $this->regularUser],
+ 'headers' => [
+ '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);
}
/**
* @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');
}
/**
* @Given user :user uploads new chunk file :num with :data to id :id
*/
public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id) {
- $data = \GuzzleHttp\Psr7\stream_for($data);
+ $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');
}
/**
@@ -563,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');
}
/**
@@ -577,20 +853,78 @@ 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 "([^"]*)"$/
*/
public function downloadingFileAs($fileName, $user) {
try {
$this->response = $this->makeDavRequest($user, 'GET', $fileName, []);
- } catch (\GuzzleHttp\Exception\ServerException $ex) {
- $this->response = $ex->getResponse();
+ } 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();
}
}
@@ -684,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() {
@@ -711,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');
}
}
}
@@ -728,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);
}
}
@@ -744,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'];
}
@@ -767,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;
+ }
}
diff --git a/build/integration/features/comments-search.feature b/build/integration/features/comments-search.feature
deleted file mode 100644
index a1d116ee3f4..00000000000
--- a/build/integration/features/comments-search.feature
+++ /dev/null
@@ -1,271 +0,0 @@
-Feature: comments-search
-
- Scenario: Search my own comment on a file belonging to myself
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "first" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | My first comment |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My first comment |
-
- Scenario: Search my own comment on a file shared by someone with me
- Given user "user0" exists
- And user "user1" exists
- And User "user1" uploads file "data/textfile.txt" to "/sharedFileToComment.txt"
- And as "user1" creating a share with
- | path | sharedFileToComment.txt |
- | shareWith | user0 |
- | shareType | 0 |
- And user "user0" accepts last share
- And "user0" posts a comment with content "My first comment" on the file named "/sharedFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "first" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | My first comment |
- | authorId | user0 |
- | authorName | user0 |
- | path | sharedFileToComment.txt |
- | fileName | sharedFileToComment.txt |
- | name | My first comment |
-
- Scenario: Search other user's comment on a file shared by me
- Given user "user0" exists
- And user "user1" exists
- And User "user0" uploads file "data/textfile.txt" to "/mySharedFileToComment.txt"
- And as "user0" creating a share with
- | path | mySharedFileToComment.txt |
- | shareWith | user1 |
- | shareType | 0 |
- And user "user1" accepts last share
- And "user1" posts a comment with content "Other's first comment" on the file named "/mySharedFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "first" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | Other's first comment |
- | authorId | user1 |
- | authorName | user1 |
- | path | mySharedFileToComment.txt |
- | fileName | mySharedFileToComment.txt |
- | name | Other's first comment |
-
- Scenario: Search other user's comment on a file shared by someone with me
- Given user "user0" exists
- And user "user1" exists
- And User "user1" uploads file "data/textfile.txt" to "/sharedFileToComment.txt"
- And as "user1" creating a share with
- | path | sharedFileToComment.txt |
- | shareWith | user0 |
- | shareType | 0 |
- And user "user0" accepts last share
- And "user1" posts a comment with content "Other's first comment" on the file named "/sharedFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "first" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | Other's first comment |
- | authorId | user1 |
- | authorName | user1 |
- | path | sharedFileToComment.txt |
- | fileName | sharedFileToComment.txt |
- | name | Other's first comment |
-
- Scenario: Search several comments on a file belonging to myself
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "My first comment to be found" on the file named "/myFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "The second comment should not be found" on the file named "/myFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "My third comment to be found" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "comment to be found" in app "files"
- Then the list of search results has "2" results
- And search result "0" contains
- | type | comment |
- | comment | My third comment to be found |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My third comment to be found |
- And search result "1" contains
- | type | comment |
- | comment | My first comment to be found |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My first comment to be found |
-
- Scenario: Search comment with a large message ellipsized on the right
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "verbose" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | A very verbose message that is meant to… |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments |
-
- Scenario: Search comment with a large message ellipsized on the left
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "searching" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | …ed message returned when searching for long comments |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments |
-
- Scenario: Search comment with a large message ellipsized on both ends
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "ellipsized" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | …t to be used to test the ellipsized message returned when se… |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | A very verbose message that is meant to be used to test the ellipsized message returned when searching for long comments |
-
- Scenario: Search comment on a file in a subfolder
- Given user "user0" exists
- And user "user0" created a folder "/subfolder"
- And User "user0" uploads file "data/textfile.txt" to "/subfolder/myFileToComment.txt"
- And "user0" posts a comment with content "My first comment" on the file named "/subfolder/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "first" in app "files"
- Then the list of search results has "1" results
- And search result "0" contains
- | type | comment |
- | comment | My first comment |
- | authorId | user0 |
- | authorName | user0 |
- | path | subfolder/myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My first comment |
-
- Scenario: Search several comments
- Given user "user0" exists
- And user "user1" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And User "user0" uploads file "data/textfile.txt" to "/mySharedFileToComment.txt"
- And as "user0" creating a share with
- | path | mySharedFileToComment.txt |
- | shareWith | user1 |
- | shareType | 0 |
- And user "user1" accepts last share
- And User "user1" uploads file "data/textfile.txt" to "/sharedFileToComment.txt"
- And as "user1" creating a share with
- | path | sharedFileToComment.txt |
- | shareWith | user0 |
- | shareType | 0 |
- And user "user0" accepts last share
- And "user0" posts a comment with content "My first comment to be found" on the file named "/myFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "The second comment should not be found" on the file named "/myFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "My first comment to be found" on the file named "/mySharedFileToComment.txt" it should return "201"
- And "user1" posts a comment with content "Other's first comment that should not be found" on the file named "/mySharedFileToComment.txt" it should return "201"
- And "user1" posts a comment with content "Other's second comment to be found" on the file named "/mySharedFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "My first comment that should not be found" on the file named "/sharedFileToComment.txt" it should return "201"
- And "user1" posts a comment with content "Other's first comment to be found" on the file named "/sharedFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "My second comment to be found that happens to be more verbose than the others and thus should be ellipsized" on the file named "/sharedFileToComment.txt" it should return "201"
- And "user0" posts a comment with content "My third comment to be found" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "comment to be found" in app "files"
- Then the list of search results has "6" results
- And search result "0" contains
- | type | comment |
- | comment | My third comment to be found |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My third comment to be found |
- And search result "1" contains
- | type | comment |
- | comment | My second comment to be found that happens to be more … |
- | authorId | user0 |
- | authorName | user0 |
- | path | sharedFileToComment.txt |
- | fileName | sharedFileToComment.txt |
- | name | My second comment to be found that happens to be more verbose than the others and thus should be ellipsized |
- And search result "2" contains
- | type | comment |
- | comment | Other's first comment to be found |
- | authorId | user1 |
- | authorName | user1 |
- | path | sharedFileToComment.txt |
- | fileName | sharedFileToComment.txt |
- | name | Other's first comment to be found |
- And search result "3" contains
- | type | comment |
- | comment | Other's second comment to be found |
- | authorId | user1 |
- | authorName | user1 |
- | path | mySharedFileToComment.txt |
- | fileName | mySharedFileToComment.txt |
- | name | Other's second comment to be found |
- And search result "4" contains
- | type | comment |
- | comment | My first comment to be found |
- | authorId | user0 |
- | authorName | user0 |
- | path | mySharedFileToComment.txt |
- | fileName | mySharedFileToComment.txt |
- | name | My first comment to be found |
- And search result "5" contains
- | type | comment |
- | comment | My first comment to be found |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | My first comment to be found |
-
- Scenario: Search comment with a query that also matches a file name
- Given user "user0" exists
- And User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt"
- And "user0" posts a comment with content "A comment in myFileToComment.txt" on the file named "/myFileToComment.txt" it should return "201"
- When Logging in using web as "user0"
- And searching for "myFileToComment" in app "files"
- Then the list of search results has "2" results
- And search result "0" contains
- | type | file |
- | path | /myFileToComment.txt |
- | name | myFileToComment.txt |
- And search result "1" contains
- | type | comment |
- | comment | A comment in myFileToComment.txt |
- | authorId | user0 |
- | authorName | user0 |
- | path | myFileToComment.txt |
- | fileName | myFileToComment.txt |
- | name | A comment in myFileToComment.txt |
diff --git a/build/integration/features/contacts-menu.feature b/build/integration/features/contacts-menu.feature
new file mode 100644
index 00000000000..772c0e5405c
--- /dev/null
+++ b/build/integration/features/contacts-menu.feature
@@ -0,0 +1,194 @@
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: contacts-menu
+
+ Scenario: users can be searched by display name
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | displayname |
+ | value | Test name |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ Then the list of searched contacts has "1" contacts
+ And searched contact "0" is named "Test name"
+
+ Scenario: users can be searched by email
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | email |
+ | value | test@example.com |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ Then the list of searched contacts has "1" contacts
+ And searched contact "0" is named "user1"
+
+ Scenario: users can not be searched by id
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | displayname |
+ | value | Test name |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "user"
+ Then the list of searched contacts has "0" contacts
+
+ Scenario: search several users
+ Given user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And user "user3" exists
+ And user "user4" exists
+ And user "user5" exists
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | displayname |
+ | value | Test name |
+ And sending "PUT" to "/cloud/users/user2" with
+ | key | email |
+ | value | test@example.com |
+ And sending "PUT" to "/cloud/users/user3" with
+ | key | displayname |
+ | value | Unmatched name |
+ And sending "PUT" to "/cloud/users/user4" with
+ | key | email |
+ | value | unmatched@example.com |
+ And sending "PUT" to "/cloud/users/user5" with
+ | key | displayname |
+ | value | Another test name |
+ And sending "PUT" to "/cloud/users/user5" with
+ | key | email |
+ | value | another_test@example.com |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ Then the list of searched contacts has "3" contacts
+ # Results are sorted alphabetically
+ And searched contact "0" is named "Another test name"
+ And searched contact "1" is named "Test name"
+ And searched contact "2" is named "user2"
+
+ Scenario: users can not be found by display name if visibility is private
+ Given user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | displayname | Test name |
+ | displaynameScope | v2-private |
+ And Logging in using web as "user2"
+ And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken
+ | displayname | Another test name |
+ | displaynameScope | v2-federated |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ # Disabled because it regularly fails on drone:
+ # Then the list of searched contacts has "1" contacts
+ # And searched contact "0" is named "Another test name"
+
+ Scenario: users can not be found by email if visibility is private
+ Given user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | email | test@example.com |
+ | emailScope | v2-private |
+ And Logging in using web as "user2"
+ And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken
+ | email | another_test@example.com |
+ | emailScope | v2-federated |
+ # Disabled because it regularly fails on drone:
+ # When Logging in using web as "user0"
+ # And searching for contacts matching with "test"
+ # Then the list of searched contacts has "1" contacts
+ # And searched contact "0" is named "user2"
+
+ Scenario: users can be found by other properties if the visibility of one is private
+ Given user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | displayname | Test name |
+ | displaynameScope | v2-federated |
+ | email | test@example.com |
+ | emailScope | v2-private |
+ And Logging in using web as "user2"
+ And Sending a "PUT" to "/settings/users/user2/settings" with requesttoken
+ | displayname | Another test name |
+ | displaynameScope | v2-private |
+ | email | another_test@example.com |
+ | emailScope | v2-federated |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ Then the list of searched contacts has "2" contacts
+ # Disabled because it regularly fails on drone:
+ # And searched contact "0" is named ""
+ And searched contact "1" is named "Test name"
+
+
+
+ Scenario: users can be searched by display name if visibility is increased again
+ Given user "user0" exists
+ And user "user1" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | displayname | Test name |
+ | displaynameScope | v2-private |
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | displaynameScope | v2-federated |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ Then the list of searched contacts has "1" contacts
+ And searched contact "0" is named "Test name"
+
+ Scenario: users can be searched by email if visibility is increased again
+ Given user "user0" exists
+ And user "user1" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | email | test@example.com |
+ | emailScope | v2-private |
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | emailScope | v2-federated |
+ # Disabled because it regularly fails on drone:
+ # When Logging in using web as "user0"
+ # And searching for contacts matching with "test"
+ # Then the list of searched contacts has "1" contacts
+ # And searched contact "0" is named "user1"
+
+
+
+ Scenario: users can not be searched by display name if visibility is private even if updated with provisioning
+ Given user "user0" exists
+ And user "user1" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | displaynameScope | v2-private |
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | displayname |
+ | value | Test name |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ # Disabled because it regularly fails on drone:
+ # Then the list of searched contacts has "0" contacts
+
+ Scenario: users can not be searched by email if visibility is private even if updated with provisioning
+ Given user "user0" exists
+ And user "user1" exists
+ And Logging in using web as "user1"
+ And Sending a "PUT" to "/settings/users/user1/settings" with requesttoken
+ | emailScope | v2-private |
+ And As an "admin"
+ And sending "PUT" to "/cloud/users/user1" with
+ | key | email |
+ | value | test@example.com |
+ When Logging in using web as "user0"
+ And searching for contacts matching with "test"
+ # Disabled because it regularly fails on drone:
+ # Then the list of searched contacts has "0" contacts
diff --git a/build/integration/features/dav-v2.feature b/build/integration/features/dav-v2.feature
deleted file mode 100644
index 5405510283f..00000000000
--- a/build/integration/features/dav-v2.feature
+++ /dev/null
@@ -1,83 +0,0 @@
-Feature: dav-v2
- Background:
- Given using api version "1"
-
- Scenario: moving a file new endpoint way
- Given using new dav path
- And As an "admin"
- And user "user0" exists
- When User "user0" moves file "/textfile0.txt" to "/FOLDER/textfile0.txt"
- Then the HTTP status code should be "201"
-
- Scenario: download a file with range using new endpoint
- Given using new dav path
- And As an "admin"
- And user "user0" exists
- And As an "user0"
- When Downloading file "/welcome.txt" with range "bytes=52-78"
- Then Downloaded content should be "example file for developers"
-
- Scenario: Downloading a file on the new endpoint should serve security headers
- Given using new dav path
- And As an "admin"
- When Downloading file "/welcome.txt"
- Then The following headers should be set
- |Content-Disposition|attachment; filename*=UTF-8''welcome.txt; filename="welcome.txt"|
- |Content-Security-Policy|default-src 'none';|
- |X-Content-Type-Options |nosniff|
- |X-Download-Options|noopen|
- |X-Frame-Options|SAMEORIGIN|
- |X-Permitted-Cross-Domain-Policies|none|
- |X-Robots-Tag|none|
- |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
- Given Logging in using web as "admin"
- When Sending a "GET" to "/remote.php/dav/files/admin/welcome.txt" without requesttoken
- Then Downloaded content should start with "Welcome to your Nextcloud account!"
- Then the HTTP status code should be "200"
-
- Scenario: Doing a GET with a web login should work with CSRF token on the new backend
- Given Logging in using web as "admin"
- When Sending a "GET" to "/remote.php/dav/files/admin/welcome.txt" with requesttoken
- Then Downloaded content should start with "Welcome to your Nextcloud account!"
- Then the HTTP status code should be "200"
-
- Scenario: Doing a PROPFIND with a web login should not work without CSRF token on the new backend
- Given Logging in using web as "admin"
- When Sending a "PROPFIND" to "/remote.php/dav/files/admin/welcome.txt" without requesttoken
- Then the HTTP status code should be "401"
-
- Scenario: Doing a PROPFIND with a web login should work with CSRF token on the new backend
- Given Logging in using web as "admin"
- When Sending a "PROPFIND" to "/remote.php/dav/files/admin/welcome.txt" with requesttoken
- Then the HTTP status code should be "207"
-
- Scenario: Uploading a file having 0B as quota
- Given using new dav path
- And As an "admin"
- And user "user0" exists
- And user "user0" has a quota of "0 B"
- And As an "user0"
- When User "user0" uploads file "data/textfile.txt" to "/asdf.txt"
- Then the HTTP status code should be "507"
-
- Scenario: Uploading a file as recipient using webdav new endpoint having quota
- Given using new dav path
- And As an "admin"
- And user "user0" exists
- And user "user1" exists
- And user "user0" has a quota of "10 MB"
- And user "user1" has a quota of "10 MB"
- And As an "user1"
- And user "user1" created a folder "/testquota"
- And as "user1" creating a share with
- | path | testquota |
- | shareType | 0 |
- | permissions | 31 |
- | shareWith | user0 |
- And user "user0" accepts last share
- And As an "user0"
- When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt"
- Then the HTTP status code should be "201"
diff --git a/build/integration/features/external-storage.feature b/build/integration/features/external-storage.feature
deleted file mode 100644
index 59a873dd816..00000000000
--- a/build/integration/features/external-storage.feature
+++ /dev/null
@@ -1,62 +0,0 @@
-Feature: external-storage
- Background:
- Given using api version "1"
- Given using old dav path
-
- @local_storage
- Scenario: Share by link a file inside a local external storage
- Given user "user0" exists
- And user "user1" exists
- And As an "user0"
- And user "user0" created a folder "/local_storage/foo"
- And User "user0" moved file "/textfile0.txt" to "/local_storage/foo/textfile0.txt"
- And folder "/local_storage/foo" of user "user0" is shared with user "user1"
- And As an "user1"
- And accepting last share
- When creating a share with
- | path | foo |
- | shareType | 3 |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And Share fields of last share match with
- | id | A_NUMBER |
- | url | AN_URL |
- | token | A_TOKEN |
- | mimetype | httpd/unix-directory |
-
- Scenario: Shares don't overwrite external storages
- Given user "user0" exists
- And user "user1" exists
- And As an "user0"
- And User "user0" moved file "/textfile0.txt" to "/local_storage/textfile0.txt"
- And invoking occ with "files_external:create --user user0 test local null::null -c datadir=./build/integration/work/local_storage"
- And invoking occ with "files:scan --path /user0/files/test"
- And as "user0" the file "/local_storage/textfile0.txt" exists
- And as "user0" the folder "/test" exists
- And as "user0" the file "/test/textfile0.txt" exists
- And As an "user1"
- And user "user1" created a folder "/test"
- And User "user1" moved file "/textfile0.txt" to "/test/textfile1.txt"
- And folder "/test" of user "user1" is shared with user "user0"
- And As an "user0"
- Then as "user0" the file "/test/textfile1.txt" does not exist
-
- Scenario: Move a file into storage works
- Given user "user0" exists
- And user "user1" exists
- And As an "user0"
- And user "user0" created a folder "/local_storage/foo1"
- When User "user0" moved file "/textfile0.txt" to "/local_storage/foo1/textfile0.txt"
- Then as "user1" the file "/local_storage/foo1/textfile0.txt" exists
- And as "user0" the file "/local_storage/foo1/textfile0.txt" exists
-
- Scenario: Move a file out of the storage works
- Given user "user0" exists
- And user "user1" exists
- And As an "user0"
- And user "user0" created a folder "/local_storage/foo2"
- And User "user0" moved file "/textfile0.txt" to "/local_storage/foo2/textfile0.txt"
- When User "user1" moved file "/local_storage/foo2/textfile0.txt" to "/local.txt"
- Then as "user1" the file "/local_storage/foo2/textfile0.txt" does not exist
- And as "user0" the file "/local_storage/foo2/textfile0.txt" does not exist
- And as "user1" the file "/local.txt" exists
diff --git a/build/integration/features/log-condition.feature b/build/integration/features/log-condition.feature
new file mode 100644
index 00000000000..4059db1ebf3
--- /dev/null
+++ b/build/integration/features/log-condition.feature
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: log-condition
+
+ Background:
+ Given invoking occ with "config:system:set log.condition matches 0 users 0 --value admin"
+ Then the command was successful
+
+ Scenario: Accessing /status.php with log.condition
+ When requesting "/status.php" with "GET"
+ Then the HTTP status code should be "200"
+
+ Scenario: Accessing /index.php with log.condition
+ When requesting "/index.php" with "GET"
+ Then the HTTP status code should be "200"
+
+ Scenario: Accessing /remote.php/webdav with log.condition
+ When requesting "/remote.php/webdav" with "GET"
+ Then the HTTP status code should be "401"
+
+ Scenario: Accessing /remote.php/dav with log.condition
+ When requesting "/remote.php/dav" with "GET"
+ Then the HTTP status code should be "401"
+
+ Scenario: Accessing /ocs/v1.php with log.condition
+ When requesting "/ocs/v1.php" with "GET"
+ Then the HTTP status code should be "200"
+
+ Scenario: Accessing /ocs/v2.php with log.condition
+ When requesting "/ocs/v2.php" with "GET"
+ Then the HTTP status code should be "404"
+
+ Scenario: Accessing /public.php/webdav with log.condition
+ When requesting "/public.php/webdav" with "GET"
+ Then the HTTP status code should be "401"
+
+ Scenario: Accessing /public.php/dav with log.condition
+ When requesting "/public.php/dav" with "GET"
+ Then the HTTP status code should be "503"
diff --git a/build/integration/features/maintenance-mode.feature b/build/integration/features/maintenance-mode.feature
index 56d3b9c0fb6..72af31f193f 100644
--- a/build/integration/features/maintenance-mode.feature
+++ b/build/integration/features/maintenance-mode.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: maintenance-mode
Background:
@@ -39,3 +41,9 @@ Feature: maintenance-mode
Then the HTTP status code should be "503"
Then Maintenance mode is disabled
And the command was successful
+
+ Scenario: Accessing /public.php/dav with maintenance mode enabled
+ When requesting "/public.php/dav" with "GET"
+ Then the HTTP status code should be "503"
+ Then Maintenance mode is disabled
+ And the command was successful
diff --git a/build/integration/features/ocs-v1.feature b/build/integration/features/ocs-v1.feature
index 6075189ddb4..26907580aee 100644
--- a/build/integration/features/ocs-v1.feature
+++ b/build/integration/features/ocs-v1.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: ocs
Background:
Given using api version "1"
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index 717aa04e4bd..8fcfb076497 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -1,615 +1,892 @@
+# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: provisioning
- Background:
- Given using api version "1"
-
- Scenario: Getting an not existing user
- Given As an "admin"
- When sending "GET" to "/cloud/users/test"
- Then the OCS status code should be "404"
- And the HTTP status code should be "200"
-
- Scenario: Listing all users
- Given As an "admin"
- When sending "GET" to "/cloud/users"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Create a user
- Given As an "admin"
- And user "brand-new-user" does not exist
- When sending "POST" to "/cloud/users" with
- | userid | brand-new-user |
- | password | 123456 |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And user "brand-new-user" exists
-
- Scenario: Create an existing user
- Given As an "admin"
- And user "brand-new-user" exists
- When sending "POST" to "/cloud/users" with
- | userid | brand-new-user |
- | password | 123456 |
- Then the OCS status code should be "102"
- And the HTTP status code should be "200"
- And user "brand-new-user" has
- | id | brand-new-user |
- | displayname | brand-new-user |
- | email | |
- | phone | |
- | address | |
- | website | |
- | twitter | |
-
- Scenario: Get an existing user
- Given As an "admin"
- When sending "GET" to "/cloud/users/brand-new-user"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Getting all users
- Given As an "admin"
- And user "brand-new-user" exists
- And user "admin" exists
- When sending "GET" to "/cloud/users"
- Then users returned are
- | brand-new-user |
- | admin |
-
- Scenario: Edit a user
- Given As an "admin"
- And user "brand-new-user" exists
- When sending "PUT" to "/cloud/users/brand-new-user" with
- | key | displayname |
- | value | Brand New User |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | quota |
- | value | 12MB |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | email |
- | value | no-reply@nextcloud.com |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | phone |
- | value | +49 711 / 25 24 28-90 |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | address |
- | value | Foo Bar Town |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | website |
- | value | https://nextcloud.com |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- And sending "PUT" to "/cloud/users/brand-new-user" with
- | key | twitter |
- | value | Nextcloud |
- 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 |
- | displayname | Brand New User |
- | email | no-reply@nextcloud.com |
- | phone | +4971125242890 |
- | address | Foo Bar Town |
- | website | https://nextcloud.com |
- | twitter | Nextcloud |
-
- Scenario: Search by phone number
- Given As an "admin"
- And user "phone-user" exists
- And sending "PUT" to "/cloud/users/phone-user" with
- | key | phone |
- | value | +49 711 / 25 24 28-90 |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- Then search users by phone for region "DE" with
- | random-string1 | 0711 / 123 456 78 |
- | random-string1 | 0711 / 252 428-90 |
- | random-string2 | 0711 / 90-824 252 |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
- Then phone matches returned are
- | random-string1 | phone-user@localhost:8080 |
-
- Scenario: Create a group
- Given As an "admin"
- And group "new-group" does not exist
- When sending "POST" to "/cloud/groups" with
- | groupid | new-group |
- | password | 123456 |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And group "new-group" exists
-
- Scenario: Create a group with special characters
- Given As an "admin"
- And group "España" does not exist
- When sending "POST" to "/cloud/groups" with
- | groupid | España |
- | password | 123456 |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And group "España" exists
-
- Scenario: adding user to a group without sending the group
- Given As an "admin"
- And user "brand-new-user" exists
- When sending "POST" to "/cloud/users/brand-new-user/groups" with
- | groupid | |
- Then the OCS status code should be "101"
- And the HTTP status code should be "200"
-
- Scenario: adding user to a group which doesn't exist
- Given As an "admin"
- And user "brand-new-user" exists
- And group "not-group" does not exist
- When sending "POST" to "/cloud/users/brand-new-user/groups" with
- | groupid | not-group |
- Then the OCS status code should be "102"
- And the HTTP status code should be "200"
-
- Scenario: adding user to a group without privileges
- Given As an "brand-new-user"
- When sending "POST" to "/cloud/users/brand-new-user/groups" with
- | groupid | new-group |
- Then the OCS status code should be "997"
- And the HTTP status code should be "401"
-
- Scenario: adding user to a group
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- When sending "POST" to "/cloud/users/brand-new-user/groups" with
- | groupid | new-group |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: getting groups of an user
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- When sending "GET" to "/cloud/users/brand-new-user/groups"
- Then groups returned are
- | new-group |
- And the OCS status code should be "100"
-
- Scenario: adding a user which doesn't exist to a group
- Given As an "admin"
- And user "not-user" does not exist
- And group "new-group" exists
- When sending "POST" to "/cloud/users/not-user/groups" with
- | groupid | new-group |
- Then the OCS status code should be "103"
- And the HTTP status code should be "200"
-
- Scenario: getting a group
- Given As an "admin"
- And group "new-group" exists
- When sending "GET" to "/cloud/groups/new-group"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Getting all groups
- Given As an "admin"
- And group "new-group" exists
- And group "admin" exists
- When sending "GET" to "/cloud/groups"
- Then groups returned are
- | España |
- | admin |
- | new-group |
-
- Scenario: create a subadmin
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- When sending "POST" to "/cloud/users/brand-new-user/subadmins" with
- | groupid | new-group |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: get users using a subadmin
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- And user "brand-new-user" belongs to group "new-group"
- And user "brand-new-user" is subadmin of group "new-group"
- And As an "brand-new-user"
- When sending "GET" to "/cloud/users"
- Then users returned are
- | brand-new-user |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: removing a user from a group which doesn't exists
- Given As an "admin"
- And user "brand-new-user" exists
- And group "not-group" does not exist
- When sending "DELETE" to "/cloud/users/brand-new-user/groups" with
- | groupid | not-group |
- Then the OCS status code should be "102"
-
- Scenario: removing a user from a group
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- And user "brand-new-user" belongs to group "new-group"
- When sending "DELETE" to "/cloud/users/brand-new-user/groups" with
- | groupid | new-group |
- Then the OCS status code should be "100"
- And user "brand-new-user" does not belong to group "new-group"
-
- Scenario: create a subadmin using a user which not exist
- Given As an "admin"
- And user "not-user" does not exist
- And group "new-group" exists
- When sending "POST" to "/cloud/users/not-user/subadmins" with
- | groupid | new-group |
- Then the OCS status code should be "101"
- And the HTTP status code should be "200"
-
- Scenario: create a subadmin using a group which not exist
- Given As an "admin"
- And user "brand-new-user" exists
- And group "not-group" does not exist
- When sending "POST" to "/cloud/users/brand-new-user/subadmins" with
- | groupid | not-group |
- Then the OCS status code should be "102"
- And the HTTP status code should be "200"
-
- Scenario: Getting subadmin groups
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- When sending "GET" to "/cloud/users/brand-new-user/subadmins"
- Then subadmin groups returned are
- | new-group |
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Getting subadmin groups of a user which not exist
- Given As an "admin"
- And user "not-user" does not exist
- And group "new-group" exists
- When sending "GET" to "/cloud/users/not-user/subadmins"
- Then the OCS status code should be "404"
- And the HTTP status code should be "200"
-
- Scenario: Getting subadmin users of a group
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- When sending "GET" to "/cloud/groups/new-group/subadmins"
- Then subadmin users returned are
- | brand-new-user |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Getting subadmin users of a group which doesn't exist
- Given As an "admin"
- And user "brand-new-user" exists
- And group "not-group" does not exist
- When sending "GET" to "/cloud/groups/not-group/subadmins"
- Then the OCS status code should be "101"
- And the HTTP status code should be "200"
-
- Scenario: Removing subadmin from a group
- Given As an "admin"
- And user "brand-new-user" exists
- And group "new-group" exists
- And user "brand-new-user" is subadmin of group "new-group"
- When sending "DELETE" to "/cloud/users/brand-new-user/subadmins" with
- | groupid | new-group |
- And the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: Delete a user
- Given As an "admin"
- And user "brand-new-user" exists
- When sending "DELETE" to "/cloud/users/brand-new-user"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And user "brand-new-user" does not exist
-
- Scenario: Delete a group
- Given As an "admin"
- And group "new-group" exists
- When sending "DELETE" to "/cloud/groups/new-group"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And group "new-group" does not exist
-
- Scenario: Delete a group with special characters
- Given As an "admin"
- And group "España" exists
- When sending "DELETE" to "/cloud/groups/España"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And group "España" does not exist
-
- Scenario: get enabled apps
- Given As an "admin"
- When sending "GET" to "/cloud/apps?filter=enabled"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And apps returned are
- | accessibility |
- | cloud_federation_api |
- | comments |
- | contactsinteraction |
- | dashboard |
- | dav |
- | federatedfilesharing |
- | federation |
- | files |
- | files_sharing |
- | files_trashbin |
- | files_versions |
- | lookup_server_connector |
- | provisioning_api |
- | settings |
- | sharebymail |
- | systemtags |
- | theming |
- | twofactor_backupcodes |
- | updatenotification |
- | user_ldap |
- | user_status |
- | viewer |
- | workflowengine |
- | weather_status |
- | files_external |
- | oauth2 |
-
- Scenario: get app info
- Given As an "admin"
- When sending "GET" to "/cloud/apps/files"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
-
- Scenario: get app info from app that does not exist
- Given As an "admin"
- When sending "GET" to "/cloud/apps/this_app_should_never_exist"
- Then the OCS status code should be "998"
- And the HTTP status code should be "200"
-
- Scenario: enable an app
- Given As an "admin"
- And app "testing" is disabled
- When sending "POST" to "/cloud/apps/testing"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And app "testing" is enabled
-
- Scenario: enable an app that does not exist
- Given As an "admin"
- When sending "POST" to "/cloud/apps/this_app_should_never_exist"
- Then the OCS status code should be "998"
- And the HTTP status code should be "200"
-
- Scenario: disable an app
- Given As an "admin"
- And app "testing" is enabled
- When sending "DELETE" to "/cloud/apps/testing"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And app "testing" is disabled
-
- Scenario: disable an user
- Given As an "admin"
- And user "user1" exists
- When sending "PUT" to "/cloud/users/user1/disable"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And user "user1" is disabled
-
- Scenario: enable an user
- Given As an "admin"
- And user "user1" exists
- And assure user "user1" is disabled
- When sending "PUT" to "/cloud/users/user1/enable"
- Then the OCS status code should be "100"
- And the HTTP status code should be "200"
- And user "user1" is enabled
-
- Scenario: Subadmin should be able to enable or disable an user in their group
- Given As an "admin"
- And user "subadmin" exists
- And user "user1" exists
- And group "new-group" exists
- And user "subadmin" belongs to group "new-group"
- And user "user1" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And As an "subadmin"
- When sending "PUT" to "/cloud/users/user1/disable"
- Then the OCS status code should be "100"
- Then the HTTP status code should be "200"
- And As an "admin"
- And user "user1" is disabled
-
- Scenario: Subadmin should not be able to enable or disable an user not in their group
- Given As an "admin"
- And user "subadmin" exists
- And user "user1" exists
- And group "new-group" exists
- And group "another-group" exists
- And user "subadmin" belongs to group "new-group"
- And user "user1" belongs to group "another-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And As an "subadmin"
- When sending "PUT" to "/cloud/users/user1/disable"
- Then the OCS status code should be "997"
- Then the HTTP status code should be "401"
- And As an "admin"
- And user "user1" is enabled
-
- Scenario: Subadmins should not be able to disable users that have admin permissions in their group
- Given As an "admin"
- And user "another-admin" exists
- And user "subadmin" exists
- And group "new-group" exists
- And user "another-admin" belongs to group "admin"
- And user "subadmin" belongs to group "new-group"
- And user "another-admin" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And As an "subadmin"
- When sending "PUT" to "/cloud/users/another-admin/disable"
- Then the OCS status code should be "997"
- Then the HTTP status code should be "401"
- And As an "admin"
- And user "another-admin" is enabled
-
- Scenario: Admin can disable another admin user
- Given As an "admin"
- And user "another-admin" exists
- And user "another-admin" belongs to group "admin"
- When sending "PUT" to "/cloud/users/another-admin/disable"
- Then the OCS status code should be "100"
- Then the HTTP status code should be "200"
- And user "another-admin" is disabled
-
- Scenario: Admin can enable another admin user
- Given As an "admin"
- And user "another-admin" exists
- And user "another-admin" belongs to group "admin"
- And assure user "another-admin" is disabled
- When sending "PUT" to "/cloud/users/another-admin/enable"
- Then the OCS status code should be "100"
- Then the HTTP status code should be "200"
- And user "another-admin" is enabled
-
- Scenario: Admin can disable subadmins in the same group
- Given As an "admin"
- And user "subadmin" exists
- And group "new-group" exists
- And user "subadmin" belongs to group "new-group"
- And user "admin" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- When sending "PUT" to "/cloud/users/subadmin/disable"
- Then the OCS status code should be "100"
- Then the HTTP status code should be "200"
- And user "subadmin" is disabled
-
- Scenario: Admin can enable subadmins in the same group
- Given As an "admin"
- And user "subadmin" exists
- And group "new-group" exists
- And user "subadmin" belongs to group "new-group"
- And user "admin" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And assure user "another-admin" is disabled
- When sending "PUT" to "/cloud/users/subadmin/disable"
- Then the OCS status code should be "100"
- Then the HTTP status code should be "200"
- And user "subadmin" is disabled
-
- Scenario: Admin user cannot disable himself
- Given As an "admin"
- And user "another-admin" exists
- And user "another-admin" belongs to group "admin"
- And As an "another-admin"
- When sending "PUT" to "/cloud/users/another-admin/disable"
- Then the OCS status code should be "101"
- And the HTTP status code should be "200"
- And As an "admin"
- And user "another-admin" is enabled
-
- Scenario:Admin user cannot enable himself
- Given As an "admin"
- And user "another-admin" exists
- And user "another-admin" belongs to group "admin"
- And assure user "another-admin" is disabled
- And As an "another-admin"
- When sending "PUT" to "/cloud/users/another-admin/enable"
- And As an "admin"
- Then user "another-admin" is disabled
-
- Scenario: disable an user with a regular user
- Given As an "admin"
- And user "user1" exists
- And user "user2" exists
- And As an "user1"
- When sending "PUT" to "/cloud/users/user2/disable"
- Then the OCS status code should be "997"
- And the HTTP status code should be "401"
- And As an "admin"
- And user "user2" is enabled
-
- Scenario: enable an user with a regular user
- Given As an "admin"
- And user "user1" exists
- And user "user2" exists
- And assure user "user2" is disabled
- And As an "user1"
- When sending "PUT" to "/cloud/users/user2/enable"
- Then the OCS status code should be "997"
- And the HTTP status code should be "401"
- And As an "admin"
- And user "user2" is disabled
-
- Scenario: Subadmin should not be able to disable himself
- Given As an "admin"
- And user "subadmin" exists
- And group "new-group" exists
- And user "subadmin" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And As an "subadmin"
- When sending "PUT" to "/cloud/users/subadmin/disable"
- Then the OCS status code should be "101"
- Then the HTTP status code should be "200"
- And As an "admin"
- And user "subadmin" is enabled
-
- Scenario: Subadmin should not be able to enable himself
- Given As an "admin"
- And user "subadmin" exists
- And group "new-group" exists
- And user "subadmin" belongs to group "new-group"
- And Assure user "subadmin" is subadmin of group "new-group"
- And assure user "subadmin" is disabled
- And As an "subadmin"
- When sending "PUT" to "/cloud/users/subadmin/enabled"
- And As an "admin"
- And user "subadmin" is disabled
-
- Scenario: Making a ocs request with an enabled user
- Given As an "admin"
- And user "user0" exists
- And As an "user0"
- When sending "GET" to "/cloud/capabilities"
- Then the HTTP status code should be "200"
- And the OCS status code should be "100"
-
- Scenario: Making a web request with an enabled user
- Given As an "admin"
- And user "user0" exists
- And As an "user0"
- When sending "GET" with exact url to "/index.php/apps/files"
- Then the HTTP status code should be "200"
-
- Scenario: Making a ocs request with a disabled user
- Given As an "admin"
- And user "user0" exists
- And assure user "user0" is disabled
- And As an "user0"
- When sending "GET" to "/cloud/capabilities"
- Then the OCS status code should be "997"
- And the HTTP status code should be "401"
-
- Scenario: Making a web request with a disabled user
- Given As an "admin"
- And user "user0" exists
- And assure user "user0" is disabled
- And As an "user0"
- When sending "GET" with exact url to "/index.php/apps/files"
- And the HTTP status code should be "403"
-
+ Background:
+ Given using api version "1"
+ Given parameter "whitelist_0" of app "bruteForce" is set to "127.0.0.1"
+ Given parameter "whitelist_1" of app "bruteForce" is set to "::1"
+ Given parameter "apply_allowlist_to_ratelimit" of app "bruteforcesettings" is set to "true"
+
+ Scenario: Getting an not existing user
+ Given As an "admin"
+ When sending "GET" to "/cloud/users/test"
+ Then the OCS status code should be "404"
+ And the HTTP status code should be "200"
+
+ Scenario: Listing all users
+ Given As an "admin"
+ When sending "GET" to "/cloud/users"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Create a user
+ Given As an "admin"
+ And user "brand-new-user" does not exist
+ When sending "POST" to "/cloud/users" with
+ | userid | brand-new-user |
+ | password | 123456 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And user "brand-new-user" exists
+
+ Scenario: Create an existing user
+ Given As an "admin"
+ And user "brand-new-user" exists
+ When sending "POST" to "/cloud/users" with
+ | userid | brand-new-user |
+ | password | 123456 |
+ Then the OCS status code should be "102"
+ And the HTTP status code should be "200"
+ And user "brand-new-user" has
+ | id | brand-new-user |
+ | displayname | brand-new-user |
+ | email | |
+ | phone | |
+ | address | |
+ | website | |
+ | twitter | |
+
+ Scenario: Get an existing user
+ Given As an "admin"
+ When sending "GET" to "/cloud/users/brand-new-user"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting all users
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And user "admin" exists
+ When sending "GET" to "/cloud/users"
+ Then users returned are
+ | brand-new-user |
+ | admin |
+
+ Scenario: Get editable fields
+ Given As an "admin"
+ And user "brand-new-user" exists
+ Then user "brand-new-user" has editable fields
+ | displayname |
+ | email |
+ | additional_mail |
+ | phone |
+ | address |
+ | website |
+ | twitter |
+ | bluesky |
+ | fediverse |
+ | organisation |
+ | role |
+ | headline |
+ | biography |
+ | profile_enabled |
+ | pronouns |
+ Given As an "brand-new-user"
+ Then user "brand-new-user" has editable fields
+ | displayname |
+ | email |
+ | additional_mail |
+ | phone |
+ | address |
+ | website |
+ | twitter |
+ | bluesky |
+ | fediverse |
+ | organisation |
+ | role |
+ | headline |
+ | biography |
+ | profile_enabled |
+ | pronouns |
+ Then user "self" has editable fields
+ | displayname |
+ | email |
+ | additional_mail |
+ | phone |
+ | address |
+ | website |
+ | twitter |
+ | bluesky |
+ | fediverse |
+ | organisation |
+ | role |
+ | headline |
+ | biography |
+ | profile_enabled |
+ | pronouns |
+
+ Scenario: Edit a user
+ Given As an "admin"
+ And user "brand-new-user" exists
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | displayname |
+ | value | Brand New User |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | quota |
+ | value | 12MB |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | email |
+ | value | no-reply@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | noreply@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | address |
+ | value | Foo Bar Town |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | website |
+ | value | https://nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | twitter |
+ | value | Nextcloud |
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | bluesky |
+ | value | nextcloud.bsky.social |
+ 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 |
+ | displayname | Brand New User |
+ | email | no-reply@nextcloud.com |
+ | additional_mail | no.reply@nextcloud.com;noreply@nextcloud.com |
+ | phone | +4971125242890 |
+ | address | Foo Bar Town |
+ | website | https://nextcloud.com |
+ | twitter | Nextcloud |
+ | bluesky | nextcloud.bsky.social |
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | organisation |
+ | value | Nextcloud GmbH |
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | role |
+ | value | Engineer |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ Then user "brand-new-user" has the following profile data
+ | userId | brand-new-user |
+ | displayname | Brand New User |
+ | organisation | Nextcloud GmbH |
+ | role | Engineer |
+ | address | Foo Bar Town |
+ | timezone | UTC |
+ | 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"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | phoneScope |
+ | value | v2-private |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | twitterScope |
+ | value | v2-local |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | blueskyScope |
+ | value | v2-local |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | addressScope |
+ | value | v2-federated |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | emailScope |
+ | value | v2-published |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | email |
+ | value | no-reply@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ # Duplicating primary address
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no-reply@nextcloud.com |
+ And the OCS status code should be "101"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply2@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ # Duplicating another additional address
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply2@nextcloud.com |
+ And the OCS status code should be "101"
+ And the HTTP status code should be "200"
+ Then user "brand-new-user" has
+ | id | brand-new-user |
+ | phoneScope | v2-private |
+ | twitterScope | v2-local |
+ | blueskyScope | v2-local |
+ | addressScope | v2-federated |
+ | emailScope | v2-published |
+
+ Scenario: Edit a user account multivalue property scopes
+ Given user "brand-new-user" exists
+ And As an "brand-new-user"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply3@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | noreply4@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
+ | key | no.reply3@nextcloud.com |
+ | value | v2-federated |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
+ | key | noreply4@nextcloud.com |
+ | value | v2-published |
+ Then 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 |
+ | additional_mailScope | v2-federated;v2-published |
+
+ Scenario: Edit a user account properties scopes with invalid or unsupported value
+ Given user "brand-new-user" exists
+ And As an "brand-new-user"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | phoneScope |
+ | value | invalid |
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | displaynameScope |
+ | value | v2-private |
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | emailScope |
+ | value | v2-private |
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+
+ Scenario: Edit a user account multi-value property scopes with invalid or unsupported value
+ Given user "brand-new-user" exists
+ And As an "brand-new-user"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply5@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user/additional_mailScope" with
+ | key | no.reply5@nextcloud.com |
+ | value | invalid |
+ Then the OCS status code should be "102"
+ And the HTTP status code should be "200"
+
+ Scenario: Delete a user account multi-value property value
+ Given user "brand-new-user" exists
+ And As an "brand-new-user"
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | no.reply6@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | additional_mail |
+ | value | noreply7@nextcloud.com |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When sending "PUT" to "/cloud/users/brand-new-user/additional_mail" with
+ | key | no.reply6@nextcloud.com |
+ | value | |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ Then user "brand-new-user" has
+ | additional_mail | noreply7@nextcloud.com |
+ Then user "brand-new-user" has not
+ | additional_mail | no.reply6@nextcloud.com |
+
+ Scenario: An admin cannot edit user account property scopes
+ Given As an "admin"
+ And user "brand-new-user" exists
+ When sending "PUT" to "/cloud/users/brand-new-user" with
+ | key | phoneScope |
+ | value | v2-private |
+ Then the OCS status code should be "113"
+ And the HTTP status code should be "200"
+
+ Scenario: Search by phone number
+ Given As an "admin"
+ And user "phone-user" exists
+ And sending "PUT" to "/cloud/users/phone-user" with
+ | key | phone |
+ | value | +49 711 / 25 24 28-90 |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ Then search users by phone for region "DE" with
+ | random-string1 | 0711 / 123 456 78 |
+ | random-string1 | 0711 / 252 428-90 |
+ | random-string2 | 0711 / 90-824 252 |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ Then phone matches returned are
+ | random-string1 | phone-user@localhost:8080 |
+
+ Scenario: Create a group
+ Given As an "admin"
+ And group "new-group" does not exist
+ When sending "POST" to "/cloud/groups" with
+ | groupid | new-group |
+ | password | 123456 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And group "new-group" exists
+ And group "new-group" has
+ | displayname | new-group |
+
+ Scenario: Create a group with custom display name
+ Given As an "admin"
+ And group "new-group" does not exist
+ When sending "POST" to "/cloud/groups" with
+ | groupid | new-group |
+ | password | 123456 |
+ | displayname | new-group-displayname |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And group "new-group" exists
+ And group "new-group" has
+ | displayname | new-group-displayname |
+
+ Scenario: Create a group with special characters
+ Given As an "admin"
+ And group "España" does not exist
+ When sending "POST" to "/cloud/groups" with
+ | groupid | España |
+ | password | 123456 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And group "España" exists
+ And group "España" has
+ | displayname | España |
+
+ Scenario: adding user to a group without sending the group
+ Given As an "admin"
+ And user "brand-new-user" exists
+ When sending "POST" to "/cloud/users/brand-new-user/groups" with
+ | groupid | |
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+
+ Scenario: adding user to a group which doesn't exist
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "not-group" does not exist
+ When sending "POST" to "/cloud/users/brand-new-user/groups" with
+ | groupid | not-group |
+ Then the OCS status code should be "102"
+ And the HTTP status code should be "200"
+
+ Scenario: adding user to a group without privileges
+ Given user "brand-new-user" exists
+ And As an "brand-new-user"
+ When sending "POST" to "/cloud/users/brand-new-user/groups" with
+ | groupid | new-group |
+ Then the OCS status code should be "403"
+ And the HTTP status code should be "200"
+
+ Scenario: adding user to a group
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ When sending "POST" to "/cloud/users/brand-new-user/groups" with
+ | groupid | new-group |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: getting groups of an user
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ When sending "GET" to "/cloud/users/brand-new-user/groups"
+ Then groups returned are
+ | new-group |
+ And the OCS status code should be "100"
+
+ Scenario: adding a user which doesn't exist to a group
+ Given As an "admin"
+ And user "not-user" does not exist
+ And group "new-group" exists
+ When sending "POST" to "/cloud/users/not-user/groups" with
+ | groupid | new-group |
+ Then the OCS status code should be "103"
+ And the HTTP status code should be "200"
+
+ Scenario: getting a group
+ Given As an "admin"
+ And group "new-group" exists
+ When sending "GET" to "/cloud/groups/new-group"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting all groups
+ Given As an "admin"
+ And group "new-group" exists
+ And group "admin" exists
+ When sending "GET" to "/cloud/groups"
+ Then groups returned are
+ | España |
+ | admin |
+ | hidden_group |
+ | new-group |
+
+ Scenario: create a subadmin
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ When sending "POST" to "/cloud/users/brand-new-user/subadmins" with
+ | groupid | new-group |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: get users using a subadmin
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ And user "brand-new-user" belongs to group "new-group"
+ And user "brand-new-user" is subadmin of group "new-group"
+ And As an "brand-new-user"
+ When sending "GET" to "/cloud/users"
+ Then users returned are
+ | brand-new-user |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: removing a user from a group which doesn't exists
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "not-group" does not exist
+ When sending "DELETE" to "/cloud/users/brand-new-user/groups" with
+ | groupid | not-group |
+ Then the OCS status code should be "102"
+
+ Scenario: removing a user from a group
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ And user "brand-new-user" belongs to group "new-group"
+ When sending "DELETE" to "/cloud/users/brand-new-user/groups" with
+ | groupid | new-group |
+ Then the OCS status code should be "100"
+ And user "brand-new-user" does not belong to group "new-group"
+
+ Scenario: create a subadmin using a user which not exist
+ Given As an "admin"
+ And user "not-user" does not exist
+ And group "new-group" exists
+ When sending "POST" to "/cloud/users/not-user/subadmins" with
+ | groupid | new-group |
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+
+ Scenario: create a subadmin using a group which not exist
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "not-group" does not exist
+ When sending "POST" to "/cloud/users/brand-new-user/subadmins" with
+ | groupid | not-group |
+ Then the OCS status code should be "102"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting subadmin groups
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ When sending "GET" to "/cloud/users/brand-new-user/subadmins"
+ Then subadmin groups returned are
+ | new-group |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting subadmin groups of a user which not exist
+ Given As an "admin"
+ And user "not-user" does not exist
+ And group "new-group" exists
+ When sending "GET" to "/cloud/users/not-user/subadmins"
+ Then the OCS status code should be "404"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting subadmin users of a group
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ When sending "GET" to "/cloud/groups/new-group/subadmins"
+ Then subadmin users returned are
+ | brand-new-user |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Getting subadmin users of a group which doesn't exist
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "not-group" does not exist
+ When sending "GET" to "/cloud/groups/not-group/subadmins"
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+
+ Scenario: Removing subadmin from a group
+ Given As an "admin"
+ And user "brand-new-user" exists
+ And group "new-group" exists
+ And user "brand-new-user" is subadmin of group "new-group"
+ When sending "DELETE" to "/cloud/users/brand-new-user/subadmins" with
+ | groupid | new-group |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: Delete a user
+ Given As an "admin"
+ And user "brand-new-user" exists
+ When sending "DELETE" to "/cloud/users/brand-new-user"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And user "brand-new-user" does not exist
+
+ Scenario: Delete a group
+ Given As an "admin"
+ And group "new-group" exists
+ When sending "DELETE" to "/cloud/groups/new-group"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And group "new-group" does not exist
+
+ Scenario: Delete a group with special characters
+ Given As an "admin"
+ And group "España" exists
+ When sending "DELETE" to "/cloud/groups/España"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And group "España" does not exist
+
+ Scenario: get enabled apps
+ Given As an "admin"
+ When sending "GET" to "/cloud/apps?filter=enabled"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And apps returned are
+ | cloud_federation_api |
+ | comments |
+ | contactsinteraction |
+ | dashboard |
+ | dav |
+ | federatedfilesharing |
+ | federation |
+ | files |
+ | files_reminders |
+ | files_sharing |
+ | files_trashbin |
+ | files_versions |
+ | lookup_server_connector |
+ | profile |
+ | provisioning_api |
+ | settings |
+ | sharebymail |
+ | systemtags |
+ | testing |
+ | theming |
+ | twofactor_backupcodes |
+ | updatenotification |
+ | user_ldap |
+ | user_status |
+ | viewer |
+ | workflowengine |
+ | webhook_listeners |
+ | weather_status |
+ | files_external |
+ | oauth2 |
+
+ Scenario: get app info
+ Given As an "admin"
+ When sending "GET" to "/cloud/apps/files"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+
+ Scenario: get app info from app that does not exist
+ Given As an "admin"
+ When sending "GET" to "/cloud/apps/this_app_should_never_exist"
+ Then the OCS status code should be "998"
+ And the HTTP status code should be "200"
+
+ Scenario: enable an app
+ Given invoking occ with "app:disable testing"
+ Given As an "admin"
+ And app "testing" is disabled
+ When sending "POST" to "/cloud/apps/testing"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And app "testing" is enabled
+
+ Scenario: enable an app that does not exist
+ Given As an "admin"
+ When sending "POST" to "/cloud/apps/this_app_should_never_exist"
+ Then the OCS status code should be "998"
+ And the HTTP status code should be "200"
+
+ Scenario: disable an app
+ Given invoking occ with "app:enable testing"
+ Given As an "admin"
+ And app "testing" is enabled
+ When sending "DELETE" to "/cloud/apps/testing"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And app "testing" is disabled
+ Given invoking occ with "app:enable testing"
+
+ Scenario: disable an user
+ Given As an "admin"
+ And user "user1" exists
+ When sending "PUT" to "/cloud/users/user1/disable"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And user "user1" is disabled
+
+ Scenario: enable an user
+ Given As an "admin"
+ And user "user1" exists
+ And assure user "user1" is disabled
+ When sending "PUT" to "/cloud/users/user1/enable"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And user "user1" is enabled
+
+ Scenario: Subadmin should be able to enable or disable an user in their group
+ Given As an "admin"
+ And user "subadmin" exists
+ And user "user1" exists
+ And group "new-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And user "user1" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And As an "subadmin"
+ When sending "PUT" to "/cloud/users/user1/disable"
+ Then the OCS status code should be "100"
+ Then the HTTP status code should be "200"
+ And As an "admin"
+ And user "user1" is disabled
+
+ Scenario: Subadmin should not be able to enable or disable an user not in their group
+ Given As an "admin"
+ And user "subadmin" exists
+ And user "user1" exists
+ And group "new-group" exists
+ And group "another-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And user "user1" belongs to group "another-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And As an "subadmin"
+ When sending "PUT" to "/cloud/users/user1/disable"
+ Then the OCS status code should be "998"
+ Then the HTTP status code should be "200"
+ And As an "admin"
+ And user "user1" is enabled
+
+ Scenario: Subadmins should not be able to disable users that have admin permissions in their group
+ Given As an "admin"
+ And user "another-admin" exists
+ And user "subadmin" exists
+ And group "new-group" exists
+ And user "another-admin" belongs to group "admin"
+ And user "subadmin" belongs to group "new-group"
+ And user "another-admin" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And As an "subadmin"
+ When sending "PUT" to "/cloud/users/another-admin/disable"
+ Then the OCS status code should be "998"
+ Then the HTTP status code should be "200"
+ And As an "admin"
+ And user "another-admin" is enabled
+
+ Scenario: Admin can disable another admin user
+ Given As an "admin"
+ And user "another-admin" exists
+ And user "another-admin" belongs to group "admin"
+ When sending "PUT" to "/cloud/users/another-admin/disable"
+ Then the OCS status code should be "100"
+ Then the HTTP status code should be "200"
+ And user "another-admin" is disabled
+
+ Scenario: Admin can enable another admin user
+ Given As an "admin"
+ And user "another-admin" exists
+ And user "another-admin" belongs to group "admin"
+ And assure user "another-admin" is disabled
+ When sending "PUT" to "/cloud/users/another-admin/enable"
+ Then the OCS status code should be "100"
+ Then the HTTP status code should be "200"
+ And user "another-admin" is enabled
+
+ Scenario: Admin can disable subadmins in the same group
+ Given As an "admin"
+ And user "subadmin" exists
+ And group "new-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And user "admin" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ When sending "PUT" to "/cloud/users/subadmin/disable"
+ Then the OCS status code should be "100"
+ Then the HTTP status code should be "200"
+ And user "subadmin" is disabled
+
+ Scenario: Admin can enable subadmins in the same group
+ Given As an "admin"
+ And user "subadmin" exists
+ And group "new-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And user "admin" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And assure user "another-admin" is disabled
+ When sending "PUT" to "/cloud/users/subadmin/disable"
+ Then the OCS status code should be "100"
+ Then the HTTP status code should be "200"
+ And user "subadmin" is disabled
+
+ Scenario: Admin user cannot disable himself
+ Given As an "admin"
+ And user "another-admin" exists
+ And user "another-admin" belongs to group "admin"
+ And As an "another-admin"
+ When sending "PUT" to "/cloud/users/another-admin/disable"
+ Then the OCS status code should be "101"
+ And the HTTP status code should be "200"
+ And As an "admin"
+ And user "another-admin" is enabled
+
+ Scenario:Admin user cannot enable himself
+ Given As an "admin"
+ And user "another-admin" exists
+ And user "another-admin" belongs to group "admin"
+ And assure user "another-admin" is disabled
+ And As an "another-admin"
+ When sending "PUT" to "/cloud/users/another-admin/enable"
+ And As an "admin"
+ Then user "another-admin" is disabled
+
+ Scenario: disable an user with a regular user
+ Given As an "admin"
+ And user "user1" exists
+ And user "user2" exists
+ And As an "user1"
+ When sending "PUT" to "/cloud/users/user2/disable"
+ Then the OCS status code should be "403"
+ And the HTTP status code should be "200"
+ And As an "admin"
+ And user "user2" is enabled
+
+ Scenario: enable an user with a regular user
+ Given As an "admin"
+ And user "user1" exists
+ And user "user2" exists
+ And assure user "user2" is disabled
+ And As an "user1"
+ When sending "PUT" to "/cloud/users/user2/enable"
+ Then the OCS status code should be "403"
+ And the HTTP status code should be "200"
+ And As an "admin"
+ And user "user2" is disabled
+
+ Scenario: Subadmin should not be able to disable himself
+ Given As an "admin"
+ And user "subadmin" exists
+ And group "new-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And As an "subadmin"
+ When sending "PUT" to "/cloud/users/subadmin/disable"
+ Then the OCS status code should be "101"
+ Then the HTTP status code should be "200"
+ And As an "admin"
+ And user "subadmin" is enabled
+
+ Scenario: Subadmin should not be able to enable himself
+ Given As an "admin"
+ And user "subadmin" exists
+ And group "new-group" exists
+ And user "subadmin" belongs to group "new-group"
+ And Assure user "subadmin" is subadmin of group "new-group"
+ And assure user "subadmin" is disabled
+ And As an "subadmin"
+ When sending "PUT" to "/cloud/users/subadmin/enabled"
+ And As an "admin"
+ And user "subadmin" is disabled
+
+ Scenario: Making a ocs request with an enabled user
+ Given As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When sending "GET" to "/cloud/capabilities"
+ Then the HTTP status code should be "200"
+ And the OCS status code should be "100"
+
+ Scenario: Making a web request with an enabled user
+ Given As an "admin"
+ And user "user0" exists
+ And As an "user0"
+ When sending "GET" with exact url to "/index.php/apps/files"
+ Then the HTTP status code should be "200"
+
+ Scenario: Making a ocs request with a disabled user
+ Given As an "admin"
+ And user "user0" exists
+ And assure user "user0" is disabled
+ And As an "user0"
+ When sending "GET" to "/cloud/capabilities"
+ Then the OCS status code should be "997"
+ And the HTTP status code should be "401"
+
+ Scenario: Making a web request with a disabled user
+ Given As an "admin"
+ And user "user0" exists
+ And assure user "user0" is disabled
+ And As an "user0"
+ When sending "GET" with exact url to "/index.php/apps/files"
+ And the HTTP status code should be "401"
diff --git a/build/integration/features/provisioning-v2.feature b/build/integration/features/provisioning-v2.feature
index 729c812cb8c..1169dc04b5f 100644
--- a/build/integration/features/provisioning-v2.feature
+++ b/build/integration/features/provisioning-v2.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: provisioning
Background:
Given using api version "2"
diff --git a/build/integration/federation_features/cleanup-remote-storage.feature b/build/integration/federation_features/cleanup-remote-storage.feature
new file mode 100644
index 00000000000..a3585bdee96
--- /dev/null
+++ b/build/integration/federation_features/cleanup-remote-storage.feature
@@ -0,0 +1,76 @@
+# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: cleanup-remote-storage
+ Background:
+ Given using api version "1"
+
+ Scenario: cleanup remote storage with no storage
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And As an "user1"
+ And Deleting last share
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Deleting last share
+ And Using server "LOCAL"
+ When invoking occ with "sharing:cleanup-remote-storage"
+ Then the command was successful
+ And the command output contains the text "0 remote storage(s) need(s) to be checked"
+ And the command output contains the text "0 remote share(s) exist"
+ And the command output contains the text "no storages deleted"
+
+ Scenario: cleanup remote storage with active storages
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And Using server "LOCAL"
+ # Accept and download the file to ensure that a storage is created for the
+ # federated share
+ And User "user0" from server "LOCAL" accepts last pending share
+ And As an "user0"
+ And Downloading file "/remote-share.txt"
+ And the HTTP status code should be "200"
+ When invoking occ with "sharing:cleanup-remote-storage"
+ Then the command was successful
+ And the command output contains the text "1 remote storage(s) need(s) to be checked"
+ And the command output contains the text "1 remote share(s) exist"
+ And the command output contains the text "no storages deleted"
+
+ Scenario: cleanup remote storage with inactive storages
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And Using server "LOCAL"
+ # Accept and download the file to ensure that a storage is created for the
+ # federated share
+ And User "user0" from server "LOCAL" accepts last pending share
+ And As an "user0"
+ And Downloading file "/remote-share.txt"
+ And the HTTP status code should be "200"
+ And Using server "REMOTE"
+ And As an "user1"
+ And Deleting last share
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When Using server "LOCAL"
+ And invoking occ with "sharing:cleanup-remote-storage"
+ Then the command was successful
+ And the command output contains the text "1 remote storage(s) need(s) to be checked"
+ And the command output contains the text "0 remote share(s) exist"
+ And the command output contains the text "deleted 1 storage"
diff --git a/build/integration/federation_features/federated.feature b/build/integration/federation_features/federated.feature
index 17ec6b4b43e..d3a414cb804 100644
--- a/build/integration/federation_features/federated.feature
+++ b/build/integration/federation_features/federated.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: federated
Background:
Given using api version "1"
@@ -5,7 +8,7 @@ Feature: federated
Scenario: Federate share a file with another server
Given Using server "REMOTE"
And user "user1" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
Then the OCS status code should be "100"
@@ -26,7 +29,13 @@ Feature: federated
| file_parent | A_NUMBER |
| displayname_owner | user0 |
| share_with | user1@REMOTE |
- | share_with_displayname | user1@REMOTE |
+ | share_with_displayname | user1 |
+ Given Using server "REMOTE"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ Then the list of returned shares has 1 shares
Scenario: Federated group share a file with another server
Given Using server "REMOTE"
@@ -37,7 +46,7 @@ Feature: federated
And As an "admin"
And Add user "gs-user1" to the group "group1"
And Add user "gs-user2" to the group "group1"
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
And user "gs-user0" exists
When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE"
@@ -61,11 +70,10 @@ Feature: federated
| share_with | group1@REMOTE |
| share_with_displayname | group1@REMOTE |
-
Scenario: Federate share a file with local server
Given Using server "LOCAL"
And user "user0" exists
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And user "user1" exists
When User "user1" from server "REMOTE" shares "/textfile0.txt" with user "user0" from server "LOCAL"
Then the OCS status code should be "100"
@@ -86,15 +94,15 @@ Feature: federated
| file_parent | A_NUMBER |
| displayname_owner | user1 |
| share_with | user0@LOCAL |
- | share_with_displayname | user0@LOCAL |
+ | share_with_displayname | user0 |
Scenario: Remote sharee can see the pending share
Given Using server "REMOTE"
And user "user1" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And As an "user1"
When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
Then the OCS status code should be "100"
@@ -119,11 +127,11 @@ Feature: federated
And As an "admin"
And Add user "gs-user1" to the group "group1"
And Add user "gs-user2" to the group "group1"
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
And user "gs-user0" exists
When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE"
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And As an "gs-user1"
When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
Then the OCS status code should be "100"
@@ -156,7 +164,7 @@ Feature: federated
Scenario: accept a pending remote share
Given Using server "REMOTE"
And user "user1" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
When User "user1" from server "REMOTE" accepts last pending share
@@ -172,7 +180,7 @@ Feature: federated
And As an "admin"
And Add user "gs-user1" to the group "group1"
And Add user "gs-user2" to the group "group1"
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
And user "gs-user0" exists
When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "group1" from server "REMOTE"
@@ -184,45 +192,45 @@ Feature: federated
Given Using server "REMOTE"
And user "user1" exists
And user "user2" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
And User "user1" from server "REMOTE" accepts last pending share
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And As an "user1"
When creating a share with
| path | /textfile0 (2).txt |
| shareType | 0 |
| shareWith | user2 |
| permissions | 19 |
- #Then the OCS status code should be "100"
- #And the HTTP status code should be "200"
- #And Share fields of last share match with
- # | id | A_NUMBER |
- # | item_type | file |
- # | item_source | A_NUMBER |
- # | share_type | 0 |
- # | file_source | A_NUMBER |
- # | path | /textfile0 (2).txt |
- # | permissions | 19 |
- # | stime | A_NUMBER |
- # | storage | A_NUMBER |
- # | mail_send | 1 |
- # | uid_owner | user1 |
- # | file_parent | A_NUMBER |
- # | displayname_owner | user1 |
- # | share_with | user2 |
- # | share_with_displayname | user2 |
+ # Then the OCS status code should be "100"
+ # And the HTTP status code should be "200"
+ # And Share fields of last share match with
+ # | id | A_NUMBER |
+ # | item_type | file |
+ # | item_source | A_NUMBER |
+ # | share_type | 0 |
+ # | file_source | A_NUMBER |
+ # | path | /textfile0 (2).txt |
+ # | permissions | 19 |
+ # | stime | A_NUMBER |
+ # | storage | A_NUMBER |
+ # | mail_send | 1 |
+ # | uid_owner | user1 |
+ # | file_parent | A_NUMBER |
+ # | displayname_owner | user1 |
+ # | share_with | user2 |
+ # | share_with_displayname | user2 |
Scenario: Overwrite a federated shared file as recipient
Given Using server "REMOTE"
And user "user1" exists
And user "user2" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
And User "user1" from server "REMOTE" accepts last pending share
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And As an "user1"
And User "user1" modifies text of "/textfile0.txt" with text "BLABLABLA"
When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/textfile0 (2).txt"
@@ -233,58 +241,351 @@ Feature: federated
Given Using server "REMOTE"
And user "user1" exists
And user "user2" exists
- And Using server "LOCAL"
+ Given Using server "LOCAL"
And user "user0" exists
And User "user0" from server "LOCAL" shares "/PARENT" with user "user1" from server "REMOTE"
And User "user1" from server "REMOTE" accepts last pending share
- And Using server "REMOTE"
+ Given Using server "REMOTE"
And As an "user1"
And User "user1" modifies text of "/textfile0.txt" with text "BLABLABLA"
- #When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/PARENT (2)/textfile0.txt"
- #And Downloading file "/PARENT (2)/textfile0.txt" with range "bytes=0-8"
- #Then Downloaded content should be "BLABLABLA"
+ When User "user1" uploads file "../../data/user1/files/textfile0.txt" to "/PARENT (2)/textfile0.txt"
+ And Downloading file "/PARENT (2)/textfile0.txt" with range "bytes=0-8"
+ Then Downloaded content should be "BLABLABLA"
- Scenario: Overwrite a federated shared file as recipient using old chunking
+ Scenario: List federated share from another server not accepted yet
+ Given Using server "LOCAL"
+ And user "user0" exists
Given Using server "REMOTE"
And user "user1" exists
- And user "user2" exists
- And Using server "LOCAL"
- And user "user0" exists
- And User "user0" from server "LOCAL" shares "/textfile0.txt" with user "user1" from server "REMOTE"
- And User "user1" from server "REMOTE" accepts last pending share
- And Using server "REMOTE"
- And As an "user1"
- #And user "user1" uploads chunk file "1" of "3" with "AAAAA" to "/textfile0 (2).txt"
- #And user "user1" uploads chunk file "2" of "3" with "BBBBB" to "/textfile0 (2).txt"
- #And user "user1" uploads chunk file "3" of "3" with "CCCCC" to "/textfile0 (2).txt"
- #When Downloading file "/textfile0 (2).txt" with range "bytes=0-4"
- #Then Downloaded content should be "AAAAA"
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ Then the list of returned shares has 0 shares
- Scenario: Overwrite a federated shared folder as recipient using old chunking
+ Scenario: List federated share from another server
+ Given Using server "LOCAL"
+ And user "user0" exists
Given Using server "REMOTE"
And user "user1" exists
- And user "user2" exists
- And Using server "LOCAL"
- And user "user0" exists
- And User "user0" from server "LOCAL" shares "/PARENT" with user "user1" from server "REMOTE"
- And User "user1" from server "REMOTE" accepts last pending share
- And Using server "REMOTE"
- And As an "user1"
- #And user "user1" uploads chunk file "1" of "3" with "AAAAA" to "/PARENT (2)/textfile0.txt"
- #And user "user1" uploads chunk file "2" of "3" with "BBBBB" to "/PARENT (2)/textfile0.txt"
- #And user "user1" uploads chunk file "3" of "3" with "CCCCC" to "/PARENT (2)/textfile0.txt"
- #When Downloading file "/PARENT (2)/textfile0.txt" with range "bytes=3-13"
- #Then Downloaded content should be "AABBBBBCCCC"
-
-
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ Then the list of returned shares has 1 shares
+ And remote share 0 is returned with
+ | remote | http://localhost:8180/ |
+ | name | /remote-share.txt |
+ | owner | user1 |
+ | user | user0 |
+ | mountpoint | /remote-share.txt |
+ | mimetype | text/plain |
+ | mtime | A_NUMBER |
+ | permissions | 27 |
+ | type | file |
+ | file_id | A_NUMBER |
+ Scenario: List federated share from another server no longer reachable
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And remote server is stopped
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ Then the list of returned shares has 1 shares
+ And remote share 0 is returned with
+ | remote | http://localhost:8180/ |
+ | name | /remote-share.txt |
+ | owner | user1 |
+ | user | user0 |
+ | mountpoint | /remote-share.txt |
+ Scenario: List federated share from another server no longer reachable after caching the file entry
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ # Checking that the file exists caches the file entry, which causes an
+ # exception to be thrown when getting the file info if the remote server is
+ # unreachable.
+ And as "user0" the file "/remote-share.txt" exists
+ And remote server is stopped
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ Then the list of returned shares has 1 shares
+ And remote share 0 is returned with
+ | remote | http://localhost:8180/ |
+ | name | /remote-share.txt |
+ | owner | user1 |
+ | user | user0 |
+ | mountpoint | /remote-share.txt |
+ Scenario: Delete federated share with another server
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 1 shares
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And as "user0" the file "/remote-share.txt" exists
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ Given Using server "REMOTE"
+ When As an "user1"
+ And Deleting last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 0 shares
+ Given Using server "LOCAL"
+ And as "user0" the file "/remote-share.txt" does not exist
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ Scenario: Delete federated share from another server
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 1 shares
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And as "user0" the file "/remote-share.txt" exists
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ When user "user0" deletes last accepted remote share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And as "user0" the file "/remote-share.txt" does not exist
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ Given Using server "REMOTE"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 0 shares
+ Scenario: Delete federated share from another server no longer reachable
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And as "user0" the file "/remote-share.txt" exists
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ And remote server is stopped
+ When user "user0" deletes last accepted remote share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And as "user0" the file "/remote-share.txt" does not exist
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ Scenario: Delete federated share file from another server
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 1 shares
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And as "user0" the file "/remote-share.txt" exists
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ When User "user0" deletes file "/remote-share.txt"
+ Then the HTTP status code should be "204"
+ And as "user0" the file "/remote-share.txt" does not exist
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ Given Using server "REMOTE"
+ And As an "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And the list of returned shares has 0 shares
+ Scenario: Delete federated share file from another server no longer reachable
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ Given Using server "LOCAL"
+ And User "user0" from server "LOCAL" accepts last pending share
+ And as "user0" the file "/remote-share.txt" exists
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ And remote server is stopped
+ When User "user0" deletes file "/remote-share.txt"
+ Then the HTTP status code should be "204"
+ And as "user0" the file "/remote-share.txt" does not exist
+ And As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ Scenario: Share to a non-trusted server will NOT auto accept
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "userfed2" exists
+ And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes"
+ When As an "user0"
+ When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "userfed2" from server "REMOTE"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false"
+ And the list of returned shares has 1 shares
+ Given Using server "REMOTE"
+ And using new dav path
+ And As an "userfed2"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 1 shares
+ And as "userfed2" the file "/textfile0 (2).txt" does not exist
+ Scenario: Share to a non-trusted server group will NOT auto accept
+ Given Using server "REMOTE"
+ And parameter "incoming_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
+ And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes"
+ And user "gs-userfed3" exists
+ And user "gs-userfed4" exists
+ And group "groupfed2" exists
+ And As an "admin"
+ And Add user "gs-userfed3" to the group "groupfed2"
+ And Add user "gs-userfed4" to the group "groupfed2"
+ Given Using server "LOCAL"
+ And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
+ And user "gs-user0" exists
+ When As an "gs-user0"
+ When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "groupfed2" from server "REMOTE"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false"
+ And the list of returned shares has 1 shares
+ Given Using server "REMOTE"
+ And using new dav path
+ And As an "gs-userfed3"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 1 shares
+ And as "gs-userfed3" the file "/textfile0 (2).txt" does not exist
+ And As an "gs-userfed4"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 0 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 1 shares
+ And as "gs-userfed4" the file "/textfile0 (2).txt" does not exist
+ @TrustedFederation
+ Scenario: Share to a trusted server auto accept
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "userfed1" exists
+ And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes"
+ When As an "user0"
+ When User "user0" from server "LOCAL" shares "/textfile0.txt" with user "userfed1" from server "REMOTE"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false"
+ And the list of returned shares has 1 shares
+ Given Using server "REMOTE"
+ And using new dav path
+ And As an "userfed1"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 0 shares
+ And as "userfed1" the file "/textfile0 (2).txt" exists
+ @TrustedFederation
+ Scenario: Share to a trusted server group auto accept
+ Given Using server "REMOTE"
+ And parameter "incoming_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
+ And parameter "federated_trusted_share_auto_accept" of app "files_sharing" is set to "yes"
+ And user "gs-userfed1" exists
+ And user "gs-userfed2" exists
+ And group "groupfed1" exists
+ And As an "admin"
+ And Add user "gs-userfed1" to the group "groupfed1"
+ And Add user "gs-userfed2" to the group "groupfed1"
+ Given Using server "LOCAL"
+ And parameter "outgoing_server2server_group_share_enabled" of app "files_sharing" is set to "yes"
+ And user "gs-user0" exists
+ When As an "gs-user0"
+ When User "gs-user0" from server "LOCAL" shares "/textfile0.txt" with group "groupfed1" from server "REMOTE"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=false"
+ And the list of returned shares has 1 shares
+ Given Using server "REMOTE"
+ And using new dav path
+ And As an "gs-userfed1"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 0 shares
+ And as "gs-userfed1" the file "/textfile0 (2).txt" exists
+ And As an "gs-userfed2"
+ And sending "GET" to "/apps/files_sharing/api/v1/remote_shares"
+ And the list of returned shares has 1 shares
+ When sending "GET" to "/apps/files_sharing/api/v1/remote_shares/pending"
+ And the list of returned shares has 0 shares
+ And as "gs-userfed2" the file "/textfile0 (2).txt" exists
diff --git a/build/integration/file_conversions/file_conversions.feature b/build/integration/file_conversions/file_conversions.feature
new file mode 100644
index 00000000000..92dc11a647a
--- /dev/null
+++ b/build/integration/file_conversions/file_conversions.feature
@@ -0,0 +1,122 @@
+# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+Feature: conversions
+ Background:
+ Given using api version "2"
+ Given using new dav path
+ Given user "user0" exists
+
+ Scenario: Converting a file works
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ Then as "user0" the file "/image.jpg" exists
+ When user "user0" converts file "/image.jpg" to "image/png"
+ Then the HTTP status code should be "201"
+ Then the OCS status code should be "201"
+ Then as "user0" the file "/image.png" exists
+
+ Scenario: Converting a file to a given path works
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ And User "user0" created a folder "/folder"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the folder "/folder" exists
+ When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png"
+ Then the HTTP status code should be "201"
+ Then the OCS status code should be "201"
+ Then as "user0" the file "/folder/image.png" exists
+ Then as "user0" the file "/image.png" does not exist
+
+ Scenario: Converting a file path with overwrite
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ And user "user0" uploads file "data/green-square-256.png" to "/image.png"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the file "/image.png" exists
+ When user "user0" converts file "/image.jpg" to "image/png"
+ Then the HTTP status code should be "201"
+ Then the OCS status code should be "201"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the file "/image.png" exists
+ Then as "user0" the file "/image (2).png" exists
+
+ Scenario: Converting a file path with overwrite to a given path
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ And User "user0" created a folder "/folder"
+ And user "user0" uploads file "data/green-square-256.png" to "/folder/image.png"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the folder "/folder" exists
+ Then as "user0" the file "/folder/image.png" exists
+ When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png"
+ Then the HTTP status code should be "201"
+ Then the OCS status code should be "201"
+ Then as "user0" the file "/folder/image.png" exists
+ Then as "user0" the file "/folder/image (2).png" exists
+ Then as "user0" the file "/image.png" does not exist
+ Then as "user0" the file "/image.jpg" exists
+
+ Scenario: Converting a file which does not exist fails
+ When user "user0" converts file "/image.jpg" to "image/png"
+ Then the HTTP status code should be "404"
+ Then the OCS status code should be "404"
+ Then as "user0" the file "/image.jpg" does not exist
+ Then as "user0" the file "/image.png" does not exist
+
+ Scenario: Converting a file to an invalid destination path fails
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png"
+ Then the HTTP status code should be "404"
+ Then the OCS status code should be "404"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the file "/folder/image.png" does not exist
+
+ Scenario: Converting a file to an invalid format fails
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ When user "user0" converts file "/image.jpg" to "image/invalid"
+ Then the HTTP status code should be "500"
+ Then the OCS status code should be "999"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the file "/image.png" does not exist
+
+Scenario: Converting a file to a given path without extension fails
+ Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
+ And User "user0" created a folder "/folder"
+ Then as "user0" the file "/image.jpg" exists
+ Then as "user0" the folder "/folder" exists
+ When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image"
+ Then the HTTP status code should be "400"
+ Then the OCS status code should be "400"
+ Then as "user0" the file "/folder/image.png" does not exist
+ Then as "user0" the file "/image.png" does not exist
+
+ @local_storage
+ Scenario: Converting a file bigger than 100 MiB fails
+ Given file "/image.jpg" of size 108003328 is created in local storage
+ Then as "user0" the folder "/local_storage" exists
+ Then as "user0" the file "/local_storage/image.jpg" exists
+ When user "user0" converts file "/local_storage/image.jpg" to "image/png" and saves it to "/image.png"
+ Then the HTTP status code should be "400"
+ Then the OCS status code should be "400"
+ Then as "user0" the file "/image.png" does not exist
+
+ Scenario: Forbid conversion to a destination without create permission
+ Given user "user1" exists
+ # Share the folder with user1
+ Given User "user0" created a folder "/folder"
+ Then As an "user0"
+ When creating a share with
+ | path | folder |
+ | shareWith | user1 |
+ | shareType | 0 |
+ | permissions | 1 |
+ Then the OCS status code should be "200"
+ And the HTTP status code should be "200"
+ # Create the folder, upload the image
+ Then As an "user1"
+ Given user "user1" accepts last share
+ Given as "user1" the folder "/folder" exists
+ Given user "user1" uploads file "data/clouds.jpg" to "/image.jpg"
+ Then as "user1" the file "/image.jpg" exists
+ # Try to convert the image to a folder where user1 has no create permission
+ When user "user1" converts file "/image.jpg" to "image/png" and saves it to "/folder/folder.png"
+ Then the OCS status code should be "403"
+ And the HTTP status code should be "403"
+ Then as "user1" the file "/folder/folder.png" does not exist
diff --git a/build/integration/features/checksums.feature b/build/integration/files_features/checksums.feature
index d391e93afe8..d797c7a503e 100644
--- a/build/integration/features/checksums.feature
+++ b/build/integration/files_features/checksums.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: checksums
Scenario: Uploading a file with checksum should work
@@ -58,19 +61,3 @@ Feature: checksums
When user "user0" uploads file "data/textfile.txt" to "/myChecksumFile.txt"
And user "user0" downloads the file "/myChecksumFile.txt"
Then The OC-Checksum header should not be there
-
- Scenario: Uploading a chunked file with checksum should return the checksum in the propfind
- Given user "user0" exists
- And user "user0" uploads chunk file "1" of "3" with "AAAAA" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- And user "user0" uploads chunk file "2" of "3" with "BBBBB" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- And user "user0" uploads chunk file "3" of "3" with "CCCCC" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- When user "user0" request the checksum of "/myChecksumFile.txt" via propfind
- Then The webdav checksum should match "MD5:e892fdd61a74bc89cd05673cc2e22f88"
-
- Scenario: Uploading a chunked file with checksum should return the checksum in the download header
- Given user "user0" exists
- And user "user0" uploads chunk file "1" of "3" with "AAAAA" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- And user "user0" uploads chunk file "2" of "3" with "BBBBB" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- And user "user0" uploads chunk file "3" of "3" with "CCCCC" to "/myChecksumFile.txt" with checksum "MD5:e892fdd61a74bc89cd05673cc2e22f88"
- When user "user0" downloads the file "/myChecksumFile.txt"
- Then The header checksum should match "MD5:e892fdd61a74bc89cd05673cc2e22f88"
diff --git a/build/integration/features/download.feature b/build/integration/files_features/download.feature
index 16d346b0150..f9d4e7e95b9 100644
--- a/build/integration/features/download.feature
+++ b/build/integration/files_features/download.feature
@@ -1,59 +1,61 @@
+# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: download
- Scenario: downloading 2 small files returns a zip32
+ Scenario: downloading 2 small files
Given using new dav path
And user "user0" exists
And User "user0" copies file "/welcome.txt" to "/welcome2.txt"
When user "user0" downloads zip file for entries '"welcome.txt","welcome2.txt"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/welcome.txt" from "user0" data
And the downloaded zip file contains a file named "welcome2.txt" with the contents of "/welcome2.txt" from "user0" data
- Scenario: downloading a small file and a directory returns a zip32
+ Scenario: downloading a small file and a directory
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/emptySubFolder"
When user "user0" downloads zip file for entries '"welcome.txt","emptySubFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "emptySubFolder/"
- Scenario: downloading a small file and 2 nested directories returns a zip32
+ Scenario: downloading a small file and 2 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/subFolder"
And user "user0" created a folder "/subFolder/emptySubSubFolder"
When user "user0" downloads zip file for entries '"welcome.txt","subFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "subFolder/"
And the downloaded zip file contains a folder named "subFolder/emptySubSubFolder/"
- Scenario: downloading dir with 2 small files returns a zip32
+ Scenario: downloading dir with 2 small files
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/sparseFolder"
And User "user0" copies file "/welcome.txt" to "/sparseFolder/welcome.txt"
And User "user0" copies file "/welcome.txt" to "/sparseFolder/welcome2.txt"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a file named "sparseFolder/welcome2.txt" with the contents of "/sparseFolder/welcome2.txt" from "user0" data
- Scenario: downloading dir with a small file and a directory returns a zip32
+ Scenario: downloading dir with a small file and a directory
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/sparseFolder"
And User "user0" copies file "/welcome.txt" to "/sparseFolder/welcome.txt"
And user "user0" created a folder "/sparseFolder/emptySubFolder"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "sparseFolder/emptySubFolder/"
- Scenario: downloading dir with a small file and 2 nested directories returns a zip32
+ Scenario: downloading dir with a small file and 2 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/sparseFolder"
@@ -61,35 +63,35 @@ Feature: download
And user "user0" created a folder "/sparseFolder/subFolder"
And user "user0" created a folder "/sparseFolder/subFolder/emptySubSubFolder"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "sparseFolder/subFolder/"
And the downloaded zip file contains a folder named "sparseFolder/subFolder/emptySubSubFolder/"
- Scenario: downloading (from folder) 2 small files returns a zip32
+ Scenario: downloading (from folder) 2 small files
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
And User "user0" copies file "/welcome.txt" to "/baseFolder/welcome.txt"
And User "user0" copies file "/welcome.txt" to "/baseFolder/welcome2.txt"
When user "user0" downloads zip file for entries '"welcome.txt","welcome2.txt"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/baseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a file named "welcome2.txt" with the contents of "/baseFolder/welcome2.txt" from "user0" data
- Scenario: downloading (from folder) a small file and a directory returns a zip32
+ Scenario: downloading (from folder) a small file and a directory
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
And User "user0" copies file "/welcome.txt" to "/baseFolder/welcome.txt"
And user "user0" created a folder "/baseFolder/emptySubFolder"
When user "user0" downloads zip file for entries '"welcome.txt","emptySubFolder"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/baseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "emptySubFolder/"
- Scenario: downloading (from folder) a small file and 2 nested directories returns a zip32
+ Scenario: downloading (from folder) a small file and 2 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
@@ -97,12 +99,12 @@ Feature: download
And user "user0" created a folder "/baseFolder/subFolder"
And user "user0" created a folder "/baseFolder/subFolder/emptySubSubFolder"
When user "user0" downloads zip file for entries '"welcome.txt","subFolder"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/baseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "subFolder/"
And the downloaded zip file contains a folder named "subFolder/emptySubSubFolder/"
- Scenario: downloading (from folder) dir with 2 small files returns a zip32
+ Scenario: downloading (from folder) dir with 2 small files
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
@@ -110,12 +112,12 @@ Feature: download
And User "user0" copies file "/welcome.txt" to "/baseFolder/sparseFolder/welcome.txt"
And User "user0" copies file "/welcome.txt" to "/baseFolder/sparseFolder/welcome2.txt"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/baseFolder/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a file named "sparseFolder/welcome2.txt" with the contents of "/baseFolder/sparseFolder/welcome2.txt" from "user0" data
- Scenario: downloading (from folder) dir with a small file and a directory returns a zip32
+ Scenario: downloading (from folder) dir with a small file and a directory
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
@@ -123,12 +125,12 @@ Feature: download
And User "user0" copies file "/welcome.txt" to "/baseFolder/sparseFolder/welcome.txt"
And user "user0" created a folder "/baseFolder/sparseFolder/emptySubFolder"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/baseFolder/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "sparseFolder/emptySubFolder/"
- Scenario: downloading (from folder) dir with a small file and 2 nested directories returns a zip32
+ Scenario: downloading (from folder) dir with a small file and 2 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/baseFolder"
@@ -137,14 +139,14 @@ Feature: download
And user "user0" created a folder "/baseFolder/sparseFolder/subFolder"
And user "user0" created a folder "/baseFolder/sparseFolder/subFolder/emptySubSubFolder"
When user "user0" downloads zip file for entries '"sparseFolder"' in folder "/baseFolder/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "sparseFolder/"
And the downloaded zip file contains a file named "sparseFolder/welcome.txt" with the contents of "/baseFolder/sparseFolder/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "sparseFolder/subFolder/"
And the downloaded zip file contains a folder named "sparseFolder/subFolder/emptySubSubFolder/"
@large
- Scenario: downloading small file and dir with 65524 small files and 9 nested directories returns a zip32
+ Scenario: downloading small file and dir with 65524 small files and 9 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/crowdedFolder"
@@ -172,7 +174,7 @@ Feature: download
And user "user0" created a folder "/crowdedFolder/subFolder7/subSubFolder"
And user "user0" created a folder "/crowdedFolder/subFolder7/subSubFolder/emptySubSubSubFolder"
When user "user0" downloads zip file for entries '"welcome.txt","crowdedFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a file named "welcome.txt" with the contents of "/welcome.txt" from "user0" data
And the downloaded zip file contains a folder named "crowdedFolder/"
And the downloaded zip file contains a folder named "crowdedFolder/subFolder1/"
@@ -181,7 +183,7 @@ Feature: download
And the downloaded zip file contains a folder named "crowdedFolder/subFolder7/subSubFolder/emptySubSubSubFolder/"
@large
- Scenario: downloading dir with 65525 small files and 9 nested directories returns a zip32
+ Scenario: downloading dir with 65525 small files and 9 nested directories
Given using new dav path
And user "user0" exists
And user "user0" created a folder "/crowdedFolder"
@@ -209,7 +211,7 @@ Feature: download
And user "user0" created a folder "/crowdedFolder/subFolder7/subSubFolder"
And user "user0" created a folder "/crowdedFolder/subFolder7/subSubFolder/emptySubSubSubFolder"
When user "user0" downloads zip file for entries '"crowdedFolder"' in folder "/"
- Then the downloaded zip file is a zip32 file
+ Then the downloaded file is a zip file
And the downloaded zip file contains a folder named "crowdedFolder/"
And the downloaded zip file contains a folder named "crowdedFolder/subFolder1/"
And the downloaded zip file contains a file named "crowdedFolder/subFolder1/test.txt-0" with the contents of "/crowdedFolder/subFolder1/test.txt-0" from "user0" data
diff --git a/build/integration/files_features/external-storage.feature b/build/integration/files_features/external-storage.feature
new file mode 100644
index 00000000000..77abeb6c5a4
--- /dev/null
+++ b/build/integration/files_features/external-storage.feature
@@ -0,0 +1,127 @@
+# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+Feature: external-storage
+ Background:
+ Given using api version "1"
+ Given using old dav path
+
+ @local_storage
+ Scenario: Share by link a file inside a local external storage
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And user "user0" created a folder "/local_storage/foo"
+ And User "user0" moved file "/textfile0.txt" to "/local_storage/foo/textfile0.txt"
+ And folder "/local_storage/foo" of user "user0" is shared with user "user1"
+ And As an "user1"
+ And accepting last share
+ When creating a share with
+ | path | foo |
+ | shareType | 3 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | id | A_NUMBER |
+ | url | AN_URL |
+ | token | A_TOKEN |
+ | mimetype | httpd/unix-directory |
+
+ Scenario: Shares don't overwrite external storage
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And User "user0" moved file "/textfile0.txt" to "/local_storage/textfile0.txt"
+ And invoking occ with "files_external:create --user user0 test local null::null -c datadir=./build/integration/work/local_storage"
+ And invoking occ with "files:scan --path /user0/files/test"
+ And as "user0" the file "/local_storage/textfile0.txt" exists
+ And as "user0" the folder "/test" exists
+ And as "user0" the file "/test/textfile0.txt" exists
+ And As an "user1"
+ And user "user1" created a folder "/test"
+ And User "user1" moved file "/textfile0.txt" to "/test/textfile1.txt"
+ And folder "/test" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then as "user0" the file "/test/textfile1.txt" does not exist
+
+ Scenario: Move a file into storage works
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And user "user0" created a folder "/local_storage/foo1"
+ When User "user0" moved file "/textfile0.txt" to "/local_storage/foo1/textfile0.txt"
+ Then as "user1" the file "/local_storage/foo1/textfile0.txt" exists
+ And as "user0" the file "/local_storage/foo1/textfile0.txt" exists
+
+ Scenario: Move a file out of the storage works
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And user "user0" created a folder "/local_storage/foo2"
+ And User "user0" moved file "/textfile0.txt" to "/local_storage/foo2/textfile0.txt"
+ When User "user1" moved file "/local_storage/foo2/textfile0.txt" to "/local.txt"
+ Then as "user1" the file "/local_storage/foo2/textfile0.txt" does not exist
+ And as "user0" the file "/local_storage/foo2/textfile0.txt" does not exist
+ And as "user1" the file "/local.txt" exists
+
+
+
+ Scenario: Save an external storage with password provided by user
+ Given Logging in using web as "admin"
+ And logged in user creates external global storage
+ | mountPoint | "ExternalStorageTest" |
+ | backend | "owncloud" |
+ | authMechanism | "password::userprovided" |
+ | backendOptions | {"host":"http://localhost:8080","secure":false} |
+ And fields of last external storage match with
+ | status | 2 |
+ When logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"admin"} |
+ Then fields of last external storage match with
+ | status | 0 |
+
+ Scenario: Save an external storage again with an unmodified password provided by user
+ Given Logging in using web as "admin"
+ And logged in user creates external global storage
+ | mountPoint | "ExternalStorageTest" |
+ | backend | "owncloud" |
+ | authMechanism | "password::userprovided" |
+ | backendOptions | {"host":"http://localhost:8080","secure":false} |
+ And fields of last external storage match with
+ | status | 2 |
+ And logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"admin"} |
+ When logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"__unmodified__"} |
+ Then fields of last external storage match with
+ | status | 0 |
+
+ Scenario: Save an external storage with global credentials provided by user
+ Given Logging in using web as "admin"
+ And logged in user creates external global storage
+ | mountPoint | "ExternalStorageTest" |
+ | backend | "owncloud" |
+ | authMechanism | "password::global::user" |
+ | backendOptions | {"host":"http://localhost:8080","secure":false} |
+ And fields of last external storage match with
+ | status | 2 |
+ When logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"admin"} |
+ Then fields of last external storage match with
+ | status | 0 |
+
+ Scenario: Save an external storage again with unmodified global credentials provided by user
+ Given Logging in using web as "admin"
+ And logged in user creates external global storage
+ | mountPoint | "ExternalStorageTest" |
+ | backend | "owncloud" |
+ | authMechanism | "password::global::user" |
+ | backendOptions | {"host":"http://localhost:8080","secure":false} |
+ And fields of last external storage match with
+ | status | 2 |
+ And logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"admin"} |
+ When logged in user updates last external userglobal storage
+ | backendOptions | {"user":"admin","password":"__unmodified__"} |
+ Then fields of last external storage match with
+ | status | 0 |
diff --git a/build/integration/features/favorites.feature b/build/integration/files_features/favorites.feature
index 0439ada9d60..8e510799d20 100644
--- a/build/integration/features/favorites.feature
+++ b/build/integration/files_features/favorites.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: favorite
Background:
Given using api version "1"
diff --git a/build/integration/files_features/metadata.feature b/build/integration/files_features/metadata.feature
new file mode 100644
index 00000000000..553a7b62306
--- /dev/null
+++ b/build/integration/files_features/metadata.feature
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+Feature: metadata
+
+ Scenario: Setting metadata works
+ Given user "user0" exists
+ When User "user0" uploads file with content "AAA" to "/test.txt"
+ And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt"
+ Then User "user0" should see the prop "metadata-files-live-photo" equal to "metadata-value" for file "/test.txt"
+
+ Scenario: Deleting metadata works
+ Given user "user0" exists
+ When User "user0" uploads file with content "AAA" to "/test.txt"
+ And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt"
+ And User "user0" deletes the "metadata-files-live-photo" prop on "/test.txt"
+ Then User "user0" should not see the prop "metadata-files-live-photo" for file "/test.txt"
diff --git a/build/integration/features/tags.feature b/build/integration/files_features/tags.feature
index 495008ffdd2..fef8068cbc8 100644
--- a/build/integration/features/tags.feature
+++ b/build/integration/files_features/tags.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: tags
Scenario: Creating a normal tag as regular user should work
diff --git a/build/integration/features/transfer-ownership.feature b/build/integration/files_features/transfer-ownership.feature
index b18453cf3ec..6f7a7944166 100644
--- a/build/integration/features/transfer-ownership.feature
+++ b/build/integration/files_features/transfer-ownership.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: transfer-ownership
Scenario: transferring ownership of a file
@@ -39,7 +42,7 @@ Feature: transfer-ownership
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
- And transfer folder name contains "transferred from user0 -risky- ヂspḷay -na|-|e- on"
+ And transfer folder name contains "Transferred from user0 -risky- ヂspḷay -na|-|e- on"
And using old dav path
And as "user0" the folder "/test" does not exist
And using received transfer folder of "user1" as dav path
@@ -181,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
@@ -207,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
@@ -239,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
@@ -250,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
@@ -261,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
@@ -345,7 +347,7 @@ Feature: transfer-ownership
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
- And transfer folder name contains "transferred from user0 -risky- ヂspḷay -na|-|e- on"
+ And transfer folder name contains "Transferred from user0 -risky- ヂspḷay -na|-|e- on"
And using old dav path
And as "user0" the folder "/test" does not exist
And using received transfer folder of "user1" as dav path
@@ -511,25 +513,34 @@ Feature: transfer-ownership
And user "user2" accepts last share
When transferring ownership of path "test" from "user0" to "user1"
Then the command failed with exit code 1
- And the command output contains the text "Could not transfer files."
+ 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
+ Scenario: transferring ownership transfers received shares into subdir when requested
Given user "user0" exists
And user "user1" exists
And user "user2" exists
- And User "user2" created a folder "/test"
+ And User "user2" created a folder "/transfer-share"
+ And User "user2" created a folder "/do-not-transfer"
And User "user0" created a folder "/sub"
- And folder "/test" of user "user2" is shared with user "user0" with permissions 31
+ And folder "/transfer-share" of user "user2" is shared with user "user0" with permissions 31
+ And user "user0" accepts last share
+ 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
- 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 as "user1" the folder "/do-not-transfer" does not exist
+ And as "user1" the folder "/sub/do-not-transfer" does not exist
+ And as "user1" the folder "/sub/transfer-share" exists
+ And using old dav path
+ And as "user1" the folder "/transfer-share" does not exist
+ And as "user1" the folder "/do-not-transfer" does not exist
And using old dav path
And as "user0" the folder "/sub" does not exist
+ And as "user0" the folder "/do-not-transfer" exists
And Getting info of last share
And the OCS status code should be "404"
diff --git a/build/integration/features/trashbin.feature b/build/integration/files_features/trashbin.feature
index 3a9c29f7cb8..fd84e396ba3 100644
--- a/build/integration/features/trashbin.feature
+++ b/build/integration/files_features/trashbin.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2017 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: trashbin
Background:
Given using api version "1"
diff --git a/build/integration/files_features/windows_compatibility.feature b/build/integration/files_features/windows_compatibility.feature
new file mode 100644
index 00000000000..feaaca1ed3a
--- /dev/null
+++ b/build/integration/files_features/windows_compatibility.feature
@@ -0,0 +1,68 @@
+# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+Feature: Windows compatible filenames
+ Background:
+ Given using api version "1"
+ And using new dav path
+ And As an "admin"
+
+ Scenario: prevent upload files with invalid name
+ Given As an "admin"
+ And user "user0" exists
+ And invoking occ with "files:windows-compatible-filenames --enable"
+ Given User "user0" created a folder "/com1"
+ Then as "user0" the file "/com1" does not exist
+
+ Scenario: renaming a folder with invalid name
+ Given As an "admin"
+ When invoking occ with "files:windows-compatible-filenames --disable"
+ And user "user0" exists
+ Given User "user0" created a folder "/aux"
+ When invoking occ with "files:windows-compatible-filenames --enable"
+ And invoking occ with "files:sanitize-filenames user0"
+ Then as "user0" the file "/aux" does not exist
+ And as "user0" the file "/aux (renamed)" exists
+
+ Scenario: renaming a file with invalid base name
+ Given As an "admin"
+ When invoking occ with "files:windows-compatible-filenames --disable"
+ And user "user0" exists
+ When User "user0" uploads file with content "hello" to "/com0.txt"
+ And invoking occ with "files:windows-compatible-filenames --enable"
+ And invoking occ with "files:sanitize-filenames user0"
+ Then as "user0" the file "/com0.txt" does not exist
+ And as "user0" the file "/com0 (renamed).txt" exists
+
+ Scenario: renaming a file with invalid extension
+ Given As an "admin"
+ When invoking occ with "files:windows-compatible-filenames --disable"
+ And user "user0" exists
+ When User "user0" uploads file with content "hello" to "/foo.txt."
+ And as "user0" the file "/foo.txt." exists
+ And invoking occ with "files:windows-compatible-filenames --enable"
+ And invoking occ with "files:sanitize-filenames user0"
+ Then as "user0" the file "/foo.txt." does not exist
+ And as "user0" the file "/foo.txt" exists
+
+ Scenario: renaming a file with invalid character
+ Given As an "admin"
+ When invoking occ with "files:windows-compatible-filenames --disable"
+ And user "user0" exists
+ When User "user0" uploads file with content "hello" to "/2*2=4.txt"
+ And as "user0" the file "/2*2=4.txt" exists
+ 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
+
+ Scenario: renaming a file with invalid character and replacement setup
+ Given As an "admin"
+ When invoking occ with "files:windows-compatible-filenames --disable"
+ And user "user0" exists
+ When User "user0" uploads file with content "hello" to "/2*3=6.txt"
+ And as "user0" the file "/2*3=6.txt" exists
+ And invoking occ with "files:windows-compatible-filenames --enable"
+ And invoking occ with "files:sanitize-filenames --char-replacement + user0"
+ Then as "user0" the file "/2*3=6.txt" does not exist
+ And as "user0" the file "/2+3=6.txt" exists
diff --git a/build/integration/filesdrop_features/filesdrop.feature b/build/integration/filesdrop_features/filesdrop.feature
index 4a8759e241a..7618a31a1d0 100644
--- a/build/integration/filesdrop_features/filesdrop.feature
+++ b/build/integration/filesdrop_features/filesdrop.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: FilesDrop
Scenario: Put file via files drop
@@ -31,7 +33,7 @@ Feature: FilesDrop
And Downloading file "/drop/a (2).txt"
Then Downloaded content should be "def"
- Scenario: Files drop ignores directory
+ Scenario: Files drop forbid directory without a nickname
Given user "user0" exists
And As an "user0"
And user "user0" created a folder "/drop"
@@ -42,10 +44,9 @@ Feature: FilesDrop
And Updating last share with
| permissions | 4 |
When Dropping file "/folder/a.txt" with "abc"
- And Downloading file "/drop/a.txt"
- Then Downloaded content should be "abc"
+ Then the HTTP status code should be "400"
- Scenario: Files drop forbis MKCOL
+ Scenario: Files drop forbid MKCOL without a nickname
Given user "user0" exists
And As an "user0"
And user "user0" created a folder "/drop"
@@ -56,4 +57,181 @@ Feature: FilesDrop
And Updating last share with
| permissions | 4 |
When Creating folder "folder" in drop
+ Then the HTTP status code should be "400"
+
+ Scenario: Files drop allows MKCOL with a nickname
+ 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 | 3 |
+ | publicUpload | true |
+ And Updating last share with
+ | permissions | 4 |
+ When Creating folder "folder" in drop as "nickname"
+ Then the HTTP status code should be "201"
+
+ Scenario: Files drop forbid subfolder creation without a nickname
+ 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 | 3 |
+ | publicUpload | true |
+ And Updating last share with
+ | permissions | 4 |
+ When dropping file "/folder/a.txt" with "abc"
+ Then the HTTP status code should be "400"
+
+ Scenario: Files request drop
+ 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"
+ 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"
+ 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"
+ And Dropping file "/folder/a.txt" with "def" as "Mallory"
+ # Ensure folder structure and that we only checked
+ # for files duplicates, but merged the existing folders
+ Then as "user0" the folder "/drop/Mallory" exists
+ Then as "user0" the folder "/drop/Mallory/folder" exists
+ Then as "user0" the folder "/drop/Mallory (2)" does not exist
+ Then as "user0" the folder "/drop/Mallory/folder (2)" does not exist
+ Then as "user0" the file "/drop/Mallory/folder/a.txt" exists
+ Then as "user0" the file "/drop/Mallory/folder/a (2).txt" exists
+ And Downloading file "/drop/Mallory/folder/a.txt"
+ Then Downloaded content should be "abc"
+ And Downloading file "/drop/Mallory/folder/a (2).txt"
+ Then Downloaded content should be "def"
+
+ Scenario: Files drop prevents GET
+ 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 |
+ | shareWith | |
+ | attributes | [{"scope":"fileRequest","key":"enabled","value":true}] |
+ When Dropping file "/folder/a.txt" with "abc" as "Mallory"
+ When as "user0" the file "/drop/Mallory/folder/a.txt" exists
+ And Downloading public folder "Mallory"
Then the HTTP status code should be "405"
+ And Downloading public folder "Mallory/folder"
+ Then the HTTP status code should be "405"
+ And Downloading public file "Mallory/folder/a.txt"
+ Then the HTTP status code should be "405"
+
+ Scenario: Files drop requires nickname if file request is enabled
+ 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"
+ 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/ldap_features/ldap-ocs.feature b/build/integration/ldap_features/ldap-ocs.feature
index a9ad0478702..d6d79ad9c58 100644
--- a/build/integration/ldap_features/ldap-ocs.feature
+++ b/build/integration/ldap_features/ldap-ocs.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: LDAP
Background:
Given using api version "2"
diff --git a/build/integration/ldap_features/ldap-openldap.feature b/build/integration/ldap_features/ldap-openldap.feature
index 570cf287a2e..14fa3b63968 100644
--- a/build/integration/ldap_features/ldap-openldap.feature
+++ b/build/integration/ldap_features/ldap-openldap.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: LDAP
Background:
Given using api version "2"
diff --git a/build/integration/ldap_features/openldap-uid-username.feature b/build/integration/openldap_features/openldap-uid-username.feature
index 6793273e8c7..bee4098972b 100644
--- a/build/integration/ldap_features/openldap-uid-username.feature
+++ b/build/integration/openldap_features/openldap-uid-username.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: LDAP
Background:
Given using api version "2"
@@ -149,6 +151,7 @@ Feature: LDAP
| ldapAttributesForUserSearch | employeeNumber |
| useMemberOfToDetectMembership | 1 |
And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes"
+ And invoking occ with "ldap:check-group cn=Orcharding,ou=OtherGroups,dc=nextcloud,dc=ci --update"
And As an "alice"
When getting sharees for
# "5" is part of the employee number of some LDAP records
@@ -160,4 +163,3 @@ Feature: LDAP
And "users" sharees returned are
| Elisa | 0 | elisa |
And "exact groups" sharees returned is empty
-
diff --git a/build/integration/ldap_features/openldap-numerical-id.feature b/build/integration/openldap_numerical_features/openldap-numerical-id.feature
index 4ea63823295..f4d2b1d77d2 100644
--- a/build/integration/ldap_features/openldap-numerical-id.feature
+++ b/build/integration/openldap_numerical_features/openldap-numerical-id.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: LDAP
Background:
Given using api version "2"
@@ -66,3 +68,31 @@ Scenario: Test LDAP group membership with intermediate groups not matching filte
| 50194 | 1 |
| 59376 | 1 |
| 59463 | 1 |
+
+Scenario: Test LDAP admin group mapping, empowered user
+ Given modify LDAP configuration
+ | ldapBaseGroups | ou=NumericGroups,dc=nextcloud,dc=ci |
+ | ldapGroupFilter | (objectclass=groupOfNames) |
+ | ldapGroupMemberAssocAttr | member |
+ | ldapAdminGroup | 3001 |
+ | useMemberOfToDetectMembership | 1 |
+ And cookies are reset
+ # alice, part of the promoted group
+ And Logging in using web as "92379"
+ And sending "GET" to "/cloud/groups"
+ And sending "GET" to "/cloud/groups/2000/users"
+ And Sending a "GET" to "/index.php/settings/admin/overview" with requesttoken
+ Then the HTTP status code should be "200"
+
+Scenario: Test LDAP admin group mapping, regular user (no access)
+ Given modify LDAP configuration
+ | ldapBaseGroups | ou=NumericGroups,dc=nextcloud,dc=ci |
+ | ldapGroupFilter | (objectclass=groupOfNames) |
+ | ldapGroupMemberAssocAttr | member |
+ | ldapAdminGroup | 3001 |
+ | useMemberOfToDetectMembership | 1 |
+ And cookies are reset
+ # gustaf, not part of the promoted group
+ And Logging in using web as "59376"
+ And Sending a "GET" to "/index.php/settings/admin/overview" with requesttoken
+ Then the HTTP status code should be "403"
diff --git a/build/integration/features/ratelimiting.feature b/build/integration/ratelimiting_features/ratelimiting.feature
index bd8b2e30a73..43cfddec85d 100644
--- a/build/integration/features/ratelimiting.feature
+++ b/build/integration/ratelimiting_features/ratelimiting.feature
@@ -1,9 +1,12 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+@RateLimiting
Feature: ratelimiting
-
+
Background:
Given user "user0" exists
Given As an "admin"
- Given app "testing" is enabled
+ Given invoking occ with "app:enable --force testing"
Scenario: Accessing a page with only an AnonRateThrottle as user
Given user "user0" exists
diff --git a/build/integration/remoteapi_features/remote.feature b/build/integration/remoteapi_features/remote.feature
index 72daf8226cd..81e10027aae 100644
--- a/build/integration/remoteapi_features/remote.feature
+++ b/build/integration/remoteapi_features/remote.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: remote
Scenario: Get status of remote server
@@ -34,4 +36,5 @@ Feature: remote
Given using remote server "REMOTE"
And user "user0" exists
And using credentials "user0", "invalid"
- Then the capability "theming.name" is "Nextcloud"
+ Then the capability "theming.name" is "OC\ForbiddenException"
+ Then the request should throw a "OC\ForbiddenException"
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-docker.sh b/build/integration/run-docker.sh
index 7f8408d2126..e1ec797fcf0 100755
--- a/build/integration/run-docker.sh
+++ b/build/integration/run-docker.sh
@@ -1,22 +1,7 @@
#!/usr/bin/env bash
-# @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
-# @copyright Copyright (c) 2018, 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: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
# Helper script to run the integration tests on a fresh Nextcloud server through
# Docker.
@@ -140,7 +125,9 @@ function prepareDocker() {
echo "Starting the Nextcloud container"
# When using "nextcloudci/phpX.Y" images the container exits immediately if
# no command is given, so a Bash session is created to prevent that.
- docker run --detach --name=$NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_CONTAINER_NETWORK_OPTIONS --interactive --tty $NEXTCLOUD_LOCAL_IMAGE bash
+ docker run \
+ --volume composer_cache:/root/.composer \
+ --detach --name=$NEXTCLOUD_LOCAL_CONTAINER $NEXTCLOUD_LOCAL_CONTAINER_NETWORK_OPTIONS --interactive --tty $NEXTCLOUD_LOCAL_IMAGE bash
# Use the $TMPDIR or, if not set, fall back to /tmp.
NEXTCLOUD_LOCAL_TAR="$($MKTEMP --tmpdir="${TMPDIR:-/tmp}" --suffix=.tar nextcloud-local-XXXXXXXXXX)"
@@ -152,6 +139,7 @@ function prepareDocker() {
tar --create --file="$NEXTCLOUD_LOCAL_TAR" \
--exclude=".git" \
--exclude="./config/config.php" \
+ --exclude="./config/*.config.php" \
--exclude="./data" \
--exclude="./data-autotest" \
--exclude="./tests" \
@@ -211,8 +199,9 @@ trap cleanUp EXIT
cd "$(dirname $0)"
# "--image XXX" option can be provided to set the Docker image to use to run
-# the integration tests (one of the "nextcloudci/phpX.Y:phpX.Y-Z" images).
-NEXTCLOUD_LOCAL_IMAGE="nextcloudci/php7.3:php7.3-5"
+# the integration tests (one of the "nextcloudci/phpX.Y:phpX.Y-Z" or
+# "ghcr.io/nextcloud/continuous-integration-integration-phpX.Y:latest" images).
+NEXTCLOUD_LOCAL_IMAGE="ghcr.io/nextcloud/continuous-integration-integration-php8.2:latest"
if [ "$1" = "--image" ]; then
NEXTCLOUD_LOCAL_IMAGE=$2
@@ -238,9 +227,9 @@ fi
# "--database-image XXX" option can be provided to set the Docker image to use
# for the database container (ignored when using "sqlite").
if [ "$DATABASE" = "mysql" ]; then
- DATABASE_IMAGE="mysql:5.7"
+ DATABASE_IMAGE="mysql:8.4"
elif [ "$DATABASE" = "pgsql" ]; then
- DATABASE_IMAGE="postgres:10"
+ DATABASE_IMAGE="postgres:15"
fi
if [ "$1" = "--database-image" ]; then
DATABASE_IMAGE=$2
diff --git a/build/integration/run.sh b/build/integration/run.sh
index 4808ab58ef5..30dd0646b10 100755
--- a/build/integration/run.sh
+++ b/build/integration/run.sh
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
-
+#
+# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+#
OC_PATH=../../
OCC=${OC_PATH}occ
TAGS=""
@@ -14,10 +18,18 @@ 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
+ $OCC config:system:set ratelimit.protection.enabled --value false --type bool
# Allow local remote urls otherwise we can not share
$OCC config:system:set allow_local_remote_servers --value true --type bool
+ # Allow self signed certificates
+ $OCC config:system:set sharing.federation.allowSelfSignedCertificates --value true --type bool
+ # Allow creating users with dummy passwords
+ $OCC app:disable password_policy
else
if [ "$SCENARIO_TO_RUN" != "setup_features/setup.feature" ]; then
echo "Nextcloud instance needs to be installed" >&2
@@ -34,15 +46,30 @@ if [ -z "$EXECUTOR_NUMBER" ]; then
fi
PORT=$((8080 + $EXECUTOR_NUMBER))
echo $PORT
-php -S localhost:$PORT -t ../.. &
+export PORT
+
+echo "" > "${NC_DATADIR}/nextcloud.log"
+echo "" > phpserver.log
+
+PHP_CLI_SERVER_WORKERS=2 php -S localhost:$PORT -t ../.. &> phpserver.log &
PHPPID=$!
echo $PHPPID
+# Output filtered php server logs
+tail -f phpserver.log | grep --line-buffered -v -E ":[0-9]+ Accepted$" | grep --line-buffered -v -E ":[0-9]+ Closing$" &
+LOGPID=$!
+echo $LOGPID
+
+function cleanup() {
+ kill $PHPPID
+ kill $LOGPID
+}
+trap cleanup EXIT
+
+# The federated server is started and stopped by the tests themselves
PORT_FED=$((8180 + $EXECUTOR_NUMBER))
echo $PORT_FED
-php -S localhost:$PORT_FED -t ../.. &
-PHPPID_FED=$!
-echo $PHPPID_FED
+export PORT_FED
export TEST_SERVER_URL="http://localhost:$PORT/ocs/"
export TEST_SERVER_FED_URL="http://localhost:$PORT_FED/ocs/"
@@ -61,12 +88,9 @@ if [ "$INSTALLED" == "true" ]; then
fi
-vendor/bin/behat --strict -f junit -f pretty $TAGS $SCENARIO_TO_RUN
+vendor/bin/behat --strict --colors -f junit -f pretty $TAGS $SCENARIO_TO_RUN
RESULT=$?
-kill $PHPPID
-kill $PHPPID_FED
-
if [ "$INSTALLED" == "true" ]; then
$OCC files_external:delete -y $ID_STORAGE
diff --git a/build/integration/setup_features/setup.feature b/build/integration/setup_features/setup.feature
index e1d9a73187e..5fb2ff16330 100644
--- a/build/integration/setup_features/setup.feature
+++ b/build/integration/setup_features/setup.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: setup
Scenario: setup page is shown properly
diff --git a/build/integration/sharees_features/sharees.feature b/build/integration/sharees_features/sharees.feature
index 1d770b96b72..8b0a0e5133e 100644
--- a/build/integration/sharees_features/sharees.feature
+++ b/build/integration/sharees_features/sharees.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: sharees
Background:
Given using api version "1"
diff --git a/build/integration/sharees_features/sharees_provisioningapiv2.feature b/build/integration/sharees_features/sharees_provisioningapiv2.feature
index 6f2b8df8e0e..7bd8ecbdbb7 100644
--- a/build/integration/sharees_features/sharees_provisioningapiv2.feature
+++ b/build/integration/sharees_features/sharees_provisioningapiv2.feature
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+# SPDX-FileCopyrightText: 2017 ownCloud, Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
Feature: sharees_provisioningapiv2
Background:
Given using api version "2"
diff --git a/build/integration/sharing_features/sharing-activity.feature b/build/integration/sharing_features/sharing-activity.feature
new file mode 100644
index 00000000000..016b376488b
--- /dev/null
+++ b/build/integration/sharing_features/sharing-activity.feature
@@ -0,0 +1,46 @@
+# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: sharing
+ Background:
+ Given using api version "1"
+ Given using new dav path
+ Given invoking occ with "app:enable --force activity"
+ Given the command was successful
+ Given user "user0" exists
+ And Logging in using web as "user0"
+ And Sending a "POST" to "/apps/activity/settings" with requesttoken
+ | public_links_notification | 1 |
+ | public_links_upload_notification | 1 |
+ | notify_setting_batchtime | 0 |
+ | activity_digest | 0 |
+
+ Scenario: Creating a new mail share and check activity
+ Given dummy mail server is listening
+ And As an "user0"
+ When creating a share with
+ | path | welcome.txt |
+ | shareType | 4 |
+ | shareWith | dumy@test.com |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And last share can be downloaded
+ Then last activity should be
+ | app | files_sharing |
+ | type | public_links |
+ | object_type | files |
+ | object_name | /welcome.txt |
+
+ Scenario: Creating a new public share and check activity
+ Given user "user0" exists
+ And As an "user0"
+ When creating a share with
+ | path | welcome.txt |
+ | shareType | 3 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And last link share can be downloaded
+ Then last activity should be
+ | app | files_sharing |
+ | type | public_links |
+ | object_type | files |
+ | object_name | /welcome.txt |
diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature
index 8fc06fbddeb..a6e4c67165a 100644
--- a/build/integration/sharing_features/sharing-v1-part2.feature
+++ b/build/integration/sharing_features/sharing-v1-part2.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: sharing
Background:
Given using api version "1"
@@ -541,6 +543,29 @@ Feature: sharing
And the HTTP status code should be "200"
And last share_id is included in the answer
+ Scenario: Group shares are deleted when the group is deleted
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And user "user0" belongs to group "group0"
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ When sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And last share_id is included in the answer
+ When group "group0" does not exist
+ Then sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And last share_id is not included in the answer
+ When group "group0" exists
+ Then sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And last share_id is not included in the answer
+
Scenario: User is not allowed to reshare file
As an "admin"
Given user "user0" exists
@@ -699,6 +724,79 @@ Feature: sharing
Then the OCS status code should be "404"
And the HTTP status code should be "200"
+ Scenario: download restrictions can not be dropped
+ As an "admin"
+ Given user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And User "user0" uploads file with content "foo" to "/tmp.txt"
+ And As an "user0"
+ And creating a share with
+ | path | /tmp.txt |
+ | shareType | 0 |
+ | shareWith | user1 |
+ | permissions | 17 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+ And As an "user1"
+ And accepting last share
+ When Getting info of last share
+ Then Share fields of last share match with
+ | uid_owner | user0 |
+ | uid_file_owner | user0 |
+ | permissions | 17 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+ When creating a share with
+ | path | /tmp.txt |
+ | shareType | 0 |
+ | shareWith | user2 |
+ | permissions | 1 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When As an "user2"
+ And accepting last share
+ And Getting info of last share
+ Then Share fields of last share match with
+ | share_type | 0 |
+ | permissions | 1 |
+ | uid_owner | user1 |
+ | uid_file_owner | user0 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+
+ Scenario: download restrictions can not be dropped when re-sharing even on link shares
+ As an "admin"
+ Given user "user0" exists
+ And user "user1" exists
+ And User "user0" uploads file with content "foo" to "/tmp.txt"
+ And As an "user0"
+ And creating a share with
+ | path | /tmp.txt |
+ | shareType | 0 |
+ | shareWith | user1 |
+ | permissions | 17 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+ And As an "user1"
+ And accepting last share
+ When Getting info of last share
+ Then Share fields of last share match with
+ | uid_owner | user0 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+ When creating a share with
+ | path | /tmp.txt |
+ | shareType | 3 |
+ | permissions | 1 |
+ And Getting info of last share
+ And Updating last share with
+ | hideDownload | false |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ When Getting info of last share
+ Then Share fields of last share match with
+ | share_type | 3 |
+ | uid_owner | user1 |
+ | uid_file_owner | user0 |
+ | hide_download | 1 |
+ | attributes | [{"scope":"permissions","key":"download","value":false}] |
+
Scenario: User is not allowed to reshare file with additional delete permissions
As an "admin"
Given user "user0" exists
@@ -919,7 +1017,7 @@ Feature: sharing
And As an "user1"
When Deleting last share
Then the OCS status code should be "403"
- And the HTTP status code should be "401"
+ And the HTTP status code should be "200"
Scenario: Keep usergroup shares (#22143)
Given As an "admin"
@@ -1167,4 +1265,72 @@ 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 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"
+ And file "document.odt" of user "user0" is shared with user "user1" view-only
+ And user "user1" accepts last share
+ 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 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"
+ And User "user0" moves file "/textfile0.txt" to "/sharedviewonly/document.odt"
+ And folder "sharedviewonly" of user "user0" is shared with user "user1" view-only
+ And user "user1" accepts last share
+ 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 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"
+ And file "document.odt" of user "user0" is shared with user "user1" view-only
+ 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"
+ And User "user0" moves file "/textfile0.txt" to "/sharedviewonly/document.odt"
+ And folder "sharedviewonly" of user "user0" is shared with user "user1" view-only
+ 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/sharing_features/sharing-v1-part3.feature b/build/integration/sharing_features/sharing-v1-part3.feature
index 5094111d025..3c2945e3ad4 100644
--- a/build/integration/sharing_features/sharing-v1-part3.feature
+++ b/build/integration/sharing_features/sharing-v1-part3.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 20198 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: sharing
Background:
Given using api version "1"
@@ -403,7 +405,7 @@ Feature: sharing
And Updating last share with
| permissions | 19 |
Then the OCS status code should be "403"
- And the HTTP status code should be "401"
+ And the HTTP status code should be "200"
Scenario: do not allow to increase permissions on non received share with user with resharing rights
Given As an "admin"
@@ -427,7 +429,7 @@ Feature: sharing
And Updating last share with
| permissions | 19 |
Then the OCS status code should be "403"
- And the HTTP status code should be "401"
+ And the HTTP status code should be "200"
Scenario: do not allow to increase link share permissions on reshare
Given As an "admin"
@@ -514,6 +516,68 @@ Feature: sharing
Then as "user1" the file "/shared/shared_file.txt" exists
And as "user0" the file "/shared/shared_file.txt" exists
+ Scenario: receiving shares into a configured share_folder
+ Given As an "admin"
+ And invoking occ with "config:system:set share_folder --value received_shares"
+ And user "user0" exists
+ And user "user1" exists
+ And user "user0" created a folder "/shared_folder"
+ And User "user0" moved file "/textfile0.txt" to "/shared_file.txt"
+ When folder "/shared_folder" of user "user0" is shared with user "user1"
+ And user "user1" accepts last share
+ Then as "user1" the file "/received_shares/shared_folder" exists
+ When file "/shared_file.txt" of user "user0" is shared with user "user1"
+ And user "user1" accepts last share
+ Then as "user1" the file "/received_shares/shared_file.txt" exists
+
+ Scenario: Owner of subshares is adjusted after moving into received share
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And user "user0" created a folder "/shared"
+ And folder "/shared" of user "user0" is shared with user "user1"
+ And user "user1" accepts last share
+ And user "user1" created a folder "/movein"
+ And user "user1" created a folder "/movein/subshare"
+ When As an "user1"
+ And folder "/movein" of user "user1" is shared with user "user2"
+ And save last share id
+ Then Getting info of last share
+ And Share fields of last share match with
+ | uid_file_owner | user1 |
+ | share_with | user2 |
+ When User "user1" moved file "/movein" to "/shared/movein"
+ Then As an "user0"
+ And Getting info of last share
+ And Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_with | user2 |
+
+ Scenario: Owner of subshares is adjusted after moving out of received share
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And user "user2" exists
+ And user "user0" created a folder "/shared"
+ And user "user0" created a folder "/shared/moveout"
+ And user "user0" created a folder "/shared/moveout/subshare"
+ And folder "/shared" of user "user0" is shared with user "user1"
+ And user "user1" accepts last share
+ And As an "user1"
+ And folder "/shared/moveout/subshare" of user "user1" is shared with user "user2"
+ And save last share id
+ When As an "user1"
+ Then Getting info of last share
+ And Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_with | user2 |
+ When User "user1" moved file "/shared/moveout" to "/moveout"
+ Then Getting info of last share
+ And Share fields of last share match with
+ | uid_file_owner | user1 |
+ | share_with | user2 |
+
Scenario: Link shares inside of group shares keep their original data when the root share is updated
Given As an "admin"
And user "user0" exists
diff --git a/build/integration/sharing_features/sharing-v1-part4.feature b/build/integration/sharing_features/sharing-v1-part4.feature
new file mode 100644
index 00000000000..d138f0a1769
--- /dev/null
+++ b/build/integration/sharing_features/sharing-v1-part4.feature
@@ -0,0 +1,184 @@
+# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+Feature: sharing
+ Background:
+ Given using api version "1"
+ Given using new dav path
+
+# See sharing-v1-part3.feature
+
+Scenario: Creating a new share of a file shows size and mtime
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And parameter "shareapi_default_permissions" of app "core" is set to "7"
+ When creating a share with
+ | path | welcome.txt |
+ | shareWith | user1 |
+ | shareType | 0 |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Getting info of last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | item_size | A_NUMBER |
+ | item_mtime | A_NUMBER |
+
+Scenario: Creating a new share of a file you own shows the file permissions
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And parameter "shareapi_default_permissions" of app "core" is set to "7"
+ When creating a share with
+ | path | welcome.txt |
+ | shareWith | user1 |
+ | shareType | 0 |
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Getting info of last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | item_permissions | 27 |
+
+Scenario: Receiving a share of a file gives no create permission
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And parameter "shareapi_default_permissions" of app "core" is set to "31"
+ And file "welcome.txt" of user "user0" is shared with user "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And share 0 is returned with
+ | path | /welcome.txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+ When As an "user1"
+ And user "user1" accepts last share
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | path | /welcome (2).txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+
+Scenario: Receiving a share of a folder gives create permission
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And parameter "shareapi_default_permissions" of app "core" is set to "31"
+ And file "PARENT/CHILD" of user "user0" is shared with user "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And share 0 is returned with
+ | path | /PARENT/CHILD |
+ | permissions | 31 |
+ | item_permissions | 31 |
+ When As an "user1"
+ And user "user1" accepts last share
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | path | /CHILD |
+ | permissions | 31 |
+ | item_permissions | 31 |
+
+# User can remove itself from a share
+Scenario: Receiving a share of a file without delete permission gives delete permission anyway
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And parameter "shareapi_default_permissions" of app "core" is set to "23"
+ And file "welcome.txt" of user "user0" is shared with user "user1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And share 0 is returned with
+ | path | /welcome.txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+ When As an "user1"
+ And user "user1" accepts last share
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | path | /welcome (2).txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+
+Scenario: Receiving a share of a file without delete permission gives delete permission anyway
+ Given user "user0" exists
+ And user "user1" exists
+ And As an "user0"
+ And group "group1" exists
+ And user "user1" belongs to group "group1"
+ And parameter "shareapi_default_permissions" of app "core" is set to "23"
+ And file "welcome.txt" of user "user0" is shared with group "group1"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares"
+ And share 0 is returned with
+ | path | /welcome.txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+ When As an "user1"
+ And user "user1" accepts last share
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | path | /welcome (2).txt |
+ | permissions | 19 |
+ | item_permissions | 27 |
+
+# This is a regression test as in the past creating a file drop required creating with permissions=5
+# and then afterwards update the share to permissions=4
+Scenario: Directly create link share with CREATE only permissions (file drop)
+ Given user "user0" exists
+ And As an "user0"
+ And user "user0" created a folder "/TMP"
+ When creating a share with
+ | path | TMP |
+ | shareType | 3 |
+ | permissions | 4 |
+ And Getting info of last share
+ Then Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_type | 3 |
+ | permissions | 4 |
+
+Scenario: Directly create email share with CREATE only permissions (file drop)
+ Given user "user0" exists
+ And As an "user0"
+ And user "user0" created a folder "/TMP"
+ When creating a share with
+ | path | TMP |
+ | shareType | 4 |
+ | shareWith | j.doe@example.com |
+ | permissions | 4 |
+ And Getting info of last share
+ Then Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_type | 4 |
+ | permissions | 4 |
+
+# This ensures the legacy behavior of sharing v1 is kept
+Scenario: publicUpload overrides permissions
+ Given user "user0" exists
+ And As an "user0"
+ And parameter "outgoing_server2server_share_enabled" of app "files_sharing" is set to "no"
+ And user "user0" created a folder "/TMP"
+ When creating a share with
+ | path | TMP |
+ | shareType | 3 |
+ | permissions | 4 |
+ | publicUpload | true |
+ And Getting info of last share
+ Then Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_type | 3 |
+ | permissions | 15 |
+ When creating a share with
+ | path | TMP |
+ | shareType | 3 |
+ | permissions | 4 |
+ | publicUpload | false |
+ And Getting info of last share
+ Then Share fields of last share match with
+ | uid_file_owner | user0 |
+ | share_type | 3 |
+ | permissions | 1 |
diff --git a/build/integration/sharing_features/sharing-v1.feature b/build/integration/sharing_features/sharing-v1.feature
index 00a754d54d3..25f168db2e7 100644
--- a/build/integration/sharing_features/sharing-v1.feature
+++ b/build/integration/sharing_features/sharing-v1.feature
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: sharing
Background:
Given using api version "1"
@@ -54,7 +56,7 @@ Feature: sharing
| shareWith | a-room-token |
| shareType | 10 |
Then the OCS status code should be "403"
- And the HTTP status code should be "401"
+ And the HTTP status code should be "200"
Scenario: Creating a new mail share
Given dummy mail server is listening
@@ -84,7 +86,7 @@ Feature: sharing
Scenario: Creating a new mail share with password when password protection is enforced
Given dummy mail server is listening
And As an "admin"
- And parameter "enforcePasswordProtection" of app "sharebymail" is set to "yes"
+ And parameter "shareapi_enforce_links_password" of app "core" is set to "yes"
And user "user0" exists
And As an "user0"
When creating a share with
@@ -229,6 +231,62 @@ Feature: sharing
| url | AN_URL |
| mimetype | httpd/unix-directory |
+ Scenario: Creating a new share with expiration date empty, when default expiration is set
+ Given user "user0" exists
+ And user "user1" exists
+ And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes"
+ And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3"
+ And As an "user0"
+ When creating a share with
+ | path | welcome.txt |
+ | shareWith | user1 |
+ | shareType | 0 |
+ | expireDate | |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Getting info of last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | expiration ||
+
+ Scenario: Creating a new share with expiration date removed, when default expiration is set
+ Given user "user0" exists
+ And user "user1" exists
+ And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes"
+ And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3"
+ And As an "user0"
+ When creating a share with
+ | path | welcome.txt |
+ | shareWith | user1 |
+ | shareType | 0 |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Getting info of last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | expiration | +3 days |
+
+ Scenario: Creating a new share with expiration date null, when default expiration is set
+ Given user "user0" exists
+ And user "user1" exists
+ And parameter "shareapi_default_internal_expire_date" of app "core" is set to "yes"
+ And parameter "shareapi_internal_expire_after_n_days" of app "core" is set to "3"
+ And As an "user0"
+ When creating a share with
+ | path | welcome.txt |
+ | shareWith | user1 |
+ | shareType | 0 |
+ | expireDate | null |
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Getting info of last share
+ Then the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Share fields of last share match with
+ | expiration | +3 days |
+
Scenario: Creating a new public share, updating its password and getting its info
Given user "user0" exists
And As an "user0"
@@ -269,6 +327,7 @@ Feature: sharing
| shareType | 3 |
And Updating last share with
| permissions | 7 |
+ | publicUpload | true |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And Getting info of last share
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/integration/sharing_features/sharing-v1-video-verification.feature b/build/integration/videoverification_features/sharing-v1-video-verification.feature
index e57ec9a9f98..0bd4ed4b0f2 100644
--- a/build/integration/sharing_features/sharing-v1-video-verification.feature
+++ b/build/integration/videoverification_features/sharing-v1-video-verification.feature
@@ -1,9 +1,12 @@
@Talk
+# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
Feature: sharing
Background:
Given using api version "1"
Given using old dav path
- Given invoking occ with "app:enable spreed"
+ Given invoking occ with "app:enable --force spreed"
+ Given the command was successful
Scenario: Creating a link share with send password by Talk
Given user "user0" exists