--- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,91 +0,0 @@ -FROM ubuntu:noble - -ARG DEBIAN_FRONTEND=noninteractive - -# PHP -RUN apt-get update -y && \ - apt install -y apache2 vim software-properties-common sudo nano gnupg2 wget curl git \ - lsb-release ca-certificates apt-transport-https && \ - add-apt-repository ppa:ondrej/php -y && \ - apt-get update -y - -RUN apt-get install --no-install-recommends -y \ - php8.4 \ - php8.4-common \ - php8.4-gd \ - php8.4-zip \ - php8.4-curl \ - php8.4-xml \ - php8.4-xmlrpc \ - php8.4-mbstring \ - php8.4-sqlite \ - php8.4-xdebug \ - php8.4-pgsql \ - php8.4-intl \ - php8.4-imagick \ - php8.4-gmp \ - php8.4-apcu \ - php8.4-bcmath \ - php8.4-redis \ - php8.4-soap \ - php8.4-imap \ - php8.4-opcache \ - php8.4-cli \ - php8.4-dev \ - libmagickcore-6.q16-7-extra \ - lsof \ - make \ - unzip - -# Composer -RUN curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php && \ - curl -sS https://composer.github.io/installer.sig -o /tmp/composer-setup.sig && \ - php -r "if (hash_file('sha384', '/tmp/composer-setup.php') !== trim(file_get_contents('/tmp/composer-setup.sig'))) { echo 'Composer installation failed, invalid hash'; exit(1); }" && \ - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer && \ - rm /tmp/composer-setup.php /tmp/composer-setup.sig - -RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.4/cli/conf.d/20-xdebug.ini && \ - echo "xdebug.remote_autostart = 1" >> /etc/php/8.4/cli/conf.d/20-xdebug.ini && \ - echo "apc.enable_cli=1" >> /etc/php/8.4/cli/conf.d/20-apcu.ini - -# Autostart XDebug for apache -RUN { \ - echo "xdebug.mode=debug"; \ - echo "xdebug.start_with_request=yes"; \ -} >> /etc/php/8.4/apache2/conf.d/20-xdebug.ini - -# Increase PHP memory limit to 512mb -RUN sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/8.4/apache2/php.ini - -# Docker CLI only (for controlling host Docker via socket) -RUN install -m 0755 -d /etc/apt/keyrings && \ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \ - chmod a+r /etc/apt/keyrings/docker.asc && \ - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ - tee /etc/apt/sources.list.d/docker.list > /dev/null && \ - apt-get update -y && \ - apt-get install -y docker-ce-cli && \ - ln -s /var/run/docker-host.sock /var/run/docker.sock - -# Dedicated DevContainer user runs Apache -ENV APACHE_RUN_USER=devcontainer -ENV APACHE_RUN_GROUP=devcontainer -# Delete any existing user/group with UID/GID 1000 first -RUN (getent passwd 1000 && userdel -r $(getent passwd 1000 | cut -d: -f1)) || true && \ - (getent group 1000 && groupdel $(getent group 1000 | cut -d: -f1)) || true && \ - groupadd -g 1000 ${APACHE_RUN_GROUP} && \ - useradd -u 1000 -g 1000 -ms /bin/bash ${APACHE_RUN_USER} && \ - adduser ${APACHE_RUN_USER} sudo && \ - echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ - sed -ri "s/^export APACHE_RUN_USER=.*$/export APACHE_RUN_USER=${APACHE_RUN_USER}/" "/etc/apache2/envvars" && \ - sed -ri "s/^export APACHE_RUN_GROUP=.*$/export APACHE_RUN_GROUP=${APACHE_RUN_GROUP}/" "/etc/apache2/envvars" - -USER devcontainer - -# NVM -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash -RUN bash --login -i -c 'source /home/devcontainer/.bashrc && nvm install 22' - -WORKDIR /var/www/html --- a/.devcontainer/Dockerfile.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/.devcontainer/README.md +++ /dev/null @@ -1,76 +0,0 @@ - -# Nextcloud DevContainer - -## Usage - -Make sure you have the [VSCode DevContainer](https://code.visualstudio.com/docs/devcontainers/containers) extensions installed. If you open the project, VSCode will ask you if you want to open it inside of the DevContainer. If that's not the case, use F1→*Dev Containers: Open Folder in Container*. - -Alternatively open the project directly in [GitHub Codespaces](https://github.com/codespaces/new?hide_repo_select=true&ref=stable35&repo=60243197&skip_quickstart=true). - -That's already it. Everything else will be configured automatically by the Containers startup routine. - -## Credentials - -On first start the Container installs and configures Nextcloud with the following credentials: - -**Nextcloud Admin Login** - -Username: `admin`
-Password: `admin` - -**Postgres credentials** - -Username: `postgres`
-Password: `postgres`
-Database: `postgres` - -## Services - -The following services will be started: - -| Service | Local port | Description | -|---------|------------|-------------| -| Nextcloud (served via Apache) | `80` | The main application | -| Mailhog | `8025` | SMTP email delivery for testing | -| Adminer | `8080` | Database viewer. Use credentials from above and connect to `localhost` to get access to the NC database | - -## Permissions - -The container runs with the user `devcontainer` who is also running the Apache2 process. All mounted source files have -proper permissions so that this user can access everything which is inside the current workspace. If you need to -get root permissions for whatever reason, use `sudo su` or `sudo ` (for example `sudo service apache2 restart`). -Everything else (like building the application, adjusting files, ...) should be done as `devcontainer` user. - -## NodeJs and NVM - -The container comes with [`nvm`](https://github.com/nvm-sh/nvm) and Node 16 installed. This should be sufficient to -build Nextcloud Core sources via `make`. If you need a different Node Version (for example for -app development), you can easily switch between different versions by running: - -```bash -# Install and use Node 14 -nvm install 14 -nvm use 14 - -# Check version -node -v - -# Switch back to Node 16 -nvm use 16 - -# Check version -node -v -``` - -Note that `nvm` is only installed for the user `devcontainer` and won't work out of the box for -any other user. - -## Debugging - -The Apache webserver is already configured to automatically try to connect to a debugger process -listening on port `9003`. To start the VSCode debugger process, use the delivered debug profile `Listen for XDebug`. -After you started the VSCode debugger, just navigate to the appropriate Nextcloud URL to get your -debug hits. --- a/.devcontainer/codespace.config.php +++ /dev/null @@ -1,39 +0,0 @@ - 'no-reply', - 'mail_smtpmode' => 'smtp', - 'mail_sendmailmode' => 'smtp', - 'mail_domain' => 'example.com', - 'mail_smtphost' => 'localhost', - 'mail_smtpport' => '1025', - 'memcache.local' => '\OC\Memcache\APCu', -]; - -if(is_string($codespaceName) && !empty($codespaceName) && is_string($codespaceDomain) && !empty($codespaceDomain)) { - $host = $codespaceName . '-80.' . $codespaceDomain; - $CONFIG['overwritehost'] = $host; - $CONFIG['overwrite.cli.url'] = 'https://' . $host; - $CONFIG['overwriteprotocol'] = 'https'; - $CONFIG['trusted_domains'] = [ $host ]; -} --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "NextcloudServer", - "dockerComposeFile": "docker-compose.yml", - "service": "nextclouddev", - "postCreateCommand": ".devcontainer/setup.sh", - "postStartCommand": ".devcontainer/postStart.sh", - "forwardPorts": [ - 80, - 8080, - 8025 - ], - "customizations": { - "vscode": { - "extensions": [ - "felixfbecker.php-debug", - "felixfbecker.php-intellisense", - "ms-azuretools.vscode-docker", - "xdebug.php-debug", - "donjayamanne.githistory" - ], - "settings": { - "php.suggest.basic": false - } - } - }, - "workspaceFolder": "/var/www/html", - "remoteUser": "devcontainer" -} --- a/.devcontainer/devcontainer.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -services: - nextclouddev: - build: . - volumes: - - .:/workspace:cached - - /var/run/docker.sock:/var/run/docker-host.sock - - ..:/var/www/html - command: /var/www/html/.devcontainer/entrypoint.sh - ports: - - 80:80 - - 8080:8080 - - 8025:8025 - - db: - image: postgres - restart: always - environment: - POSTGRES_PASSWORD: postgres - PGDATA: /data/postgres - volumes: - - db:/data/postgres - network_mode: service:nextclouddev - - adminer: - image: adminer - restart: always - network_mode: service:nextclouddev - - mailhog: - image: mailhog/mailhog - restart: always - network_mode: service:nextclouddev - -volumes: - db: --- a/.devcontainer/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -# -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -sudo service apache2 start - -while sleep 1000; do :; done --- a/.devcontainer/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for Xdebug", - "type": "php", - "request": "launch", - "port": 9003 - } - ] -} --- a/.devcontainer/launch.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/.devcontainer/postStart.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Set git safe.directory -git config --global --add safe.directory /var/www/html -git config --global --add safe.directory /var/www/html/3rdparty - -# Ensure devcontainer user has access to docker socket -if [ -S /var/run/docker.sock ]; then - sudo chmod 666 /var/run/docker.sock -fi --- a/.devcontainer/setup.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../" >/dev/null 2>&1 && pwd )" - -cd $DIR/ - -git submodule update --init - -# Codespace config -cp .devcontainer/codespace.config.php config/codespace.config.php - -# VSCode debugger profile -mkdir -p .vscode && cp .devcontainer/launch.json .vscode/launch.json - -# Onetime installation setup -if [[ ! $(sudo -u ${APACHE_RUN_USER} php occ status) =~ installed:[[:space:]]*true ]]; then - echo "Running NC installation" - sudo -u ${APACHE_RUN_USER} php occ maintenance:install \ - --verbose \ - --database=pgsql \ - --database-name=postgres \ - --database-host=127.0.0.1 \ - --database-port=5432 \ - --database-user=postgres \ - --database-pass=postgres \ - --admin-user admin \ - --admin-pass admin -fi - -sudo service apache2 restart --- a/.github/CODEOWNERS +++ /dev/null @@ -1,141 +0,0 @@ -# Fallback owners for code review - ensure all PRs have someone assigned for review. -# (the last match will used so this is only used if there is no more specific code owner below) - -# Backend -# is the first and gets everything to make things easier from matching syntax -* @nextcloud/server-backend - -# Frontend -# this will override the backend code owners if needed -/__mocks__ @nextcloud/server-frontend -/__tests__ @nextcloud/server-frontend -/dist @nextcloud/server-frontend -/tests/playwright @nextcloud/server-frontend -**/css @nextcloud/server-frontend -**/js @nextcloud/server-frontend -**/src @nextcloud/server-frontend -*.js @nextcloud/server-frontend -*.cjs @nextcloud/server-frontend -*.mjs @nextcloud/server-frontend -*.ts @nextcloud/server-frontend - -# dependency management -package.json @nextcloud/server-dependabot @nextcloud/server-frontend -package-lock.json @nextcloud/server-dependabot - -# Two-Factor Authentication -# https://github.com/nextcloud/wg-two-factor-authentication#members -**/TwoFactorAuth @ChristophWurst @miaulalala -/apps/twofactor_backupcodes @ChristophWurst @miaulalala -/core/templates/twofactor* @ChristophWurst @miaulalala - -# Limit login to IP -# Watch login routes for https://github.com/nextcloud/limit_login_to_ip -/core/routes.php @Altahrim @nextcloud/server-backend - -# OpenAPI -openapi*.json @provokateurin @nextcloud/server-backend -ResponseDefinitions.php @provokateurin @nextcloud/server-backend - -# Files Team -/lib/public/Files* @nextcloud/files-backend -/lib/public/Files_FullTextSearch @nextcloud/files-backend @GhassenKefi -/lib/public/Share* @nextcloud/files-backend -/lib/private/Share* @nextcloud/files-backend -/apps/encryption @nextcloud/files-backend -/apps/encryption/src @nextcloud/files-frontend -/apps/federatedfilesharing @nextcloud/files-backend -/apps/federatedfilesharing/src @nextcloud/files-frontend -/apps/files @nextcloud/files -/apps/files/lib @nextcloud/files-backend -/apps/files/tests @nextcloud/files-backend -/apps/files/src @nextcloud/files-frontend @skjnldsv -/apps/files_* @nextcloud/files -/apps/files_*/lib @nextcloud/files-backend -/apps/files_*/tests @nextcloud/files-backend -/apps/files_*/src @nextcloud/files-frontend -/apps/sharebymail @nextcloud/files-backend -/apps/sharebymail/src @nextcloud/files-frontend -/apps/sharing @nextcloud/files -/apps/sharing/lib @nextcloud/files-backend @provokateurin -/apps/sharing/src @nextcloud/files-frontend - -# IDP team -/lib/public/Accounts @nextcloud/idp -/lib/private/Accounts @nextcloud/idp -/lib/public/Authentication @nextcloud/idp -/lib/private/Authentication @nextcloud/idp -/lib/public/*User* @nextcloud/idp -/lib/private/User @nextcloud/idp -/lib/public/*Group* @nextcloud/idp -/lib/private/Group @nextcloud/idp -/lib/public/Teams @nextcloud/idp -/lib/private/Teams @nextcloud/idp -/lib/public/LDAP @nextcloud/idp -/lib/private/LDAP @nextcloud/idp -/apps/lookup_server_connector @nextcloud/idp -/apps/profile @nextcloud/idp -/apps/user_ldap @nextcloud/idp - -# Talk team -/lib/private/Comments @nickvergessen @nextcloud/talk-backend -/lib/private/Federation @nickvergessen @nextcloud/talk-backend @nextcloud/server-backend -/lib/private/Talk @nickvergessen @nextcloud/talk-backend -/lib/public/Comments @nickvergessen @nextcloud/talk-backend -/lib/public/Federation @nickvergessen @nextcloud/talk-backend @nextcloud/server-backend -/lib/public/OCM @nickvergessen @nextcloud/talk-backend @nextcloud/server-backend -/lib/public/Talk @nickvergessen @nextcloud/talk-backend -/lib/public/UserStatus @nickvergessen @nextcloud/talk-backend @nextcloud/idp -*/Notifications/* @nickvergessen @nextcloud/talk-backend - -# Groupware team -/build/integration/dav_features/caldav.feature @ChristophWurst @SebastianKrupinski @tcitworld -/build/integration/dav_features/carddav.feature @hamza221 @SebastianKrupinski -/lib/private/Calendar @ChristophWurst @SebastianKrupinski @tcitworld -/lib/private/Contacts @hamza221 @SebastianKrupinski -/lib/public/Calendar @ChristophWurst @SebastianKrupinski @tcitworld -/lib/public/Contacts @hamza221 @SebastianKrupinski -/apps/contactsinteraction @kesselb @SebastianKrupinski -/apps/dav/lib/CalDAV @ChristophWurst @SebastianKrupinski @tcitworld -/apps/dav/lib/CardDAV @hamza221 @SebastianKrupinski -/apps/dav/tests/unit/CalDAV @ChristophWurst @SebastianKrupinski @tcitworld -/apps/dav/tests/unit/CardDAV @hamza221 @SebastianKrupinski - -# Desktop client teamn -/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php @nextcloud/desktop - -# Security team -/build/psalm-baseline-security.xml @nickvergessen @nextcloud/server-backend -/resources/codesigning @nickvergessen @mgallien @miaulalala -/resources/config/ca-bundle.crt @nickvergessen @miaulalala - -# App maintainers -/apps/admin_audit/appinfo/info.xml @luka-nextcloud @samin-z -/apps/cloud_federation_api/appinfo/info.xml @leftybournes @provokateurin -/apps/comments/appinfo/info.xml @edward-ly @marcelklehr -/apps/contactsinteraction/appinfo/info.xml @kesselb @SebastianKrupinski -/apps/dashboard/appinfo/info.xml @julien-nc @juliusknorr -/apps/encryption/appinfo/info.xml @GhassenKefi @icewind1991 -/apps/federatedfilesharing/appinfo/info.xml @leftybournes @icewind1991 -/apps/federation/appinfo/info.xml @leftybournes @sorbaugh -/apps/files/appinfo/info.xml @nextcloud/files-frontend -/apps/files_external/appinfo/info.xml @icewind1991 @artonge -/apps/files_reminders/appinfo/info.xml @skjnldsv @sorbaugh -/apps/files_sharing/appinfo/info.xml @skjnldsv @provokateurin -/apps/files_trashbin/appinfo/info.xml @icewind1991 @sorbaugh -/apps/files_versions/appinfo/info.xml @artonge @icewind1991 -/apps/lookup_server_connector/appinfo/info.xml @cristianscheid @Altahrim -/apps/oauth2/appinfo/info.xml @julien-nc @ChristophWurst -/apps/provisioning_api/appinfo/info.xml @come-nc @artonge -/apps/settings/appinfo/info.xml @hweihwang @sorbaugh -/apps/sharebymail/appinfo/info.xml @provokateurin @skjnldsv -/apps/sharing/appinfo/info.xml @provokateurin @skjnldsv -/apps/systemtags/appinfo/info.xml @Antreesy @marcelklehr -/apps/theming/appinfo/info.xml @skjnldsv @juliusknorr -/apps/twofactor_backupcodes/appinfo/info.xml @miaulalala @ChristophWurst -/apps/updatenotification/appinfo/info.xml @enjeck @sorbaugh -/apps/user_ldap/appinfo/info.xml @come-nc @blizzz -/apps/user_status/appinfo/info.xml @Antreesy @nickvergessen -/apps/weather_status/appinfo/info.xml @julien-nc @juliusknorr -/apps/webhook_listeners/appinfo/info.xml @janepie @julien-nc -/apps/workflowengine/appinfo/info.xml @blizzz @juliusknorr --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,99 +0,0 @@ - -## Submitting issues - -If you have questions about how to install or use Nextcloud, please direct these to our [forum][forum]. We are also available on [IRC][irc] (unofficial). - -### Short version - - * The [**issue templates can be found here**][templates] but be aware of the different repositories! See list below. Please always use an issue template when reporting issues. - -### Guidelines -* Please search the existing issues first, it's likely that your issue was already reported or even fixed. - - Go to one of the repositories, click "issues" and type any word in the top search/command bar. - - You can also filter by appending e. g. "state:open" to the search string. - - More info on [search syntax within github](https://help.github.com/articles/searching-issues) -* This repository ([server](https://github.com/nextcloud/server/issues)) is *only* for issues within the Nextcloud Server code. This also includes the apps: files, encryption, external storage, sharing, deleted files, versions, LDAP, and WebDAV Auth -* __SECURITY__: Report any potential security bug to us via [our HackerOne page](https://hackerone.com/nextcloud) following our [security policy](https://nextcloud.com/security/) instead of filing an issue in our bug tracker. -* The issues in other components should be reported in their respective repositories: You will find them in our [GitHub Organization](https://github.com/nextcloud/) - -* Report the issue using one of our [templates][templates], they include all the information we need to track down the issue. - -Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. - -[templates]: ./ISSUE_TEMPLATE -[forum]: https://help.nextcloud.com/ -[irc]: https://web.libera.chat/#nextcloud - -## Contributing to Source Code - -Thanks for wanting to contribute source code to Nextcloud. That's great! - -Please read the [general contribution guidelines][contributing] that apply to all Nextcloud repositories, and the [AI Contribution Policy][aipolicy] if you are using AI tools. - -Please read the [Developer Manuals][devmanual] to learn how to create your first application or how to test the Nextcloud code with PHPUnit. - -### AI-assisted contributions - -Nextcloud allows contributions made with the help of AI tools. You are the author of everything you submit - AI assistance does not change that responsibility. - -* **Disclosure:** Declare AI tool use in the PR description and add an `Assisted-by: AGENT_NAME:MODEL_VERSION` git trailer to each affected commit. - -* **Accountability:** You must be able to explain, defend, and modify every line you submit. If a reviewer asks why something works a certain way, "the AI wrote it" is not an answer. - -* **Communication:** PR descriptions, review comments, and issue reports must be written in your own words. This applies throughout the review process - passing reviewer feedback to an AI and posting whatever comes out is not acceptable. - -* **Quality:** AI output must be quality assured by the human, i.e. reviewed, cleaned up, and tested before submission. New features must be tested on a live instance by you, not by an agent. Code that has never been executed, or that shifts debugging work onto maintainers, will not be accepted. - -* **Licensing:** Ensure AI-generated code contains no material incompatible with the license of the repository you are contributing to. - -For the full policy including autonomous agent rules, security reports, and beginner issues, read the [AI Contribution Policy][aipolicy]. - -### Conventional Commits - -Please use [Conventional Commits](https://www.conventionalcommits.org) for your commit messages. This helps maintain clarity and consistency across the project, making it easier to understand changes and automate versioning. -``` -feat(files_sharing): allow sharing with contacts -``` - -### Tests - -In order to constantly increase the quality of our software we can no longer accept pull request which submit un-tested code. -It is a must have that changed and added code segments are unit tested. -In some areas unit testing is hard (aka almost impossible) as of today - in these areas refactoring WHILE fixing a bug is encouraged to enable unit testing. - -### Sign your work - -We use the Developer Certificate of Origin (DCO) as an additional safeguard -for the Nextcloud project. This is a well established and widely used -mechanism to assure contributors have confirmed their right to license -their contribution under the project's license. -Please read [contribute/developer-certificate-of-origin][dcofile]. -If you can certify it, then just add a line to every git commit message: - -``` -Signed-off-by: Random J Developer -``` - -Use your real name (sorry, no pseudonyms or anonymous contributions). -If you set your `user.name` and `user.email` git configs, you can sign your -commit automatically with `git commit -s`. You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases) -like `git config --global alias.ci 'commit -s'`. Now you can commit with -`git ci` and the commit will be signed. - -### Apply a license - -In case you are not sure how to add or update the license header correctly please have a look at [contribute/HowToApplyALicense.md][applyalicense] - -[devmanual]: https://docs.nextcloud.com/server/latest/developer_manual/ -[dcofile]: https://github.com/nextcloud/server/blob/master/contribute/developer-certificate-of-origin -[applyalicense]: https://github.com/nextcloud/server/blob/master/contribute/HowToApplyALicense.md -[contributing]: https://github.com/nextcloud/.github/blob/master/CONTRIBUTING.md -[aipolicy]: https://github.com/nextcloud/.github/blob/master/AI_POLICY.md - -## Translations -Please submit translations via [Transifex][transifex]. - -[transifex]: https://www.transifex.com/nextcloud --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -custom: https://nextcloud.com/include/ --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ /dev/null @@ -1,213 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -name: "🐛 Bug report: Nextcloud Server" -description: "Submit a report and help us improve Nextcloud Server" -title: "[Bug]: " -labels: ["bug", "0. Needs triage"] -type: "Bug" -body: - - type: markdown - attributes: - value: | - ### 👍 Thank you for contributing to our project! - Please note this is a **free and open-source** project. Most people take on their own time to help you, so please, be patient. - You can obtain [Enterprise support](https://nextcloud.com/support/) if you run Nextcloud Server in a mission critical environment. - - type: markdown - attributes: - value: | - ### 🚨 SECURITY INFO - If you are reporting a security concern, please report it via [our HackerOne page](https://hackerone.com/nextcloud) instead and review our [security policy](https://nextcloud.com/security/). - This allows us to coordinate the fix and release without potentially exposing all Nextcloud servers and users in the meantime. - It also may qualify your report for a bug bounty reward. - Thank you for helping make Nextcloud more secure! - - type: checkboxes - id: before-posting - attributes: - label: "⚠️ This issue respects the following points: ⚠️" - description: All conditions are **required**. Your issue can be closed if these are checked incorrectly. - options: - - label: This is a **bug**, not a question or a configuration/webserver/proxy issue. - required: true - - label: This issue is **not** already reported on [Github](https://github.com/nextcloud/server/issues?q=is%3Aopen+is%3Aissue+label%3Abug) OR [Nextcloud Community Forum](https://help.nextcloud.com/) _(I've searched it)_. - required: true - - label: Nextcloud Server **is** up to date. See [Maintenance and Release Schedule](https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule) for supported versions. - required: true - - label: I agree to follow Nextcloud's [Code of Conduct](https://nextcloud.com/contribute/code-of-conduct/). - required: true - - type: textarea - id: bug-description - attributes: - label: Bug description - description: | - Provide a description of the bug you're experiencing. - Don't just expect someone will guess what your specific problem is and provide full details. - validations: - required: true - - type: textarea - id: reproduce - attributes: - label: Steps to reproduce - description: | - Describe the steps to reproduce the bug. - The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ answer. - value: | - 1. - 2. - 3. - validations: - required: true - - type: textarea - id: expected-behavior - attributes: - label: Expected behavior - description: Describe what you expected to happen instead. - validations: - required: true - - type: dropdown - id: nextcloud-version - attributes: - label: Nextcloud Server version - description: | - Select Nextcloud Server version. - _Versions not listed here are not maintained and not supported anymore_ - options: - - "32" - - "33" - - "34" - - "35 (master)" - validations: - required: true - - type: dropdown - id: system - attributes: - label: Operating system - description: | - Select operating system where Nextcloud Server is installed. - _Describe in the "Additional info" section if you chose "Other"._ - options: - - "Debian/Ubuntu" - - "RHEL/CentOS" - - "Other" - - type: dropdown - id: php - attributes: - label: PHP engine version - description: | - Select PHP engine version serving Nextcloud Server. - _Describe in the "Additional info" section if you chose "Other"._ - options: - - "PHP 8.5" - - "PHP 8.4" - - "PHP 8.3" - - "PHP 8.2" - - "PHP 8.1" - - "Other" - - type: dropdown - id: webserver - attributes: - label: Web server - description: | - Select Webserver serving Nextcloud Server. - _Describe in the "Additional info" section if you chose "Other"._ - options: - - "Apache (supported)" - - "Nginx" - - "Other" - - type: dropdown - id: database - attributes: - label: Database engine version - description: | - Select Database engine serving Nextcloud Server. - _Describe in the "Additional info" section if you chose "Other"._ - options: - - "MySQL" - - "MariaDB" - - "PostgreSQL" - - "SQlite" - - "Oracle" - - "Other" - - type: dropdown - id: fresh - attributes: - label: Is this bug present after an update or on a fresh install? - description: | - Select if bug is present after an update or on a fresh install. - options: - - "Updated from a MINOR version (ex. 32.0.1 to 32.0.2)" - - "Upgraded to a MAJOR version (ex. 31 to 32)" - - "Fresh Nextcloud Server install" - - type: dropdown - id: encryption - attributes: - label: Are you using the Nextcloud Server Encryption module? - description: Select if encryption is Enabled on your Nextcloud Server. - options: - - "Encryption is Enabled" - - "Encryption is Disabled" - - type: checkboxes - id: users - attributes: - label: What user-backends are you using? - description: | - Select **all** user-backends Enabled on your Nextcloud Server. - _Describe in the "Additional info" section if you chose "Other"._ - options: - - label: "Default user-backend _(database)_" - - label: "LDAP/ Active Directory" - - label: "SSO - SAML" - - label: "Other" - - type: textarea - id: configs - attributes: - label: Configuration report - description: | - Provide information about your configuration. - To get this information, execute one of the following commands on the CLI: - ```shell - sudo -u www-data php occ config:list system - php occ config:list system - ./occ config:list system - ``` - > NOTE: This will be automatically formatted into code for better readability. - render: json - - type: textarea - id: apps - attributes: - label: List of activated Apps - description: | - Provide information about your apps. - To get this information, execute one of the following commands on the CLI: - ```shell - sudo -u www-data php occ app:list - php occ app:list - ./occ app:list - ``` - > NOTE: This will be automatically formatted into code for better readability. - render: shell - - type: textarea - id: nextcloud-signingstatus - attributes: - label: Nextcloud Signing status - description: | - Provide Nextcloud Signing status. - First, login as Admin user into your Nextcloud, then access this URL: - ```shell - https://yournextcloud.tld/index.php/settings/integrity/failed - ``` - > NOTE: This will be automatically formatted into code for better readability. - render: shell - - type: textarea - id: nextcloud-log - attributes: - label: Nextcloud Logs - description: | - Provide relevant Nextcloud log entries (e.g. from the time period you reproduced the problem). - Copy full individual entries from `data/nextcloud.log` or use `Copy raw entry` from `/settings/admin/logging` section: - > NOTE: This will be automatically formatted into code for better readability. - render: json - - type: textarea - id: additional-info - attributes: - label: Additional info - description: Any additional information related to the issue (ex. browser console errors, software versions). --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: 🚀 Feature request -about: Suggest an idea for this project -labels: enhancement, 0. Needs triage -type: "Enhancement" ---- - - - - - - -> [!TIP] -> ### Help move this idea forward -> * Use the 👍 reaction to show support for this feature. -> * Avoid commenting unless you have relevant information to add; unnecessary comments create noise for subscribers. -> * Subscribe to receive notifications about status changes and new comments. ---- - - -**Is your feature request related to a problem? Please describe.** - - -**Describe the solution you'd like** - - -**Describe alternatives you've considered** - - -**Additional context** - --- a/.github/ISSUE_TEMPLATE/Feature_request.md.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -contact_links: - - name: 🚨 Report a security or privacy issue - url: https://hackerone.com/nextcloud - about: Report security and privacy related issues privately to the Nextcloud team, so we can coordinate the fix and release without potentially exposing all Nextcloud servers and users in the meantime. - - name: ❓ Community Support and Help - url: https://help.nextcloud.com/ - about: Configuration, webserver/proxy or performance issues and other questions - - name: 💼 Nextcloud Enterprise - url: https://portal.nextcloud.com/ - about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly --- a/.github/codeql-config.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: "CodeQL config" - -paths-ignore: - - dist --- a/.github/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -firstPRMergeComment: > - Thanks for your first pull request and welcome to the community! - Feel free to keep them coming! If you are looking for issues to tackle then have a look at this selection: https://github.com/nextcloud/server/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 --- a/.github/dependabot.yml +++ /dev/null @@ -1,234 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -version: 2 -updates: -# GitHub Actions -- package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: monthly - timezone: Europe/Paris - groups: - github-actions: - patterns: - - "*" - labels: - - "3. to review" - - "feature: dependencies" - -# Main composer (linting, testing, openapi) -- package-ecosystem: composer - directories: - - "/" - - "/vendor-bin/behat" - - "/vendor-bin/cs-fixer" - - "/vendor-bin/openapi-extractor" - - "/vendor-bin/phpunit" - - "/vendor-bin/psalm" - - "/vendor-bin/rector" - schedule: - interval: weekly - day: saturday - time: "03:00" - timezone: Europe/Paris - labels: - - "3. to review" - - "feature: dependencies" - open-pull-requests-limit: 20 - versioning-strategy: increase - -# Main master npm frontend dependencies -- package-ecosystem: npm - directories: - - "/" - - "/build/frontend" - - "/build/frontend-legacy" - schedule: - interval: weekly - day: saturday - time: "03:00" - timezone: Europe/Paris - labels: - - "3. to review" - - "feature: dependencies" - open-pull-requests-limit: 20 - versioning-strategy: increase - # Disable automatic rebasing because without a build CI will likely fail anyway - rebase-strategy: "disabled" - cooldown: - default-days: 4 - semver-major-days: 8 - groups: - eslint: - patterns: - - "eslint*" - - "@nextcloud/eslint-config" - vite: - patterns: - - "vite" - - "@nextcloud/vite-config" - vitest: - patterns: - - "vitest" - - "@vitest/*" - -# Latest stable release - -# Composer dependencies for linting and testing -- package-ecosystem: composer - target-branch: stable34 - directories: - - "/" - - "/vendor-bin/behat" - - "/vendor-bin/cs-fixer" - - "/vendor-bin/openapi-extractor" - - "/vendor-bin/phpunit" - - "/vendor-bin/psalm" - - "/vendor-bin/rector" - schedule: - interval: weekly - day: saturday - time: "03:30" - timezone: Europe/Paris - labels: - - "3. to review" - - "feature: dependencies" - ignore: - # only patch updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major", "version-update:semver-minor"] - -# frontend dependencies -- package-ecosystem: npm - target-branch: stable34 - directories: - - "/" - - "/build/frontend" - - "/build/frontend-legacy" - schedule: - interval: weekly - day: saturday - time: "03:30" - timezone: Europe/Paris - cooldown: - default-days: 4 - semver-major-days: 8 - labels: - - "3. to review" - - "feature: dependencies" - open-pull-requests-limit: 20 - versioning-strategy: increase - # Disable automatic rebasing because without a build CI will likely fail anyway - rebase-strategy: "disabled" - groups: - vite: - patterns: - - "vite" - - "@nextcloud/vite-config" - vitest: - patterns: - - "vitest" - - "@vitest/*" - ignore: - # no major updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major"] - -# Older stable releases - -# Composer dependencies for linting and testing -- package-ecosystem: composer - target-branch: stable33 - directories: - - "/" - - "/vendor-bin/behat" - - "/vendor-bin/cs-fixer" - - "/vendor-bin/openapi-extractor" - - "/vendor-bin/phpunit" - - "/vendor-bin/psalm" - schedule: - interval: weekly - day: saturday - time: "04:30" - timezone: Europe/Paris - labels: - - "3. to review" - - "feature: dependencies" - ignore: - # only patch updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major", "version-update:semver-minor"] - -# frontend dependencies -- package-ecosystem: npm - target-branch: stable33 - directories: - - "/" - - "/build/frontend" - - "/build/frontend-legacy" - schedule: - interval: weekly - day: saturday - time: "04:30" - timezone: Europe/Paris - cooldown: - default-days: 4 - semver-major-days: 8 - labels: - - "3. to review" - - "feature: dependencies" - open-pull-requests-limit: 20 - versioning-strategy: increase - # Disable automatic rebasing because without a build CI will likely fail anyway - rebase-strategy: "disabled" - ignore: - # no major updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major"] - -# Composer dependencies for linting and testing -- package-ecosystem: composer - target-branch: stable32 - directories: - - "/" - - "/vendor-bin/behat" - - "/vendor-bin/cs-fixer" - - "/vendor-bin/openapi-extractor" - - "/vendor-bin/phpunit" - - "/vendor-bin/psalm" - schedule: - interval: weekly - day: saturday - time: "04:30" - timezone: Europe/Paris - labels: - - "3. to review" - - "feature: dependencies" - ignore: - # only patch updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major", "version-update:semver-minor"] - -# frontend dependencies -- package-ecosystem: npm - target-branch: stable32 - directory: "/" - schedule: - interval: weekly - day: saturday - time: "04:30" - timezone: Europe/Paris - cooldown: - default-days: 4 - semver-major-days: 8 - open-pull-requests-limit: 20 - labels: - - "3. to review" - - "feature: dependencies" - # Disable automatic rebasing because without a build CI will likely fail anyway - rebase-strategy: "disabled" - ignore: - # no major updates on stable branches - - dependency-name: "*" - update-types: ["version-update:semver-major"] --- a/.github/pull_request_template.md +++ /dev/null @@ -1,29 +0,0 @@ - - -* Resolves: # - -## Summary - - -## TODO - -- [ ] ... - -## Checklist - -- Code is [properly formatted](https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/continuous_integration.html#linting) -- [Sign-off message](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) is added to all commits -- [ ] Tests ([unit](https://docs.nextcloud.com/server/latest/developer_manual/app_development/tutorial.html#unit-tests), [integration](https://docs.nextcloud.com/server/latest/developer_manual/app_development/tutorial.html#integration-tests), api and/or acceptance) are included -- [ ] Screenshots before/after for front-end changes -- [ ] Documentation ([manuals](https://github.com/nextcloud/documentation/) or wiki) has been updated or is not required -- [ ] [Backports requested](https://github.com/nextcloud/backportbot/#usage) where applicable (ex: critical bugfixes) -- [ ] [Labels added](https://github.com/nextcloud/server/labels) where applicable (ex: bug/enhancement, `3. to review`, feature component) -- [ ] [Milestone added](https://github.com/nextcloud/server/milestones) for target branch/version (ex: 32.x for `stable32`) - -## AI (if applicable) - -- [ ] The content of this PR was partly or fully generated using AI --- a/.github/pull_request_template.md.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/.github/workflows/ai-policy.yml +++ /dev/null @@ -1,178 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: AI Policy - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: [master, main] - -permissions: - contents: read - # Required to add the "AI assisted" label via `gh pr edit --add-label` - pull-requests: write - # Required to create the "AI assisted" label via the REST labels endpoint - # (labels are an issues-scoped resource in the GitHub API) - issues: write - -concurrency: - group: ai-policy-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - check-ai-trailers: - runs-on: ubuntu-latest-low - steps: - - name: Collect PR commit messages - id: collect - env: - # Fall back to the default token when the PAT is unavailable - # (e.g. Dependabot- or fork-triggered runs don't receive Actions secrets) - GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT || github.token }} - COMMITS_URL: ${{ github.event.pull_request.commits_url }} - run: | - set -euo pipefail - gh api ${COMMITS_URL} | jq -r '.[] | .commit.message' > /tmp/pr_commits.txt - echo "--- PR commit messages ---" - cat /tmp/pr_commits.txt - echo "--------------------------" - - - name: Define shared agent detection patterns - run: | - set -euo pipefail - - # Email addresses known to be used by coding agents. - # These should never appear in Signed-off-by because the DCO can only be attested by a human. - EMAIL_PATTERN="copilot@github\.com\ - |noreply@anthropic\.com\ - |devin@cognition\.ai\ - |devin@cognition-labs\.com\ - |aider@aider\.chat\ - |noreply@aider\.chat\ - |codex@openai\.com\ - |cursor@anysphere\.com\ - |windsurf@codeium\.com\ - |codeium@codeium\.com\ - |amazon-q@amazon\.com\ - |codewhisperer@amazon\.com\ - |gemini-code-assist@google\.com\ - |openhands@all-hands\.dev\ - |swe-agent@princeton\.edu" - - # Strip embedded whitespace (used above only for readability) - EMAIL_PATTERN=$(echo "$EMAIL_PATTERN" | tr -d ' \n') - echo "AGENT_EMAIL_PATTERN=${EMAIL_PATTERN}" >> "$GITHUB_ENV" - - # Display-name prefixes used by known coding agents (shared by Signed-off-by and Co-Authored-By checks) - # shellcheck disable=SC2016 - echo 'AGENT_NAMES=GitHub Copilot|Claude( [A-Za-z0-9. -]+)?|Devin( AI)?|aider( \(.*\))?|OpenAI Codex|Cursor( AI)?|Windsurf|Amazon Q|CodeWhisperer|Gemini Code Assist|OpenHands|SWE-agent|AutoCodeRover|Tabnine' >> "$GITHUB_ENV" - - - name: Check for AI-assistant / Assisted-by trailers - id: ai_trailers - run: | - set -euo pipefail - AI_ASSISTED=false - if grep -qiE '^(AI-assistant|Assisted-by|AI-Assisted-By):' /tmp/pr_commits.txt; then - AI_ASSISTED=true - echo "Found AI-assistant/Assisted-by/AI-Assisted-By trailer(s):" - grep -iE '^(AI-assistant|Assisted-by|AI-Assisted-By):' /tmp/pr_commits.txt - fi - echo "ai_assisted=${AI_ASSISTED}" >> "$GITHUB_OUTPUT" - - - name: Check for coding-agent Signed-off-by trailers - id: agent_signoff - run: | - set -euo pipefail - - EMAIL_HITS=$(grep -iE "^Signed-off-by:.*<(${AGENT_EMAIL_PATTERN})>" /tmp/pr_commits.txt 2>/dev/null || true) - NAME_HITS=$(grep -iE "^Signed-off-by: *(${AGENT_NAMES}) *[<(]" /tmp/pr_commits.txt 2>/dev/null || true) - - AGENT_LINES=$(printf '%s\n%s' "$EMAIL_HITS" "$NAME_HITS" | sort -u | sed '/^[[:space:]]*$/d') - - AGENT_SIGNOFF=false - if [ -n "$AGENT_LINES" ]; then - AGENT_SIGNOFF=true - fi - - echo "agent_signoff=${AGENT_SIGNOFF}" >> "$GITHUB_OUTPUT" - { - echo "agent_lines<> "$GITHUB_OUTPUT" - - - name: Check for coding-agent Co-Authored-By trailers - id: co_authored - run: | - set -euo pipefail - - EMAIL_HITS=$(grep -iE "^Co-Authored-By:.*<(${AGENT_EMAIL_PATTERN})>" /tmp/pr_commits.txt 2>/dev/null || true) - NAME_HITS=$(grep -iE "^Co-Authored-By: *(${AGENT_NAMES}) *[<(]" /tmp/pr_commits.txt 2>/dev/null || true) - - CO_AUTHORED=false - if [ -n "$EMAIL_HITS" ] || [ -n "$NAME_HITS" ]; then - CO_AUTHORED=true - echo "Found coding-agent Co-Authored-By trailer(s):" - printf '%s\n%s' "$EMAIL_HITS" "$NAME_HITS" | sort -u | sed '/^[[:space:]]*$/d' - fi - - echo "co_authored=${CO_AUTHORED}" >> "$GITHUB_OUTPUT" - - - name: Create 'AI assisted' label if absent - if: steps.ai_trailers.outputs.ai_assisted == 'true' || steps.agent_signoff.outputs.agent_signoff == 'true' || steps.co_authored.outputs.co_authored == 'true' - env: - # Fall back to the default token when the PAT is unavailable - # (e.g. Dependabot- or fork-triggered runs don't receive Actions secrets) - GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT || github.token }} - run: | - gh api "repos/${{ github.repository }}/labels" \ - --method POST \ - -f name="AI assisted" \ - -f color="d93f0b" \ - -f description="This PR contains AI-assisted commits" \ - 2>/dev/null || true - - - name: Label PR as AI assisted - if: steps.ai_trailers.outputs.ai_assisted == 'true' || steps.agent_signoff.outputs.agent_signoff == 'true' || steps.co_authored.outputs.co_authored == 'true' - env: - # Fall back to the default token when the PAT is unavailable - # (e.g. Dependabot- or fork-triggered runs don't receive Actions secrets) - GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT || github.token }} - run: | - gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \ - --method POST \ - -f "labels[]=AI assisted" - echo "Added 'AI assisted' label to PR #${{ github.event.pull_request.number }}" - - - name: Fail on coding-agent Signed-off-by - if: steps.agent_signoff.outputs.agent_signoff == 'true' - env: - AGENT_LINES: ${{ steps.agent_signoff.outputs.agent_lines }} - AGENTS_MD_URL: https://github.com/${{ github.repository }}/blob/${{ github.base_ref }}/AGENTS.md - run: | - echo "::error title=Coding-agent sign-off detected::A Signed-off-by trailer from a known coding agent was found in one or more commits." - echo "" - echo "Offending trailer(s):" - echo "${AGENT_LINES}" - echo "" - echo "The 'Signed-off-by' trailer represents the Developer Certificate of Origin (DCO)" - echo "and must only be attested by a human contributor." - echo "Please amend the affected commit(s) to remove the coding-agent sign-off" - echo "and replace it with an 'Assisted-by' trailer, for example:" - echo "" - echo " Assisted-by: Claude Code:claude-sonnet-4-6" - echo "" - echo "References:" - echo " • AGENTS.md (this repository)" - echo " ${AGENTS_MD_URL}" - echo " • AI Contribution Policy" - echo " https://github.com/nextcloud/.github/blob/master/AI_POLICY.md" - echo " • Contribution Guidelines" - echo " https://github.com/nextcloud/.github/blob/master/CONTRIBUTING.md" - exit 1 --- a/.github/workflows/autocheckers.yml +++ /dev/null @@ -1,123 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Code checkers - -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: autocheckers-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src }} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/appinfo/**' - - '**/lib/**' - - '**/templates/**' - - 'vendor/**' - - 'vendor-bin/**' - - 'composer.json' - - 'composer.lock' - - '**.php' - - build/autoloaderchecker.sh - - autoloader: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: PHP checkers - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check auto loaders - run: bash ./build/autoloaderchecker.sh - - autocheckers: - runs-on: ubuntu-latest-low - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: Translation and Files checkers - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check translations are JSON decodeable - run: php ./build/translation-checker.php - - - name: Check translations do not contain triple dot but ellipsis - run: php ./build/triple-dot-checker.php - - - name: Check .htaccess does not contain invalid changes - run: php ./build/htaccess-checker.php - - - name: Check that all and only expected files are included - run: php ./build/files-checker.php - - - name: Check that all shipped apps are linted by psalm - run: sh ./build/psalm-checker.sh - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, autoloader, autocheckers] - - if: always() - - name: autocheckers-summary - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && (needs.autocheckers.result != 'success' || needs.autoloader.result != 'success') }}; then exit 1; fi --- a/.github/workflows/block-merge-eol.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Block merges for EOL - -on: pull_request - -permissions: - contents: read - -concurrency: - group: block-merge-eol-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - block-merges-eol: - name: Block merges for EOL branches - - # Only run on stableXX branches - if: startsWith( github.base_ref, 'stable') - runs-on: ubuntu-latest-low - - steps: - - name: Set server major version environment - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const regex = /^stable(\d+)$/ - const baseRef = context.payload.pull_request.base.ref - const match = baseRef.match(regex) - if (match) { - console.log('Setting server_major to ' + match[1]); - core.exportVariable('server_major', match[1]); - console.log('Setting current_day to ' + (new Date()).toISOString().substr(0, 10)); - core.exportVariable('current_day', (new Date()).toISOString().substr(0, 10)); - } - - - name: Checking if server ${{ env.server_major }} is EOL - if: ${{ env.server_major != '' }} - run: | - curl -s https://raw.githubusercontent.com/nextcloud-releases/updater_server/production/config/major_versions.json \ - | jq '.["${{ env.server_major }}"]["eol"] // "9999-99-99" | . >= "${{ env.current_day }}"' \ - | grep -q true --- a/.github/workflows/block-merge-freeze.yml +++ /dev/null @@ -1,57 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Block merges during freezes - -on: - pull_request: - types: [opened, ready_for_review, reopened, synchronize] - -permissions: - contents: read - -concurrency: - group: block-merge-freeze-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - block-merges-during-freeze: - name: Block merges during freezes - - if: github.event.pull_request.draft == false - - runs-on: ubuntu-latest-low - - steps: - - name: Register server reference to fallback to master branch - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const baseRef = context.payload.pull_request.base.ref - if (baseRef === 'main' || baseRef === 'master') { - core.exportVariable('server_ref', 'master'); - console.log('Setting server_ref to master'); - } else { - const regex = /^stable(\d+)$/ - const match = baseRef.match(regex) - if (match) { - core.exportVariable('server_ref', match[0]); - console.log('Setting server_ref to ' + match[0]); - } else { - console.log('Not based on master/main/stable*, so skipping freeze check'); - } - } - - - name: Download version.php from ${{ env.server_ref }} - if: ${{ env.server_ref != '' }} - run: curl 'https://raw.githubusercontent.com/nextcloud/server/${{ env.server_ref }}/version.php' --output version.php - - - name: Run check - if: ${{ env.server_ref != '' }} - run: cat version.php | grep 'OC_VersionString' | grep -i -v -E '(\.0\.0 RC[3-9]|\.[1-9][0-9]* RC)' --- a/.github/workflows/block-outdated-3rdparty.yml +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Block merging with outdated 3rdparty/ - -on: - pull_request: - types: [opened, ready_for_review, reopened, synchronize] - -permissions: - contents: read - -concurrency: - group: block-outdated-3rdparty-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - block-outdated-3rdparty: - name: Block merging with outdated 3rdparty/ - - runs-on: ubuntu-latest-low - - steps: - - name: Check requirement - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '3rdparty' - - 'version.php' - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: 3rdparty commit hash on current branch - id: actual - run: | - echo "commit=$(git submodule status | grep ' 3rdparty' | egrep -o '[a-f0-9]{40}')" >> "$GITHUB_OUTPUT" - - - name: Register server reference to fallback to master branch - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const baseRef = context.payload.pull_request.base.ref - if (baseRef === 'main' || baseRef === 'master') { - core.exportVariable('server_ref', 'master'); - console.log('Setting server_ref to master'); - } else { - const regex = /^stable(\d+)$/ - const match = baseRef.match(regex) - if (match) { - core.exportVariable('server_ref', match[0]); - console.log('Setting server_ref to ' + match[0]); - } else { - console.log('Not based on master/main/stable*, so skipping outdated 3rdparty check'); - } - } - - - name: Last 3rdparty commit on target branch - if: ${{ env.server_ref != '' }} - id: target - run: | - echo "commit=$(git ls-remote https://github.com/nextcloud/3rdparty refs/heads/${{ env.server_ref }} | awk '{ print $1}')" >> "$GITHUB_OUTPUT" - - - name: Compare if 3rdparty commits are different - if: ${{ env.server_ref != '' }} - run: | - echo '3rdparty/ seems to not point to the last commit of the dedicated branch:' - echo 'Branch has: ${{ steps.actual.outputs.commit }}' - echo '${{ env.server_ref }} has: ${{ steps.target.outputs.commit }}' - - - name: Fail if 3rdparty commits are different - if: ${{ env.server_ref != '' && steps.changes.outputs.src != 'false' && steps.actual.outputs.commit != steps.target.outputs.commit }} - run: | - exit 1 --- a/.github/workflows/block-unconventional-commits.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Block unconventional commits - -on: - pull_request: - types: [opened, ready_for_review, reopened, synchronize] - -permissions: - contents: read - -concurrency: - group: block-unconventional-commits-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - block-unconventional-commits: - name: Block unconventional commits - - runs-on: ubuntu-latest-low - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: webiny/action-conventional-commits@7f91b1595ca1951cdb671ddc9f07a49081ec5b69 # v1.4.2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} --- a/.github/workflows/bug-report-labeler.yml +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Auto-label bug reports -on: - issues: - types: [opened] - -jobs: - add-version-label: - if: contains(github.event.issue.title, '[Bug]') - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Extract version number and apply label - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const body = context.payload.issue.body || ''; - const normalizedBody = body.replace(/\r\n?/g, '\n'); - let label = ''; - - // Extract Nextcloud Server version number from a block like: - // ### Nextcloud Server version - // 32 - const versionMatch = normalizedBody.match(/### Nextcloud Server version\s*\n+([0-9]{1,3})\b/); - let nextcloudVersion = null; - if (versionMatch) { - nextcloudVersion = parseInt(versionMatch[1], 10); - label = nextcloudVersion + '-feedback'; - } - - if (label) { - try { - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [label] - }); - } catch (error) { - core.setFailed(`Failed to add label "${label}": ${error.message || error}`); - } - } --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: "CodeQL Advanced" - -on: - push: - branches: [ "master", "stable*" ] - pull_request: - branches: [ "master", "stable*" ] - schedule: - - cron: '28 18 * * 1' - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Initialize CodeQL - uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - config-file: ./.github/codeql-config.yml - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 - with: - category: "/language:${{matrix.language}}" --- a/.github/workflows/command-compile.yml +++ /dev/null @@ -1,235 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Compile Command -on: - issue_comment: - types: [created] - -permissions: - contents: read - -jobs: - init: - runs-on: ubuntu-latest-low - - # On pull requests and if the comment starts with `/compile` - if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/compile') - - outputs: - git_path: ${{ steps.git-path.outputs.path }} - arg1: ${{ steps.command.outputs.arg1 }} - arg2: ${{ steps.command.outputs.arg2 }} - head_ref: ${{ steps.comment-branch.outputs.head_ref }} - base_ref: ${{ steps.comment-branch.outputs.base_ref }} - - steps: - - name: Get repository from pull request comment - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - id: get-repository - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const pull = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number - }); - - const repositoryName = pull.data.head?.repo?.full_name - console.log(repositoryName) - return repositoryName - - - name: Disabled on forks - if: ${{ fromJSON(steps.get-repository.outputs.result) != github.repository }} - run: | - echo 'Can not execute /compile on forks' - exit 1 - - - name: Check actor permission - uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 - with: - require: write - - - name: Add reaction on start - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '+1' - - - name: Parse command - uses: skjnldsv/parse-command-comment@5c955203c52424151e6d0e58fb9de8a9f6a605a1 # v3.1 - id: command - - # Init path depending on which command is run - - name: Init path - id: git-path - run: | - if ${{ startsWith(steps.command.outputs.arg1, '/') }}; then - echo "path=${{steps.command.outputs.arg1}}" >> $GITHUB_OUTPUT - else - echo "path=${{steps.command.outputs.arg2}}" >> $GITHUB_OUTPUT - fi - - - name: Init branch - uses: xt0rted/pull-request-comment-branch@e8b8daa837e8ea7331c0003c9c316a64c6d8b0b1 # v3.0.0 - id: comment-branch - - - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: failure() - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '-1' - - process: - runs-on: ubuntu-latest - needs: init - - steps: - - name: Restore cached git repository - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: .git - key: git-repo - - - name: Checkout ${{ needs.init.outputs.head_ref }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - token: ${{ secrets.COMMAND_BOT_PAT }} - fetch-depth: 0 - ref: ${{ needs.init.outputs.head_ref }} - - - name: Setup git - run: | - git config --local user.email 'nextcloud-command@users.noreply.github.com' - git config --local user.name 'nextcloud-command' - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: package-engines-versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.package-engines-versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.package-engines-versions.outputs.nodeVersion }} - cache: npm - - - name: Set up npm ${{ steps.package-engines-versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.package-engines-versions.outputs.npmVersion }}' - - - name: Rebase to ${{ needs.init.outputs.base_ref }} - if: ${{ contains(needs.init.outputs.arg1, 'rebase') }} - env: - BASE_REF: ${{ needs.init.outputs.base_ref }} - run: | - git fetch origin "${BASE_REF}:${BASE_REF}" - - # Start the rebase - git rebase "origin/${BASE_REF}" || { - # Handle rebase conflicts in a loop - while [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; do - echo "Handling rebase conflict..." - - # Remove and checkout /dist and /js folders from the base branch - if [ -d "dist" ]; then - rm -rf dist - git checkout "origin/${BASE_REF}" -- dist/ 2>/dev/null || echo "No dist folder in base branch" - fi - if [ -d "js" ]; then - rm -rf js - git checkout "origin/${BASE_REF}" -- js/ 2>/dev/null || echo "No js folder in base branch" - fi - - # Stage all changes - git add . - - # Check if there are any changes after resolving conflicts - if git diff --cached --quiet; then - echo "No changes after conflict resolution, skipping commit" - git rebase --skip - else - echo "Changes found, continuing rebase without editing commit message" - git -c core.editor=true rebase --continue - fi - - # Break if rebase is complete - if [ ! -d .git/rebase-merge ] && [ ! -d .git/rebase-apply ]; then - break - fi - done - } - - - name: Install dependencies & build - env: - CYPRESS_INSTALL_BINARY: 0 - PUPPETEER_SKIP_DOWNLOAD: true - run: | - npm ci - npm run build --if-present - - - name: Commit default - if: ${{ !contains(needs.init.outputs.arg1, 'fixup') && !contains(needs.init.outputs.arg1, 'amend') }} - env: - GIT_PATH: ${{ needs.init.outputs.git_path }} - run: | - git add "${GITHUB_WORKSPACE}${GIT_PATH}" - git commit --signoff -m 'chore(assets): Recompile assets' - - - name: Commit fixup - if: ${{ contains(needs.init.outputs.arg1, 'fixup') }} - env: - GIT_PATH: ${{ needs.init.outputs.git_path }} - run: | - git add "${GITHUB_WORKSPACE}${GIT_PATH}" - git commit --fixup=HEAD --signoff - - - name: Commit amend - if: ${{ contains(needs.init.outputs.arg1, 'amend') }} - env: - GIT_PATH: ${{ needs.init.outputs.git_path }} - run: | - git add "${GITHUB_WORKSPACE}${GIT_PATH}" - git commit --amend --no-edit --signoff - # Remove any [skip ci] from the amended commit - git commit --amend -m "$(git log -1 --format='%B' | sed '/\[skip ci\]/d')" - - - name: Push normally - if: ${{ !contains(needs.init.outputs.arg1, 'rebase') && !contains(needs.init.outputs.arg1, 'amend') }} - env: - HEAD_REF: ${{ needs.init.outputs.head_ref }} - BOT_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} # zizmor: ignore[secrets-outside-env] - run: | - git remote set-url origin "https://x-access-token:${BOT_TOKEN}@github.com/${{ github.repository }}.git" - git push origin "$HEAD_REF" - - - name: Force push - if: ${{ contains(needs.init.outputs.arg1, 'rebase') || contains(needs.init.outputs.arg1, 'amend') }} - env: - HEAD_REF: ${{ needs.init.outputs.head_ref }} - BOT_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} # zizmor: ignore[secrets-outside-env] - run: | - git remote set-url origin "https://x-access-token:${BOT_TOKEN}@github.com/${{ github.repository }}.git" - git push --force-with-lease origin "$HEAD_REF" - - - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: failure() - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '-1' --- a/.github/workflows/command-pull-3rdparty.yml +++ /dev/null @@ -1,111 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Update 3rdparty command - -on: - issue_comment: - types: created - -permissions: - contents: read - -jobs: - rebase: - runs-on: ubuntu-latest - permissions: - contents: none - pull-requests: read - - # On pull requests and if the comment starts with `/update-3rdparty` - if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/update-3rdparty') - - steps: - - name: Add reaction on start - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '+1' - - # issue_comment events carry no pull_request context in their payload, so we - # must fetch the PR via the API. This also gives us base.ref for free, avoiding - # a second API call. The GITHUB_TOKEN needs pull-requests:read (granted above). - - name: Get pull request metadata - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - id: get-pr - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const pull = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - core.setOutput('head_repo', pull.data.head.repo?.full_name ?? ''); - core.setOutput('base_ref', pull.data.base.ref); - - - name: Disabled on forks - if: steps.get-pr.outputs.head_repo != github.repository - run: | - echo 'Can not execute /update-3rdparty on forks' - exit 1 - - - name: Init branch - uses: xt0rted/pull-request-comment-branch@e8b8daa837e8ea7331c0003c9c316a64c6d8b0b1 # v3.0.0 - id: comment-branch - - - name: Checkout ${{ steps.comment-branch.outputs.head_ref }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - fetch-depth: 0 - token: ${{ secrets.COMMAND_BOT_PAT }} - ref: ${{ steps.comment-branch.outputs.head_ref }} - - - name: Register server reference to fallback to master branch - run: | - base_ref="${{ steps.get-pr.outputs.base_ref }}" - if [[ "$base_ref" == "main" || "$base_ref" == "master" ]]; then - echo "server_ref=master" >> "$GITHUB_ENV" - echo "Setting server_ref to master" - elif [[ "$base_ref" =~ ^stable[0-9]+$ ]]; then - echo "server_ref=$base_ref" >> "$GITHUB_ENV" - echo "Setting server_ref to $base_ref" - else - echo "Not based on master/main/stable*, so skipping pull 3rdparty command" - fi - - - name: Setup git - run: | - git config --local user.email 'nextcloud-command@users.noreply.github.com' - git config --local user.name 'nextcloud-command' - - - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: ${{ env.server_ref == '' }} - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '-1' - - - name: Pull 3rdparty - if: ${{ env.server_ref != '' }} - run: git submodule foreach 'if [ "$sm_path" == "3rdparty" ]; then git pull origin '"'"'${{ env.server_ref }}'"'"'; fi' - - - name: Commit and push changes - if: ${{ env.server_ref != '' }} - run: | - git add 3rdparty - git commit -s -m 'Update submodule 3rdparty to latest ${{ env.server_ref }}' - git push - - - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: failure() - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - repository: ${{ github.event.repository.full_name }} - comment-id: ${{ github.event.comment.id }} - reactions: '-1' --- a/.github/workflows/dependabot-approve-merge.yml +++ /dev/null @@ -1,101 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Auto approve Dependabot PRs - -on: - pull_request_target: # zizmor: ignore[dangerous-triggers] - branches: - - main - - master - - stable* - -permissions: - contents: read - -concurrency: - group: dependabot-approve-merge-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - auto-approve-merge: - if: github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest-low - env: - # env variable for maintainers: 'true' allows to auto-merge 1.0.2 -> 2.0.0 - ALLOW_MAJOR: false - # env variable for maintainers: 'true' allows to auto-merge 1.0.2 -> 1.1.0 - ALLOW_MINOR: true - # env variable for maintainers: RegExp string to ignore some dependencies from auto-approve and auto-merge - IGNORE_PATTERN: '' - permissions: - # for auto-approve step to work - pull-requests: write - # for alexwilson/enable-github-automerge-action to approve PRs - contents: write - - steps: - - name: Disabled on forks - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - run: | - echo 'Can not approve PRs from forks' - exit 1 - - - uses: mdecoleman/pr-branch-name@55795d86b4566d300d237883103f052125cc7508 # v3.0.0 - id: branchname - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Dependabot metadata - id: metadata - if: startsWith(steps.branchname.outputs.branch, 'dependabot/') - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Check for ignored dependencies in the PR - id: validate - if: startsWith(steps.branchname.outputs.branch, 'dependabot/') - env: - IGNORE_PATTERN: ${{ env.IGNORE_PATTERN }} - DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }} - run: | - if [[ -z ${IGNORE_PATTERN} ]]; then - echo "ignore=false" >> "$GITHUB_OUTPUT" - elif [[ -z ${DEPENDENCY_NAMES} ]]; then - echo "ignore=false" >> "$GITHUB_OUTPUT" - elif [[ ${DEPENDENCY_NAMES} =~ ${IGNORE_PATTERN} ]]; then - echo "ignore=true" >> "$GITHUB_OUTPUT" - fi - - - name: GitHub actions bot approve - id: auto_approve - if: ${{ - startsWith(steps.branchname.outputs.branch, 'dependabot/') - && steps.validate.outputs.ignore != 'true' - }} - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Enable GitHub auto merge - - name: Auto merge - uses: alexwilson/enable-github-automerge-action@2c32e18a76e0726ffe7a573bfff2d42a20885126 # 3.0.0 - if: ${{ - startsWith(steps.branchname.outputs.branch, 'dependabot/') - && steps.auto_approve.conclusion == 'success' - && (github.event.action == 'opened' || github.event.action == 'reopened') - && ( - steps.metadata.outputs.update-type == 'version-update:semver-patch' - || (fromJSON(env.ALLOW_MINOR) && steps.metadata.outputs.update-type == 'version-update:semver-minor') - || (fromJSON(env.ALLOW_MAJOR) && steps.metadata.outputs.update-type == 'version-update:semver-major') - ) - }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} --- a/.github/workflows/files-external.yml +++ /dev/null @@ -1,248 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: PHPUnit files_external -on: - workflow_call: - -permissions: - contents: read - -jobs: - files-external: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - backend: ['generic', 'proftpd', 'vsftpd', 'pure-ftpd', 's3-minio', 's3-localstack', 'openssh', 'smb', 'webdav'] - - name: php${{ matrix.php-versions }}-${{ matrix.backend }} - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - # Storage backends run as plain containers instead of job services so that a - # single matrix job can pick the one it needs. - - name: Start storage backend - env: - BACKEND: ${{ matrix.backend }} - run: | - case "$BACKEND" in - proftpd|vsftpd|pure-ftpd) - sudo mkdir /tmp/ftp - sudo chmod -R 0777 /tmp/ftp - ;; - esac - - case "$BACKEND" in - proftpd) - echo '$6$Q7V2n3q2GRVv5YeQ$/AhLu07H76Asojy7bxGXMY1caKLAbp5Vt82LOZYMkD/8uDzyMAEXwk0c1Bdz1DkBsk2Vh/9SF130mOPavRGMo.' > /tmp/secret.txt - echo 'FTP_ROOT=/home/test' >> $GITHUB_ENV - docker run --name ftp -d --net host -e PASV_ADDRESS=127.0.0.1 -e FTPUSER_NAME=test -v /tmp/secret.txt:/run/secrets/ftp-user-password-secret -v /tmp/ftp:/home/test instantlinux/proftpd - ;; - vsftpd) - docker run --name ftp -d --net host -e FTP_USER=test -e FTP_PASS=test -e PASV_ADDRESS=127.0.0.1 -v /tmp/ftp:/home/vsftpd/test fauria/vsftpd - ;; - pure-ftpd) - docker run --name ftp -d --net host -e PUBLICHOST=localhost -e FTP_USER_NAME=test -e FTP_USER_PASS=test -e FTP_USER_HOME=/home/test -v /tmp/ftp:/home/test -v /tmp/ftp:/etc/pure-ftpd/passwd stilliard/pure-ftpd - ;; - openssh) - sudo mkdir /tmp/sftp - sudo chmod -R 0777 /tmp/sftp - docker run --name sftp -d -p 2222:22 -v /tmp/sftp:/home/test atmoz/sftp 'test:test:::data' - ;; - s3-minio) - docker run --name minio -d -p 9000:9000 -e MINIO_ROOT_USER=nextcloud -e MINIO_ROOT_PASSWORD=bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= -e MINIO_DEFAULT_BUCKETS=nextcloud bitnami/minio@sha256:50cec18ac4184af4671a78aedd5554942c8ae105d51a465fa82037949046da01 # v2025.4.22 - ;; - s3-localstack) - docker run --name localstack -d -p 4566:4566 -e SERVICES=s3 -e DEBUG=1 localstack/localstack@sha256:9d4253786e0effe974d77fe3c390358391a56090a4fff83b4600d8a64404d95d # v4.5.0 - ;; - smb) - docker run --name samba -d -p 445:445 -e ACCOUNT_test=test -e UID_test=1000 -e 'SAMBA_VOLUME_CONFIG_test=[public]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes' ghcr.io/servercontainers/samba:smbd-only-a3.18.0-s4.18.2-r0 - ;; - webdav) - docker run --name apache -d -p 8081:80 ghcr.io/nextcloud/continuous-integration-webdav-apache:latest - ;; - esac - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite${{ matrix.backend == 'smb' && ', smbclient' || '' }} - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up smbclient - # This is needed as icewind/smb php library for notify - if: ${{ matrix.backend == 'smb' }} - run: | - sudo apt-get update - sudo apt-get install -y smbclient - - - name: Set up Nextcloud - env: - BACKEND: ${{ matrix.backend }} - OBJECT_STORE_KEY: nextcloud - OBJECT_STORE_SECRET: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= - run: | - composer install - mkdir -p data - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password - ./occ app:enable --force files_external - - case "$BACKEND" in - smb|webdav) - ./occ config:system:set --value true --type boolean allow_local_remote_servers - ;; - esac - - case "$BACKEND" in - generic) - echo 'TEST_ARGS=' >> $GITHUB_ENV - ;; - proftpd|vsftpd|pure-ftpd) - echo " true,'host' => 'localhost','user' => 'test','password' => 'test', 'root' => '$FTP_ROOT'];" > apps/files_external/tests/config.ftp.php - echo 'TEST_ARGS=apps/files_external/tests/Storage/FtpTest.php' >> $GITHUB_ENV - ;; - openssh) - echo " true, 'host' => 'localhost:2222','user' => 'test','password' => 'test', 'root' => 'data'];" > apps/files_external/tests/config.sftp.php - echo 'TEST_ARGS=apps/files_external/tests/Storage/SftpTest.php' >> $GITHUB_ENV - ;; - s3-minio) - echo " true, 'minio' => true, 'secret' => 'actually-not-secret', 'passwordsalt' => 'actually-not-secret', 'hostname' => 'localhost','key' => '$OBJECT_STORE_KEY','secret' => '$OBJECT_STORE_SECRET', 'bucket' => 'bucket', 'port' => 9000, 'use_ssl' => false, 'autocreate' => true, 'use_path_style' => true];" > apps/files_external/tests/config.amazons3.php - echo 'TEST_ARGS=--group S3 apps/files_external/tests/Storage' >> $GITHUB_ENV - ;; - s3-localstack) - echo " true, 'localstack' => true, 'hostname' => 'localhost','key' => 'ignored','secret' => 'ignored', 'bucket' => 'bucket', 'port' => 4566, 'use_ssl' => false, 'autocreate' => true, 'use_path_style' => true];" > apps/files_external/tests/config.amazons3.php - echo 'TEST_ARGS=--group S3 apps/files_external/tests/Storage' >> $GITHUB_ENV - ;; - smb) - echo "true, 'host'=>'localhost', 'user'=>'test', 'password'=>'test', 'root'=>'', 'share'=>'public'];" > apps/files_external/tests/config.smb.php - echo 'TEST_ARGS=apps/files_external/tests/Storage/SmbTest.php' >> $GITHUB_ENV - ;; - webdav) - echo " true, 'host' => 'localhost:8081/webdav/', 'user' => 'test', 'password'=>'pass', 'root' => '', 'wait' => 0];" > apps/files_external/tests/config.webdav.php - echo 'TEST_ARGS=apps/files_external/tests/Storage/WebdavTest.php' >> $GITHUB_ENV - ;; - esac - - - name: Wait for storage backend - env: - BACKEND: ${{ matrix.backend }} - run: | - case "$BACKEND" in - proftpd|vsftpd|pure-ftpd) - php -r "var_dump(file_put_contents('ftp://test:test@localhost$FTP_ROOT/ftp.txt', 'asd'));" - php -r "var_dump(file_get_contents('ftp://test:test@localhost$FTP_ROOT/ftp.txt'));" - php -r "var_dump(mkdir('ftp://test:test@localhost$FTP_ROOT/asdads'));" - ls -l /tmp/ftp - [ -f /tmp/ftp/ftp.txt ] - ;; - s3-minio) - curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready - ;; - smb) - apps/files_external/tests/env/wait-for-connection 127.0.0.1 445 60 - ;; - webdav) - sleep 5 - curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://test:pass@localhost:8081/webdav/ - ;; - esac - - - name: PHPUnit - run: composer run test:files_external -- --log-junit junit.xml $TEST_ARGS - - - name: Nextcloud logs - if: always() - run: | - if [ -f data/nextcloud.log ]; then cat data/nextcloud.log; fi - - - name: Storage backend logs - if: always() - run: | - docker ps -a - docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done - - files-external-smb-kerberos: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - name: smb-kerberos-sso - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Checkout user_saml - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: nextcloud/user_saml - path: apps/user_saml - - - name: Install user_saml - run: | - cd apps/user_saml - composer i - cd ../.. - - - name: Pull images - run: | - docker pull ghcr.io/icewind1991/samba-krb-test-dc - docker pull ghcr.io/icewind1991/samba-krb-test-apache-gssapi:8.4 - docker pull ghcr.io/icewind1991/samba-krb-test-client - docker tag ghcr.io/icewind1991/samba-krb-test-dc icewind1991/samba-krb-test-dc - docker tag ghcr.io/icewind1991/samba-krb-test-apache-gssapi:8.4 icewind1991/samba-krb-test-apache-gssapi - docker tag ghcr.io/icewind1991/samba-krb-test-client icewind1991/samba-krb-test-client - - - name: Setup AD-DC - run: | - DC_IP=$(apps/files_external/tests/sso-setup/start-dc.sh) - sleep 1 - apps/files_external/tests/sso-setup/start-apache.sh $DC_IP $PWD - echo "DC_IP=$DC_IP" >> $GITHUB_ENV - - - name: Set up Nextcloud - run: | - apps/files_external/tests/sso-setup/setup-sso-nc.sh - - - name: Test SSO - run: | - apps/files_external/tests/sso-setup/test-sso-smb.sh ${{ env.DC_IP }} - - - name: Show logs DC - if: always() - run: | - docker logs dc - echo "------------" - docker exec dc cat /var/log/samba/log.samba - - - name: Show logs Apache - if: always() - run: | - docker logs apache - - - name: Show logs - if: always() - run: | - FILEPATH=$(docker exec --user 33 apache ./occ log:file | grep "Log file:" | cut -d' ' -f3) - echo "$FILEPATH:" - docker exec --user 33 apache cat $FILEPATH --- a/.github/workflows/fixup.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Block fixup and squash commits - -on: - pull_request: - types: [opened, ready_for_review, reopened, synchronize] - -permissions: - contents: read - -concurrency: - group: fixup-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - commit-message-check: - if: github.event.pull_request.draft == false - - permissions: - pull-requests: write - name: Block fixup and squash commits - - runs-on: ubuntu-latest-low - - steps: - - name: Run check - uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} --- a/.github/workflows/integration-dav.yml +++ /dev/null @@ -1,129 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: DAV integration tests -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: integration-caldav-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/*.php' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - 'build/integration/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - integration-caldav: - runs-on: ubuntu-latest - needs: changes - - if: needs.changes.outputs.src != 'false' && github.repository_owner != 'nextcloud-gmbh' - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - endpoint: ['old', 'new'] - service: ['CalDAV', 'CardDAV'] - - name: ${{ matrix.service }} (${{ matrix.endpoint }} endpoint) php${{ matrix.php-versions }} - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Python - uses: LizardByte/actions/actions/setup_python@d0ae7f82215a479fe2b74f4088c53ee6460513dd # v2026.728.214955 - with: - python-version: '2.7' - - - name: Set up CalDAVTester - run: | - git clone --depth=1 https://github.com/apple/ccs-caldavtester.git CalDAVTester - git clone --depth=1 https://github.com/apple/ccs-pycalendar.git pycalendar - - - name: Set up Nextcloud - run: | - mkdir data - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - # disable the trashbin, so recurrent deletion of the same object works - ./occ config:app:set dav calendarRetentionObligation --value=0 - # Prepare users - OC_PASS=user01 ./occ user:add --password-from-env user01 - OC_PASS=user02 ./occ user:add --password-from-env user02 - # Prepare calendars - ./occ dav:create-calendar user01 calendar - ./occ dav:create-calendar user01 shared - ./occ dav:create-calendar user02 calendar - # Prepare address books - ./occ dav:create-addressbook user01 addressbook - ./occ dav:create-addressbook user02 addressbook - - - name: Run Nextcloud - run: | - php -S localhost:8888 & - - - name: Run CalDAVTester - run: | - cp "apps/dav/tests/testsuits/caldavtest/serverinfo-${{ matrix.endpoint }}${{ matrix.endpoint == 'old' && (matrix.service == 'CardDAV' && '-carddav' || '-caldav') || '' }}-endpoint.xml" "apps/dav/tests/testsuits/caldavtest/serverinfo.xml" - pushd CalDAVTester - PYTHONPATH="../pycalendar/src" python testcaldav.py --print-details-onfail --basedir "../apps/dav/tests/testsuits/caldavtest" -o cdt.txt \ - "${{ matrix.service }}/current-user-principal.xml" \ - "${{ matrix.service }}/sync-report.xml" \ - ${{ matrix.endpoint == 'new' && format('{0}/sharing-{1}.xml', matrix.service, matrix.service == 'CalDAV' && 'calendars' || 'addressbooks') || ';' }} - popd - - - name: Print Nextcloud logs - if: always() - run: | - cat data/nextcloud.log - - caldav-integration-summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, integration-caldav] - - if: always() - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.integration-caldav.result != 'success' }}; then exit 1; fi --- a/.github/workflows/integration-litmus.yml +++ /dev/null @@ -1,116 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Litmus integration tests -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: integration-litmus-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/*.php' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - 'build/integration/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - integration-litmus: - runs-on: ubuntu-latest - needs: changes - - if: needs.changes.outputs.src != 'false' && github.repository_owner != 'nextcloud-gmbh' - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - endpoint: ['webdav', 'dav'] - - name: Litmus WebDAV ${{ matrix.endpoint }} - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Nextcloud - run: | - mkdir data - ./occ maintenance:install \ - --verbose \ - --database=sqlite \ - --database-name=nextcloud \ - --database-user=root \ - --database-pass=rootpassword \ - --admin-user admin \ - --admin-pass admin - ./occ config:system:set trusted_domains 2 --value=host.docker.internal:8080 - - - name: Run Nextcloud - run: | - php -S 0.0.0.0:8080 & - - - name: Run Litmus test - run: | - docker run \ - --rm \ - --add-host=host.docker.internal:host-gateway \ - ghcr.io/nextcloud/continuous-integration-litmus-php8.3:latest \ - bash -c '\ - cd /tmp/litmus/litmus-0.13; - make URL=http://host.docker.internal:8080/remote.php/${{ matrix.endpoint }}${{ matrix.endpoint == 'dav' && '/files/admin' || ''}} CREDS="admin admin" TESTS="basic copymove props largefile" check; - status=$?; - cat debug.log; - exit $status;' - - - name: Print Nextcloud logs - if: always() - run: cat data/nextcloud.log - - integration-litmus-summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, integration-litmus] - - if: always() - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.integration-litmus.result != 'success' }}; then exit 1; fi --- a/.github/workflows/integration-s3-primary.yml +++ /dev/null @@ -1,82 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: S3 primary storage integration tests -on: - workflow_call: - -permissions: - contents: read - -jobs: - integration-s3-primary: - runs-on: ubuntu-latest - - if: github.repository_owner != 'nextcloud-gmbh' - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - key: ['objectstore', 'objectstore_multibucket'] - - name: php${{ matrix.php-versions }}-${{ matrix.key }}-minio - - services: - redis: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - ports: - - 6379:6379/tcp - minio: - image: bitnami/minio@sha256:50cec18ac4184af4671a78aedd5554942c8ae105d51a465fa82037949046da01 # v2025.4.22 - env: - MINIO_ROOT_USER: nextcloud - MINIO_ROOT_PASSWORD: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= - MINIO_DEFAULT_BUCKETS: nextcloud - ports: - - "9000:9000" - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Wait for S3 - run: | - curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready - - - name: Set up Nextcloud - run: | - composer install - mkdir data - echo ' ["class" => "OC\Files\ObjectStore\S3", "arguments" => ["bucket" => "nextcloud", "autocreate" => true, "key" => "nextcloud", "secret" => "bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ=", "hostname" => "localhost", "port" => 9000, "use_ssl" => false, "use_path_style" => true, "uploadPartSize" => 52428800]]];' > config/config.php - echo ' ["host" => "localhost", "port" => 6379], "memcache.local" => "\OC\Memcache\Redis", "memcache.distributed" => "\OC\Memcache\Redis"];' > config/redis.config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f index.php - - - name: Integration - run: | - cd build/integration - bash run.sh --tags "~@failure-s3" dav_features/webdav-related.feature - - - name: S3 logs - if: always() - run: | - cat data/nextcloud.log - docker ps -a - docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done --- a/.github/workflows/integration-sqlite.yml +++ /dev/null @@ -1,191 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Integration sqlite - -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: integration-sqlite-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/*.php' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - 'build/integration/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - 'core/shipped.json' - - integration-sqlite: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - strategy: - fail-fast: false - matrix: - test-suite: - - 'capabilities_features' - - 'collaboration_features' - - 'comments_features' - - '--tags ~@requires-s3 dav_features' - - 'encryption_features' - - 'features' - - 'federation_features' - - '--tags ~@large files_features' - - 'filesdrop_features' - - 'file_conversions' - - 'files_reminders' - - 'openldap_features' - - 'openldap_numerical_features' - - 'ldap_features' - - 'routing_features' - - 'setup_features' - - 'sharees_features' - - 'sharing_features' - - 'theming_features' - - 'videoverification_features' - - 'guests_features' - - php-versions: ['8.3', '8.5'] - guests-versions: ['main'] - spreed-versions: ['stable35'] - activity-versions: ['stable35'] - - services: - redis: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - ports: - - 6379:6379/tcp - openldap: - image: ghcr.io/nextcloud/continuous-integration-openldap:openldap-8 # zizmor: ignore[unpinned-images] - ports: - - 389:389 - - 636:636 - env: - SLAPD_DOMAIN: nextcloud.ci - SLAPD_ORGANIZATION: Nextcloud - SLAPD_PASSWORD: admin - SLAPD_ADDITIONAL_MODULES: memberof - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Checkout Talk app - if: ${{ matrix.test-suite == 'videoverification_features' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: nextcloud/spreed - path: apps/spreed - ref: ${{ matrix.spreed-versions }} - - - name: Checkout Guests app - if: ${{ matrix.test-suite == 'guests_features' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: nextcloud/guests - path: apps/guests - ref: ${{ matrix.guests-versions }} - - - name: Checkout Activity app - if: ${{ matrix.test-suite == 'sharing_features' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: nextcloud/activity - path: apps/activity - ref: ${{ matrix.activity-versions }} - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, imagick, intl, json, ldap, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: | - composer install - - - name: Set up Talk dependencies - if: ${{ matrix.test-suite == 'videoverification_features' }} - working-directory: apps/spreed - run: composer i --no-dev - - - name: Set up Nextcloud - run: | - mkdir data - ./occ maintenance:install --verbose ${{ contains(matrix.test-suite,'ldap') && '--data-dir=/dev/shm/nc_int' || '' }} --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - ./occ config:system:set hashing_default_password --value=true --type=boolean - - - name: Configure caching - if: ${{ contains(matrix.test-suite,'ldap') }} - run: | - ./occ config:system:set redis host --value=localhost - ./occ config:system:set redis port --value=6379 --type=integer - ./occ config:system:set redis timeout --value=0 --type=integer - ./occ config:system:set memcache.local --value='\OC\Memcache\Redis' - ./occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' - - - name: Run integration - working-directory: build/integration - env: - LDAP_HOST: localhost - run: bash run.sh ${{ matrix.test-suite }} no-tail-log - - - name: Print logs - if: always() - run: | - cat $(./occ log:file |grep "Log file"|cut -d" " -f3) - docker ps -a - docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, integration-sqlite] - - if: always() - - name: integration-sqlite-summary - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.integration-sqlite.result != 'success' }}; then exit 1; fi --- a/.github/workflows/lint-eslint.yml +++ /dev/null @@ -1,100 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Lint eslint - -on: pull_request - -permissions: - contents: read - -concurrency: - group: lint-eslint-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '**/src/**' - - '**/appinfo/info.xml' - - 'package.json' - - 'package-lock.json' - - 'tsconfig.json' - - '.eslintrc.*' - - '.eslintignore' - - '**.js' - - '**.ts' - - '**.vue' - - lint: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: NPM lint - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Install dependencies - env: - CYPRESS_INSTALL_BINARY: 0 - PUPPETEER_SKIP_DOWNLOAD: true - run: npm ci - - - name: Lint - run: npm run lint - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, lint] - - if: always() - - # This is the summary, we just avoid to rename it so that branch protection rules still match - name: eslint - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi --- a/.github/workflows/lint-php-cs.yml +++ /dev/null @@ -1,88 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Lint php-cs - -on: pull_request - -permissions: - contents: read - -concurrency: - group: lint-php-cs-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - '**.php' - - lint: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: php-cs - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: 8.3 - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install dependencies - run: composer i - - - name: Lint - run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, lint] - - if: always() - - # This is the summary, we just avoid to rename it so that branch protection rules still match - name: php-cs - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi --- a/.github/workflows/lint-php.yml +++ /dev/null @@ -1,86 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Lint php - -on: pull_request - -permissions: - contents: read - -concurrency: - group: lint-php-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - outputs: - src: ${{ steps.changes.outputs.src}} - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - '**.php' - - lint: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - strategy: - matrix: - php-versions: ['8.3', '8.5'] - - name: php-lint - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Lint - run: composer run lint - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, lint] - - if: always() - - name: php-lint-summary - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi --- a/.github/workflows/lint-stylelint.yml +++ /dev/null @@ -1,96 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Lint stylelint - -on: pull_request - -permissions: - contents: read - -concurrency: - group: lint-stylelint-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - src: ${{ steps.changes.outputs.src }} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '**/src/**' - - '**/appinfo/info.xml' - - 'package.json' - - 'package-lock.json' - - '**.css' - - '**.scss' - - '**.vue' - - lint: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: stylelint - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Install dependencies - env: - CYPRESS_INSTALL_BINARY: 0 - run: npm ci - - - name: Lint - run: npm run stylelint - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, lint] - - if: always() - - # This is the summary, we just avoid to rename it so that branch protection rules still match - name: stylelint - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi --- a/.github/workflows/node-dist-unchanged.yml +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Check node dist files - -on: - pull_request: - - -jobs: - changed_files: - runs-on: ubuntu-latest-low - name: Check node dist files - permissions: - pull-requests: read - - steps: - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 - with: - files: dist/** - - - name: Run step if any file(s) in the dist folder change - if: steps.changed-files.outputs.any_changed == 'true' - run: | - echo "One or more files in the dist folder has changed. Do NOT commit files in there as they will be generated automatically once a pull request is merged" - echo "List all the files that have changed: ${{ steps.changed-files.outputs.all_changed_files }}" - exit 1 --- a/.github/workflows/node-test.yml +++ /dev/null @@ -1,106 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Node tests - -on: - pull_request: - schedule: - - cron: "5 2 * * *" - -permissions: - contents: read - -concurrency: - group: node-tests-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '**/__tests__/**' - - '**/__mocks__/**' - - 'apps/*/src/**' - - 'apps/*/appinfo/info.xml' - - 'core/src/**' - - 'package.json' - - '**/package-lock.json' - - 'tsconfig.json' - - '**.js' - - '**.ts' - - '**.vue' - - test: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Install dependencies - env: - CYPRESS_INSTALL_BINARY: 0 - run: | - npm ci - - - name: Test and process coverage - run: npm run test:coverage - - - name: Collect coverage - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - files: ./coverage/lcov.info,./coverage/legacy/lcov.info - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, test] - - if: always() - - name: node-test-summary - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi --- a/.github/workflows/npm-audit-fix.yml +++ /dev/null @@ -1,85 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Npm audit fix and compile - -on: - workflow_dispatch: - schedule: - # At 2:30 on Sundays - - cron: '30 2 * * 0' - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - branches: - - ${{ github.event.repository.default_branch }} - - 'stable34' - - 'stable33' - - 'stable32' - - name: npm-audit-fix-${{ matrix.branches }} - - steps: - - name: Checkout - id: checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ matrix.branches }} - continue-on-error: true - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Fix npm audit - id: npm-audit - uses: nextcloud-libraries/npm-audit-action@1b1728b2b4a7a78d69de65608efcf4db0e3e42d0 # v0.2.0 - - - name: Run npm ci and npm run build - if: steps.checkout.outcome == 'success' - env: - CYPRESS_INSTALL_BINARY: 0 - run: | - npm ci - npm run build --if-present - - - name: Create Pull Request - if: steps.checkout.outcome == 'success' - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: 'fix(deps): Fix npm audit' - committer: GitHub - author: nextcloud-command - signoff: true - branch: automated/noid/${{ matrix.branches }}-fix-npm-audit - title: '[${{ matrix.branches }}] Fix npm audit' - body: ${{ steps.npm-audit.outputs.markdown }} - labels: | - dependencies - 3. to review --- a/.github/workflows/npm-build.yml +++ /dev/null @@ -1,114 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Build Javascript - -on: pull_request - -permissions: - contents: read - -concurrency: - group: node-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - 'src/**' - - 'appinfo/info.xml' - - 'package.json' - - 'package-lock.json' - - 'tsconfig.json' - - '**.js' - - '**.ts' - - '**.vue' - - build: - runs-on: ubuntu-latest - - needs: changes - if: needs.changes.outputs.src != 'false' - - name: NPM build - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' - - - name: Validate package-lock.json # See https://github.com/npm/cli/issues/4460 - run: | - npm i -g npm-package-lock-add-resolved@1.1.4 - npm-package-lock-add-resolved - git --no-pager diff --exit-code - - - name: Install dependencies & build - env: - CYPRESS_INSTALL_BINARY: 0 - PUPPETEER_SKIP_DOWNLOAD: true - run: | - npm ci - npm run build --if-present - - # Not used as we compile on CI, see update-node-dist.yml and node-dist-unchanged.yml - # - name: Check build changes - # run: | - # bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" - - # - name: Show changes on failure - # if: failure() - # run: | - # git status - # git --no-pager diff - # exit 1 # make it red to grab attention - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, build] - - if: always() - - # This is the summary, we just avoid to rename it so that branch protection rules still match - name: node - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.build.result != 'success' }}; then exit 1; fi --- a/.github/workflows/npm-build.yml.patch +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/.github/workflows/npm-build.yml b/.github/workflows/npm-build.yml -index 571057f4983..1cd1e12df5a 100644 ---- a/.github/workflows/npm-build.yml -+++ b/.github/workflows/npm-build.yml -@@ -35,17 +35,14 @@ jobs: - filters: | - src: - - '.github/workflows/**' -- - '**/src/**' -- - '**/appinfo/info.xml' -- - 'core/css/*' -- - 'core/img/**' -+ - 'src/**' -+ - 'appinfo/info.xml' - - 'package.json' -- - '**/package-lock.json' -+ - 'package-lock.json' - - 'tsconfig.json' - - '**.js' - - '**.ts' - - '**.vue' -- - 'version.php' - - build: - runs-on: ubuntu-latest -@@ -89,16 +86,17 @@ jobs: - npm ci - npm run build --if-present - -- - name: Check build changes -- run: | -- bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" -- -- - name: Show changes on failure -- if: failure() -- run: | -- git status -- git --no-pager diff -- exit 1 # make it red to grab attention -+ # Not used as we compile on CI, see update-node-dist.yml and node-dist-unchanged.yml -+ # - name: Check build changes -+ # run: | -+ # bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" -+ -+ # - name: Show changes on failure -+ # if: failure() -+ # run: | -+ # git status -+ # git --no-pager diff -+ # exit 1 # make it red to grab attention - - summary: - permissions: --- a/.github/workflows/object-storage.yml +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Object storage -on: - workflow_call: - -permissions: - contents: read - -concurrency: - group: object-storage-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - object-storage: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - backend: ['s3', 'azure', 'swift'] - - name: php${{ matrix.php-versions }}-${{ matrix.backend }} - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - # Object stores run as plain containers instead of job services so that a - # single matrix job can pick the one it needs. - - name: Start object store - env: - BACKEND: ${{ matrix.backend }} - run: | - case "$BACKEND" in - s3) - docker run --name minio -d -p 9000:9000 -e MINIO_ROOT_USER=nextcloud -e MINIO_ROOT_PASSWORD=bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= -e MINIO_DEFAULT_BUCKETS=nextcloud bitnami/minio@sha256:50cec18ac4184af4671a78aedd5554942c8ae105d51a465fa82037949046da01 # v2025.4.22 - echo 'OBJECT_STORE=s3' >> $GITHUB_ENV - echo 'OBJECT_STORE_KEY=nextcloud' >> $GITHUB_ENV - echo 'OBJECT_STORE_SECRET=bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ=' >> $GITHUB_ENV - ;; - azure) - docker run --name azurite -d -p 10000:10000 -e AZURITE_ACCOUNTS=nextcloud:bmV4dGNsb3Vk mcr.microsoft.com/azure-storage/azurite@sha256:0a47e12e3693483cef5c71f35468b91d751611f172d2f97414e9c69113b106d9 # v3.34.0 - echo 'OBJECT_STORE=azure' >> $GITHUB_ENV - echo 'OBJECT_STORE_KEY=nextcloud' >> $GITHUB_ENV - echo 'OBJECT_STORE_SECRET=bmV4dGNsb3Vk' >> $GITHUB_ENV - ;; - swift) - docker run --name swift -d -p 5000:5000 -p 8080:8080 ghcr.io/cscfi/docker-keystone-swift@sha256:e8b1ec21120ab9adc6ac6a2b98785fd273676439a8633fe898e37f2aea7e0712 - echo 'OBJECT_STORE=swift' >> $GITHUB_ENV - echo 'OBJECT_STORE_SECRET=veryfast' >> $GITHUB_ENV - ;; - esac - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Nextcloud - run: | - composer install - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password - php -f tests/enable_all.php - - - name: Wait for object store - env: - BACKEND: ${{ matrix.backend }} - run: | - case "$BACKEND" in - s3) - curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready - ;; - azure) - ready= - for _ in $(seq 1 60); do - if timeout 1 bash -c '< /dev/tcp/127.0.0.1/10000' 2>/dev/null; then ready=1; break; fi - sleep 1 - done - [ -n "$ready" ] || { echo 'azurite did not become ready'; exit 1; } - ;; - esac - - - name: PHPUnit - run: composer run test -- --group PRIMARY-${{ matrix.backend }} --log-junit junit.xml - - - name: Object store logs - if: always() - run: | - docker ps -a - docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done --- a/.github/workflows/openapi.yml +++ /dev/null @@ -1,48 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-FileCopyrightText: 2024 Arthur Schiwon -# SPDX-License-Identifier: MIT - -name: OpenAPI - -on: pull_request - -permissions: - contents: read - -concurrency: - group: openapi-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - openapi: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: ctype, curl, dom, fileinfo, gd, json, libxml, mbstring, openssl, pcntl, pdo, posix, session, simplexml, xml, xmlreader, xmlwriter, zip, zlib - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: OpenAPI checker - run: build/openapi-checker.sh --- a/.github/workflows/performance.yml +++ /dev/null @@ -1,124 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Performance testing -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: performance-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - performance-testing: - runs-on: ubuntu-latest - - # Skip entirely on fork PRs so the job result is 'skipped' rather than - # 'failure'. The profiler action uses github.event.pull_request.head.repo.clone_url - # and GITHUB_TOKEN in ways that do not work reliably from forks, and a - # clean skip is far less confusing for contributors than a mid-run error. - if: >- - github.repository_owner != 'nextcloud-gmbh' && - github.event.pull_request.head.repo.full_name == github.repository - - permissions: - pull-requests: write - - name: performance - - steps: - - - name: Checkout server before PR - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - ref: ${{ github.event.pull_request.base.ref }} - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Nextcloud - run: | - mkdir data - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password - - php -S localhost:8080 & - - name: Apply blueprint - uses: icewind1991/blueprint@00504403f76cb2a09efd0d16793575055e6f63cb # v0.1.2 - with: - blueprint: tests/blueprints/basic.toml - ref: ${{ github.event.pull_request.head.ref }} - - - name: Run before measurements - uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044 - with: - run: | - curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test - curl -s -u test:test http://localhost:8080/remote.php/dav/files/test/test.txt - curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test/many_files - curl -s -u test:test -T README.md http://localhost:8080/remote.php/dav/files/test/new_file.txt - curl -s -u test:test -X DELETE http://localhost:8080/remote.php/dav/files/test/new_file.txt - output: before.json - profiler-branch: stable35 - - - name: Apply PR # zizmor: ignore[template-injection] - run: | - git remote add pr '${{ github.event.pull_request.head.repo.clone_url }}' - git fetch pr '${{ github.event.pull_request.head.ref }}' - git checkout -b 'pr/${{ github.event.pull_request.head.ref }}' - git submodule update - - ./occ upgrade - - - name: Run after measurements - id: compare - uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044 - with: - run: | - curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test - curl -s -u test:test http://localhost:8080/remote.php/dav/files/test/test.txt - curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test/many_files - curl -s -u test:test -T README.md http://localhost:8080/remote.php/dav/files/test/new_file.txt - curl -s -u test:test -X DELETE http://localhost:8080/remote.php/dav/files/test/new_file.txt - output: after.json - profiler-branch: stable35 - compare-with: before.json - - - name: Upload profiles - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: profiles - path: | - before.json - after.json - - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - if: failure() && steps.compare.outcome == 'failure' - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - let comment = `Possible performance regression detected\n`; - comment += `
Show Output - - \`\`\` - ${{ steps.compare.outputs.compare }} - \`\`\` - -
`; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }) --- a/.github/workflows/phpunit-32bits.yml +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: PHPUnit 32bits - -on: - pull_request: - paths: - - "version.php" - - ".github/workflows/phpunit-32bits.yml" - - "tests/phpunit-autotest.xml" - - "lib/private/Snowflake/*" - - "tests/lib/Preview/PreviewMapperTest.php" - - "lib/private/Sharing" - - "tests/lib/Sharing" - workflow_dispatch: - schedule: - - cron: "15 1 * * 1-6" - -permissions: - contents: read - -concurrency: - group: phpunit-32bits-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - phpunit-32bits: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - strategy: - fail-fast: false - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up dependencies - uses: docker://ghcr.io/nextcloud/continuous-integration-php8.4-32bit@sha256:f0e6b5496ad768064c1af727f607f8bcd0eca60cf46987515040b834ed6a14aa - with: - args: /bin/sh -c " - git config --global --add safe.directory ${GITHUB_WORKSPACE} && - composer install --no-interaction" - - - name: Set up Nextcloud - uses: docker://ghcr.io/nextcloud/continuous-integration-php8.4-32bit@sha256:f0e6b5496ad768064c1af727f607f8bcd0eca60cf46987515040b834ed6a14aa - with: - args: /bin/sh -c " - mkdir data && - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=autotest --database-pass=rootpassword --admin-user admin --admin-pass admin && - php -f tests/enable_all.php" - - - name: PHPUnit - uses: docker://ghcr.io/nextcloud/continuous-integration-php8.4-32bit@sha256:f0e6b5496ad768064c1af727f607f8bcd0eca60cf46987515040b834ed6a14aa - with: - args: /bin/sh -c "composer run test -- --exclude-group PRIMARY-azure --exclude-group PRIMARY-s3 --exclude-group PRIMARY-swift --exclude-group Memcached --exclude-group Redis --exclude-group RoutingWeirdness" --- a/.github/workflows/phpunit-kvstore.yml +++ /dev/null @@ -1,102 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit key-value store - -on: - workflow_call: - -permissions: - contents: read - -env: - VALKEY_IMAGE: valkey/valkey:8 - -jobs: - phpunit-kvstore: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-versions: ["8.3", "8.5"] - # The three supported topologies for the key-value store cache - topology: ["single", "cluster", "sentinel"] - - name: KV store ${{ matrix.topology }} (PHP ${{ matrix.php-versions }}) - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, pdo_sqlite, posix, session, simplexml, sqlite, xmlreader, xmlwriter, zip, zlib - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Start Valkey (single server) - if: matrix.topology == 'single' - run: | - docker run -d --name kv-single -p 6379:6379 "${VALKEY_IMAGE}" - timeout 60 sh -c 'until docker exec kv-single valkey-cli ping | grep -q PONG; do sleep 1; done' - - - name: Start Valkey (cluster) - if: matrix.topology == 'cluster' - run: | - for port in 7000 7001 7002 7003 7004 7005; do - docker run -d --name "kv-cluster-$port" --network host "${VALKEY_IMAGE}" \ - valkey-server --port "$port" --cluster-enabled yes \ - --cluster-config-file "nodes-$port.conf" --cluster-node-timeout 5000 \ - --appendonly no --save "" - done - # Wait for every node to answer before forming the cluster - for port in 7000 7001 7002 7003 7004 7005; do - timeout 60 sh -c "until docker exec kv-cluster-$port valkey-cli -p $port ping | grep -q PONG; do sleep 1; done" - done - docker exec kv-cluster-7000 valkey-cli --cluster create \ - 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ - 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ - --cluster-replicas 1 --cluster-yes - - - name: Start Valkey (sentinel) - if: matrix.topology == 'sentinel' - run: | - docker run -d --name kv-master --network host "${VALKEY_IMAGE}" \ - valkey-server --port 6379 - docker run -d --name kv-replica --network host "${VALKEY_IMAGE}" \ - valkey-server --port 6380 --replicaof 127.0.0.1 6379 - printf 'port 26379\nsentinel monitor mymaster 127.0.0.1 6379 1\nsentinel down-after-milliseconds mymaster 5000\nsentinel failover-timeout mymaster 10000\n' > sentinel.conf - docker run -d --name kv-sentinel --network host -v "$PWD/sentinel.conf:/etc/valkey/sentinel.conf" \ - "${VALKEY_IMAGE}" valkey-sentinel /etc/valkey/sentinel.conf - timeout 60 sh -c 'until docker exec kv-sentinel valkey-cli -p 26379 ping | grep -q PONG; do sleep 1; done' - - - name: Set up dependencies - run: composer i - - - name: Set up Nextcloud - run: | - mkdir data - cp tests/kvstore-${{ matrix.topology }}.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit key-value store tests - run: composer run test -- --group KeyValueCache --log-junit junit.xml - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-mariadb.yml +++ /dev/null @@ -1,93 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit MariaDB - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-mariadb: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - - mariadb-versions: '10.11' - php-versions: '8.3' - - mariadb-versions: '12.3' - php-versions: '8.5' - - name: MariaDB ${{ matrix.mariadb-versions }} (PHP ${{ matrix.php-versions }}) - database tests - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - mariadb: - image: mariadb:${{ matrix.mariadb-versions }} - ports: - - 4444:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: oc_autotest - options: --health-cmd="${{ matrix.mariadb-versions <= 10.4 && 'mysqladmin' || 'mariadb-admin'}} ping" --health-interval 5s --health-timeout 2s --health-retries 5 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Enable ONLY_FULL_GROUP_BY MariaDB option - run: | - echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - echo 'SELECT @@sql_mode;' | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - - - name: Set up Nextcloud - env: - DB_PORT: 4444 - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit - run: composer run test:db -- --log-junit junit.xml - env: - DB_ROOT_USER: root - DB_ROOT_PASS: rootpassword --- a/.github/workflows/phpunit-memcached.yml +++ /dev/null @@ -1,72 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit memcached - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-memcached: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-versions: ["8.3", "8.5"] - - name: Memcached (PHP ${{ matrix.php-versions }}) - - services: - memcached: - image: ghcr.io/nextcloud/continuous-integration-memcached:latest # zizmor: ignore[unpinned-images] - ports: - - 11211:11211/tcp - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, memcached, openssl, pcntl, pdo_sqlite, posix, session, simplexml, sqlite, xmlreader, xmlwriter, zip, zlib - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Set up Nextcloud - run: | - mkdir data - cp tests/memcached.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit memcached tests - run: composer run test -- --group Memcached --log-junit junit.xml - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-mysql-sharding.yml +++ /dev/null @@ -1,143 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit sharding - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-mysql: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - - mysql-versions: '8.4' - php-versions: '8.3' - - mysql-versions: '9.7' - php-versions: '8.5' - - name: Sharding - MySQL ${{ matrix.mysql-versions }} (PHP ${{ matrix.php-versions }}) - database tests - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - mysql: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 4444:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: oc_autotest - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - shard1: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 5001:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: nextcloud - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - shard2: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 5002:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: nextcloud - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - shard3: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 5003:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: nextcloud - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - shard4: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 5004:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: nextcloud - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Enable ONLY_FULL_GROUP_BY MySQL option - run: | - echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - echo "SELECT @@sql_mode;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - - - name: Enable MD5 MySQL support - if: matrix.mysql-versions == '9.7' - run: | - for MYSQL_PORT in 4444 5001 5002 5003 5004; do - echo "INSTALL COMPONENT 'file://component_classic_hashing';" | mysql -h 127.0.0.1 -P $MYSQL_PORT -u root -prootpassword - done - - - name: Set up Nextcloud - env: - DB_PORT: 4444 - SHARDING: 1 - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit - run: composer run test:db -- --log-junit junit.xml - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-mysql.yml +++ /dev/null @@ -1,112 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit mysql - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-mysql: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - - mysql-versions: '8.4' - php-versions: '8.3' - partitioning: false - - mysql-versions: '9.7' - php-versions: '8.5' - partitioning: true - - name: MySQL ${{ matrix.mysql-versions }} (PHP ${{ matrix.php-versions }})${{ case(matrix.partitioning, ' Partitioning', '') }} - database tests - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - mysql: - image: mysql:${{ matrix.mysql-versions }} # zizmor: ignore[unpinned-images] - ports: - - 4444:3306/tcp - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: oc_autotest - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: oc_autotest - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Enable ONLY_FULL_GROUP_BY MySQL option - run: | - echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - echo "SELECT @@sql_mode;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword - - - name: Set up Nextcloud - env: - DB_PORT: 4444 - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: Enable partitioning for oc_filecache - if: ${{ matrix.partitioning }} - run: | - echo "alter table oc_filecache drop primary key, add primary key (fileid, storage);" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword nextcloud - echo "alter table oc_filecache partition by hash(storage) partitions 5;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword nextcloud - - - name: PHPUnit - run: composer run test:db -- --log-junit junit.xml - env: - DB_ROOT_USER: root - DB_ROOT_PASS: rootpassword - - - name: Show partition info - if: ${{ matrix.partitioning }} - run: | - echo "SELECT table_name, partition_name, table_rows FROM information_schema.partitions WHERE partition_name is not null;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword nextcloud - echo "SELECT table_rows FROM information_schema.tables WHERE table_name = 'oc_filecache';" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword nextcloud - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-nodb.yml +++ /dev/null @@ -1,76 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -# -# This is the testsuite running all non-database agnostic unit tests - -name: PHPUnit nodb - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-nodb: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - - name: No DB unit tests (PHP ${{ matrix.php-versions }}) - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: apcu, bz2, ctype, curl, dom, fileinfo, gd, iconv, imagick, intl, json, libxml, mbstring, openssl, pcntl, pdo_sqlite, posix, redis, session, simplexml, sqlite, xmlreader, xmlwriter, zip, zlib - coverage: none - ini-file: development - # Required for tests that use pcntl - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Set up Nextcloud - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit nodb testsuite - run: composer run test -- --exclude-group DB --exclude-group SLOWDB --exclude-group Memcached --exclude-group PRIMARY-swift --exclude-group PRIMARY-s3 --exclude-group PRIMARY-azure --log-junit junit.xml - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-object-store-primary.yml +++ /dev/null @@ -1,82 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: PHPUnit primary object store -on: - workflow_call: - -permissions: - contents: read - -jobs: - object-store-primary-tests-minio: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - key: ['s3', 's3-multibucket'] - - name: php${{ matrix.php-versions }}-${{ matrix.key }}-minio - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - minio: - image: bitnami/minio@sha256:50cec18ac4184af4671a78aedd5554942c8ae105d51a465fa82037949046da01 # v2025.4.22 - env: - MINIO_ROOT_USER: nextcloud - MINIO_ROOT_PASSWORD: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= - MINIO_DEFAULT_BUCKETS: nextcloud - ports: - - "9000:9000" - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Nextcloud - env: - OBJECT_STORE: ${{ matrix.key }} - OBJECT_STORE_KEY: nextcloud - OBJECT_STORE_SECRET: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ= - run: | - composer install - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password - php -f tests/enable_all.php - - - name: Wait for S3 - run: | - sleep 10 - curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready - - - name: PHPUnit - run: composer run test:db -- --log-junit junit.xml - - - name: S3 logs - if: always() - run: | - cat data/nextcloud.log - docker ps -a - docker ps -aq | while read container ; do IMAGE=$(docker inspect --format='{{.Config.Image}}' $container); echo $IMAGE; docker logs $container; echo "\n\n" ; done --- a/.github/workflows/phpunit-oci.yml +++ /dev/null @@ -1,96 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit OCI - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-oci: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - strategy: - fail-fast: false - matrix: - include: - - oracle-versions: '18' - php-versions: '8.3' - - oracle-versions: '23' - php-versions: '8.5' - - name: Oracle ${{ matrix.oracle-versions }} (PHP ${{ matrix.php-versions }}) - database tests - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - oracle: - image: ghcr.io/gvenzl/oracle-${{ matrix.oracle-versions < 23 && 'xe' || 'free' }}:${{ matrix.oracle-versions }} - - # Provide passwords and other environment variables to container - env: - ORACLE_PASSWORD: oracle - - # Forward Oracle port - ports: - - 1521:1521 - - # Provide healthcheck script options for startup - options: >- - --health-cmd healthcheck.sh - --health-interval 20s - --health-timeout 10s - --health-retries 10 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, oci8 - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Set up Nextcloud - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=oci --database-name=${{ matrix.oracle-versions < 23 && 'XE' || 'FREE' }} --database-host=127.0.0.1 --database-port=1521 --database-user=system --database-pass=oracle --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit - run: composer run test:db -- --log-junit junit.xml - - - name: Run repair steps - run: | - ./occ maintenance:repair --include-expensive --- a/.github/workflows/phpunit-pgsql.yml +++ /dev/null @@ -1,93 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit PostgreSQL - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-pgsql: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - - postgres-versions: '14' - php-versions: '8.3' - - postgres-versions: '18' - php-versions: '8.5' - - name: PostgreSQL ${{ matrix.postgres-versions }} (PHP ${{ matrix.php-versions }}) - database tests - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - postgres: - image: ghcr.io/nextcloud/continuous-integration-postgres-${{ matrix.postgres-versions }}:latest # zizmor: ignore[unpinned-images] - ports: - - 4444:5432/tcp - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: rootpassword - POSTGRES_DB: nextcloud - options: --mount type=tmpfs,destination=/var/lib/postgresql --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dependencies - run: composer i - - - name: Set up Nextcloud - env: - DB_PORT: 4444 - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: PHPUnit database tests - run: composer run test:db -- --log-junit junit.xml - - - name: Run repair steps - run: | - ./occ maintenance:repair --include-expensive - - - name: Print logs - if: always() - run: | - cat data/nextcloud.log --- a/.github/workflows/phpunit-sqlite.yml +++ /dev/null @@ -1,115 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit SQLite - -on: - workflow_call: - -permissions: - contents: read - -jobs: - phpunit-sqlite: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-versions: ['8.3', '8.5'] - - name: SQLite (PHP ${{ matrix.php-versions }}) - - services: - cache: - image: ghcr.io/nextcloud/continuous-integration-redis:latest # zizmor: ignore[unpinned-images] - ports: - - 6379:6379/tcp - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - steps: - - name: Checkout server - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: ${{ matrix.php-versions }} - # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, imagick, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - ini-values: disable_functions="" - env: - fail-fast: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Enable HEIC support in ImageMagick - run: | - # The HEIC preview tests need ImageMagick to actually decode HEIC files. - # GitHub-hosted runners register the HEIC coder (so the tests are not - # skipped) but decoding fails because the libheif delegate is missing - # and/or the coder is blocked by ImageMagick's security policy. Install - # the delegate and allow the HEIC/HEIF/AVIF coders so decoding succeeds. - sudo apt-get update - sudo apt-get install -y --no-install-recommends libheif1 libheif-dev - for policy in /etc/ImageMagick-{6,7}/policy.xml; do - if [ -f "$policy" ]; then - sudo sed -i -E 's/rights="none" pattern="(HEIC|HEIF|AVIF)"/rights="read|write" pattern="\1"/g' "$policy" - fi - done - - - name: Set up dependencies - run: | - sudo apt-get install -y ghostscript - composer i - - - name: Set up Nextcloud - run: | - mkdir data - cp tests/redis.config.php config/ - cp tests/preseed-config.php config/config.php - ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin - php -f tests/enable_all.php - - - name: Nextcloud debug information - run: ./occ app:list && echo "======= System config =======" && ./occ config:list system - - - name: PHPUnit database tests - run: composer run test:db -- --log-junit junit.xml - - - name: Slowest tests - if: always() - continue-on-error: true - run: | - { - echo '```' - php tests/junit-analyzer.php junit.xml 30 - echo '```' - } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}" - - - name: Outgoing HTTP requests - if: always() - continue-on-error: true - run: | - { - echo '```' - php tests/http-analyzer.php http-requests.log 20 - echo '```' - } | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}" - - - name: Print logs - if: always() - run: | - gs --version - cat /etc/ImageMagick-6/policy.xml - cat data/nextcloud.log --- a/.github/workflows/phpunit.yml +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: PHPUnit - -on: - pull_request: - schedule: - - cron: "5 2 * * *" - -permissions: - contents: read - -concurrency: - group: phpunit-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -# GitHub allows a maximum of 20 reusable workflows per caller -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - src: ${{ steps.changes.outputs.src }} - resources: ${{ steps.changes.outputs.resources }} - kvstore: ${{ steps.changes.outputs.kvstore }} - - steps: - - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/appinfo/**' - - '**/lib/**' - - '**/templates/**' - - '**/tests/**' - - 'vendor/**' - - 'vendor-bin/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - '**.php' - resources: - - 'resources/**' - kvstore: - - '.github/workflows/phpunit-kvstore.yml' - - '3rdparty/**' - - '**/appinfo/**' - - '**.php' - - mariadb: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-mariadb.yml - - memcached: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-memcached.yml - - mysql: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-mysql.yml - - mysql-sharding: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-mysql-sharding.yml - - object-store-primary: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-object-store-primary.yml - - oci: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-oci.yml - - pgsql: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-pgsql.yml - - sqlite: - needs: changes - if: needs.changes.outputs.src != 'false' - uses: ./.github/workflows/phpunit-sqlite.yml - - nodb: - needs: changes - if: needs.changes.outputs.src != 'false' || needs.changes.outputs.resources != 'false' - uses: ./.github/workflows/phpunit-nodb.yml - - kvstore: - needs: changes - if: needs.changes.outputs.kvstore != 'false' - uses: ./.github/workflows/phpunit-kvstore.yml - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, mariadb, memcached, mysql, mysql-sharding, object-store-primary, oci, pgsql, sqlite, nodb, kvstore] - - if: always() - - name: phpunit-summary - - steps: - # Skipped jobs are fine, they mean no relevant changes were detected - - name: Summary status - run: if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then exit 1; fi --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,325 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Playwright Tests - -on: - pull_request: - branches: - - master - - stable35 - types: - - opened - - synchronize - - reopened - - ready_for_review - - labeled - -permissions: - contents: read - -jobs: - gate: - runs-on: ubuntu-latest-low - steps: - - name: Evaluate e2e tests execution conditions - id: gate-e2e - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const pr = context.payload.pull_request - - const hasForceLabel = pr.labels.some((label) => label.name === 'force-e2e-tests') - const hasToReviewLabel = pr.labels.some((label) => label.name === '3. to review') - const hasToReleaseLabel = pr.labels.some((label) => label.name === '4. to release') - - const files = await github.paginate(github.rest.pulls.listFiles, { - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - per_page: 100, - }) - const playwrightTouched = files.some((file) => file.filename.startsWith('tests/playwright')) - - if (hasForceLabel || hasToReviewLabel || hasToReleaseLabel || playwrightTouched) { - return - } else { - core.setFailed('Skipping Playwright: draft state, missing labels or no playwright path changes.') - } - - playwright-setup: - timeout-minutes: 15 - name: Playwright setup - runs-on: ubuntu-latest - needs: gate - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true # for 3rdparty - - name: Read package.json - uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 - id: versions - - name: Set up node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.node-version }} - - name: Set up npm - run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' - - name: Install dependencies and build - run: | - npm ci - npm run build --if-present - - name: Save context - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: playwright-context-${{ github.run_id }} - path: ./ - - playwright-tests: - needs: [gate, playwright-setup] - timeout-minutes: 60 - name: Playwright tests ${{ matrix.shardIndex }} / ${{ matrix.shardTotal }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] - outputs: - node-version: ${{ steps.versions.outputs.node-version }} - package-manager-version: ${{ steps.versions.outputs.package-manager-version }} - - steps: - - name: Restore context - id: cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: playwright-context-${{ github.run_id }} - path: ./ - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: steps.cache.outputs.cache-hit != 'true' - with: - persist-credentials: false - submodules: true # for 3rdparty - - - name: Read package.json - uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 - id: versions - - - name: Set up node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.node-version }} - - - name: Set up npm - run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' - - - name: Install dependencies and build - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm ci - npm run build --if-present - - - name: Install Playwright browsers - run: npm run playwright:install-ci --if-present - - - name: Run Playwright tests - run: npm run playwright -- --shard='${{ matrix.shardIndex }}/${{ matrix.shardTotal }}' - - - name: Show logs - if: failure() - run: | - for id in $(docker ps -aq); do - docker container inspect "$id" --format '=== Logs for container {{.Name}} ===' - docker logs "$id" >> nextcloud.log - done - echo '=== Nextcloud server logs ===' - docker exec nextcloud-e2e-test-server_server cat data/nextcloud.log - - - name: Upload blob report to GitHub Actions Artifacts - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: blob-report-${{ matrix.shardIndex }} - path: blob-report - retention-days: 1 - - playwright-installer-tests: - needs: [gate, playwright-setup] - timeout-minutes: 30 - name: Playwright tests for installer - runs-on: ubuntu-latest - - # The installation-wizard tests exercise every supported database backend, so - # they need reachable database service containers. The Nextcloud container is - # joined to the GitHub Actions network at startup so it can resolve these by - # hostname (see tests/playwright/start-nextcloud-server.js). - services: - mysql: - image: mysql:9.7 # zizmor: ignore[unpinned-images] - ports: - - '3306/tcp' - env: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_USER: nextcloud - MYSQL_PASSWORD: nextcloud - MYSQL_DATABASE: nextcloud - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10 - - mariadb: - image: mariadb:12.3 # zizmor: ignore[unpinned-images] - ports: - - '3306/tcp' - env: - MARIADB_ROOT_PASSWORD: rootpassword - MARIADB_USER: nextcloud - MARIADB_PASSWORD: nextcloud - MARIADB_DATABASE: nextcloud - options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5 - - postgres: - image: postgres:18 # zizmor: ignore[unpinned-images] - ports: - - '5432/tcp' - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: rootpassword - POSTGRES_DB: nextcloud - options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 - - oracle: - image: ghcr.io/gvenzl/oracle-free:23 # zizmor: ignore[unpinned-images] - ports: - - '1521' - env: - ORACLE_PASSWORD: oracle - options: --health-cmd healthcheck.sh --health-interval 20s --health-timeout 10s --health-retries 10 - - steps: - - name: Restore context - id: cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: playwright-context-${{ github.run_id }} - path: ./ - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: steps.cache.outputs.cache-hit != 'true' - with: - persist-credentials: false - submodules: true # for 3rdparty - - - name: Read package.json - uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 - id: versions - - - name: Set up node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ steps.versions.outputs.node-version }} - - - name: Set up npm - run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' - - - name: Install dependencies and build - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm ci - npm run build --if-present - - - name: Install Playwright browsers - run: npm run playwright:install-ci --if-present - - - name: Run Playwright setup tests - run: npm run playwright:setup - env: - PLAYWRIGHT_SETUP: 'true' - - - name: Show logs - if: failure() - run: | - for id in $(docker ps -aq); do - docker container inspect "$id" --format '=== Logs for container {{.Name}} ===' - docker logs "$id" >> nextcloud.log - done - echo '=== Nextcloud server logs ===' - docker exec nextcloud-e2e-test-server_server cat data/nextcloud.log || true - - - name: Upload blob report to GitHub Actions Artifacts - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: blob-report-setup - path: blob-report - retention-days: 1 - - merge-reports: - # Merge reports after playwright-tests, even if some shards have failed - if: ${{ !cancelled() }} - needs: [gate, playwright-tests, playwright-installer-tests] - - runs-on: ubuntu-latest-low - steps: - - name: Restore context - id: cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - key: playwright-context-${{ github.run_id }} - path: ./ - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: steps.cache.outputs.cache-hit != 'true' - with: - persist-credentials: false - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - if: steps.cache.outputs.cache-hit != 'true' - with: - node-version: ${{ needs.playwright-tests.outputs.node-version }} - - - name: Set up npm - if: steps.cache.outputs.cache-hit != 'true' - run: npm i -g 'npm@${{ needs.playwright-tests.outputs.package-manager-version }}' - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Download blob reports from GitHub Actions Artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: all-blob-reports - pattern: blob-report-* - merge-multiple: true - - - name: Merge into HTML Report - run: npx playwright merge-reports --config tests/playwright/merge.config.ts --reporter html,github ./all-blob-reports - - - name: Upload HTML report - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: html-report--attempt-${{ github.run_attempt }} - path: playwright-report - retention-days: 7 - - - name: Show the logs - run: | - echo 'To view the report:' - echo ' 1. Extract the folder from the zip file' - echo ' 2. run "npx playwright show-report name-of-my-extracted-playwright-report"' - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [gate, playwright-tests, playwright-installer-tests] - - if: always() - - name: playwright-test-summary - - steps: - - name: Summary status - run: if ${{ needs.playwright-tests.result != 'success' || needs.playwright-installer-tests.result != 'success' }}; then exit 1; fi --- a/.github/workflows/pr-feedback.yml +++ /dev/null @@ -1,55 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization - -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-FileCopyrightText: 2023 Marcel Klehr -# SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> -# SPDX-FileCopyrightText: 2023 Daniel Kesselberg -# SPDX-FileCopyrightText: 2023 Florian Steffens -# SPDX-License-Identifier: MIT - -name: 'Ask for feedback on PRs' -on: - schedule: - - cron: '30 1 * * *' - -permissions: - contents: read - pull-requests: write - -jobs: - pr-feedback: - if: ${{ github.repository_owner == 'nextcloud' }} - runs-on: ubuntu-latest - steps: - - name: The get-github-handles-from-website action - uses: marcelklehr/get-github-handles-from-website-action@06b2239db0a48fe1484ba0bfd966a3ab81a08308 # v1.0.1 - id: scrape - with: - website: 'https://nextcloud.com/team/' - - - name: Get blocklist - id: blocklist - run: | - blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) - echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" - - - uses: nextcloud/pr-feedback-action@5227c55be184087d0aef6338bee210d8620b6297 # v1.0.1 - with: - feedback-message: | - Hello there, - Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. - - We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. - - Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 - - Thank you for contributing to Nextcloud and we hope to hear from you soon! - - (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) - days-before-feedback: 14 - start-date: '2025-06-12' - exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' - exempt-bots: true --- a/.github/workflows/rector-apply.yml +++ /dev/null @@ -1,65 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization -# -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Apply rector changes - -on: - workflow_dispatch: - schedule: - # At 14:30 on Sundays - - cron: '30 14 * * 0' - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - name: rector-apply - - steps: - - name: Checkout - id: checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ github.event.repository.default_branch }} - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - with: - php-version: '8.3' - extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite - coverage: none - ini-file: development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install dependencies - run: | - composer remove nextcloud/ocp --dev --no-scripts - composer i - git restore lib/composer/composer - - - name: Rector - run: composer run rector - - - name: Create Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: 'refactor: Apply rector changes' - committer: GitHub - author: nextcloud-command - signoff: true - branch: automated/noid/rector-changes - title: 'Apply rector changes' - labels: | - technical debt - 3. to review --- a/.github/workflows/rector.yml +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -name: Rector - -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: rector-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src}} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/lib/**' - - '**/tests/**' - - '**/vendor-bin/**' - - '.php-cs-fixer.dist.php' - - 'composer.json' - - 'composer.lock' - - '**.php' - - strict: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - with: - php-version: '8.3' - extensions: apcu,ctype,curl,dom,fileinfo,ftp,gd,imagick,intl,json,ldap,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Composer install - run: composer i - - - name: Rector - run: composer run rector:strict - - - name: Show changes - if: always() - run: git diff --exit-code -- . ':!lib/composer' - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, strict] - - if: always() - - name: rector-summary - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.strict.result != 'success' }}; then exit 1; fi --- a/.github/workflows/reuse.yml +++ /dev/null @@ -1,27 +0,0 @@ -# This workflow is provided via the organization template repository -# -# https://github.com/nextcloud/.github -# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization - -# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. -# -# SPDX-License-Identifier: CC0-1.0 - -name: REUSE Compliance Check - -on: [pull_request] - -permissions: - contents: read - -jobs: - reuse-compliance-check: - runs-on: ubuntu-latest-low - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: REUSE Compliance Check - uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Close stale issues - -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * *" - -permissions: - contents: read - -jobs: - stale: - runs-on: ubuntu-latest - - if: ${{ github.repository_owner != 'nextcloud-gmbh' }} - - permissions: - issues: write - - steps: - - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 - with: - repo-token: ${{ secrets.COMMAND_BOT_PAT }} - stale-issue-message: > - This issue has been automatically marked as stale because it has not had - recent activity and seems to be missing some essential information. - It will be closed if no further activity occurs. Thank you - for your contributions. - stale-issue-label: 'stale' - only-labels: 'needs info' - exempt-issue-labels: '1. to develop,2. developing,3. to review,4. to release,security' - days-before-stale: 30 - days-before-close: 14 - # debug-only: true - --- a/.github/workflows/static-code-analysis.yml +++ /dev/null @@ -1,243 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Psalm static code analysis - -on: - pull_request: - push: - branches: - - main - - master - - stable* - paths: - - '.github/workflows/static-code-analysis.yml' - - '**.php' - -permissions: - contents: read - -concurrency: - group: static-code-analysis-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - changes: - runs-on: ubuntu-latest-low - - outputs: - src: ${{ steps.changes.outputs.src }} - - steps: - - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 - id: changes - continue-on-error: true - with: - filters: | - src: - - '.github/workflows/**' - - '3rdparty/**' - - '**/appinfo/**' - - '**/lib/**' - - '**/templates/**' - - 'vendor/**' - - 'vendor-bin/**' - - 'composer.json' - - 'composer.lock' - - 'psalm*.xml' - - 'build/psalm-baseline*.xml' - - '**.php' - - static-code-analysis: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: apcu,ctype,curl,dom,fileinfo,ftp,gd,imagick,intl,json,ldap,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Composer install - run: composer i - - - name: Psalm - run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github --update-baseline - - - name: Show potential changes in Psalm baseline - if: always() - run: git diff --exit-code -- . ':!lib/composer' - - static-code-analysis-security: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.repository_owner != 'nextcloud-gmbh' }} - - permissions: - security-events: write - - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: ctype,curl,dom,fileinfo,ftp,gd,imagick,intl,json,ldap,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - - - name: Composer install - run: composer i - - - name: Psalm taint analysis - run: composer run psalm:security -- --threads=1 --monochrome --no-progress --output-format=github --update-baseline --report=results.sarif --report-show-info=false - - # Psalm exits 0 on taint findings when a report file is generated - - name: Fail on taint analysis findings - run: | - count=$(jq '[.runs[].results[]] | length' results.sarif) - echo "Taint analysis findings: $count" - test "$count" -eq 0 - - - name: Show potential changes in Psalm baseline - if: always() - run: git diff --exit-code -- . ':!lib/composer' - - - name: Upload Security Analysis results to GitHub - if: always() - uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - with: - sarif_file: results.sarif - - static-code-analysis-ocp: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Composer install - run: composer i - - - name: Psalm - run: composer run psalm:ocp -- --threads=1 --monochrome --no-progress --output-format=github --update-baseline - - - name: Show potential changes in Psalm baseline - if: always() - run: git diff --exit-code -- . ':!lib/composer' - - static-code-analysis-ncu: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - timeout-minutes: 5 - with: - php-version: '8.3' - extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Composer install - run: composer i - - - name: Psalm - run: composer run psalm:ncu -- --threads=1 --monochrome --no-progress --output-format=github - - static-code-analysis-strict: - runs-on: ubuntu-latest - - needs: changes - if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }} - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - submodules: true - - - name: Set up php - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - with: - php-version: '8.3' - extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip - coverage: none - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Composer install - run: composer i - - - name: Psalm - run: composer run psalm:strict -- --threads=1 --monochrome --no-progress --output-format=github - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, static-code-analysis, static-code-analysis-security, static-code-analysis-ocp, static-code-analysis-ncu, static-code-analysis-strict] - - if: always() - - name: static-code-analysis-summary - - steps: - - name: Summary status - run: | - if ${{ needs.changes.outputs.src != 'false' && ( - needs.static-code-analysis-security.result != 'success' || - (github.event_name != 'push' && ( - needs.static-code-analysis.result != 'success' || - needs.static-code-analysis-ocp.result != 'success' || - needs.static-code-analysis-ncu.result != 'success' || - needs.static-code-analysis-strict.result != 'success' - )) - ) }}; then - exit 1 - fi --- a/.github/workflows/storage.yml +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Storage - -on: - pull_request: - schedule: - - cron: "5 2 * * *" - -permissions: - contents: read - -concurrency: - group: storage-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -# GitHub allows a maximum of 20 reusable workflows per caller -jobs: - changes: - runs-on: ubuntu-latest-low - permissions: - contents: read - pull-requests: read - - outputs: - files-external: ${{ steps.changes.outputs.files-external }} - integration: ${{ steps.changes.outputs.integration }} - object-storage: ${{ steps.changes.outputs.object-storage }} - - steps: - - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 - id: changes - continue-on-error: true - with: - filters: | - shared: &shared - - '3rdparty/**' - - 'vendor/**' - - 'composer.json' - - 'composer.lock' - - 'apps/files/lib/**' - - 'apps/files/tests/**' - - 'apps/files_external/**' - - 'apps/files_sharing/lib/**' - - 'apps/files_sharing/tests/**' - - 'apps/files_trashbin/lib/**' - - 'apps/files_trashbin/tests/**' - - 'apps/files_versions/lib/**' - - 'apps/files_versions/tests/**' - - 'lib/private/Files/**' - - 'lib/public/Files/**' - - 'tests/lib/Files/**' - phpunit: &phpunit - - 'vendor-bin/phpunit/**' - object-storage: - - *shared - - *phpunit - - '.github/workflows/object-storage.yml' - files-external: - - *shared - - *phpunit - - '.github/workflows/files-external.yml' - integration: - - 'vendor-bin/behat/**' - - 'build/integration/**' - - '.github/workflows/integration-s3-primary.yml' - - files-external: - needs: changes - if: needs.changes.outputs.files-external != 'false' - uses: ./.github/workflows/files-external.yml - - object-storage: - needs: changes - if: needs.changes.outputs.object-storage != 'false' - uses: ./.github/workflows/object-storage.yml - - integration-s3-primary: - needs: changes - if: needs.changes.outputs.integration != 'false' - uses: ./.github/workflows/integration-s3-primary.yml - - summary: - permissions: - contents: none - runs-on: ubuntu-latest-low - needs: [changes, files-external, object-storage, integration-s3-primary] - - if: always() - - name: storage-summary - - steps: - # Skipped jobs are fine, they mean no relevant changes were detected - - name: Summary status - run: if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then exit 1; fi --- a/.github/workflows/update-cacert-bundle.yml +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Update CA certificate bundle - -on: - workflow_dispatch: - schedule: - - cron: "5 2 * * *" - -permissions: - contents: read - -jobs: - update-ca-certificate-bundle: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - branches: - - ${{ github.event.repository.default_branch }} - - 'stable34' - - 'stable33' - - 'stable32' - - 'stable31' - - 'stable30' - - 'stable29' - - 'stable28' - - 'stable27' - - 'stable26' - - 'stable25' - - 'stable24' - - 'stable23' - - 'stable22' - - name: update-ca-certificate-bundle-${{ matrix.branches }} - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ matrix.branches }} - submodules: true - - - name: Download CA certificate bundle from curl - run: curl --etag-compare build/ca-bundle-etag.txt --etag-save build/ca-bundle-etag.txt --output resources/config/ca-bundle.crt https://curl.se/ca/cacert.pem - - - name: Create Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: 'fix(security): Update CA certificate bundle' - committer: GitHub - author: nextcloud-command - signoff: true - branch: 'automated/noid/${{ matrix.branches }}-update-ca-cert-bundle' - title: '[${{ matrix.branches }}] fix(security): Update CA certificate bundle' - body: | - Auto-generated update of CA certificate bundle from [https://curl.se/docs/caextract.html](https://curl.se/docs/caextract.html) - labels: | - dependencies - 3. to review - reviewers: ChristophWurst, miaulalala, nickvergessen --- a/.github/workflows/update-code-signing-crl.yml +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Update code signing revocation list - -on: - workflow_dispatch: - schedule: - - cron: "5 2 * * *" - -permissions: - contents: read - -jobs: - update-code-signing-crl: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - branches: - - ${{ github.event.repository.default_branch }} - - 'stable34' - - 'stable33' - - 'stable32' - - 'stable31' - - 'stable30' - - 'stable29' - - 'stable28' - - 'stable27' - - 'stable26' - - 'stable25' - - 'stable24' - - 'stable23' - - 'stable22' - - name: update-code-signing-crl-${{ matrix.branches }} - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ matrix.branches }} - submodules: true - - - name: Download CRL file from Appstore repository - run: curl --output resources/codesigning/root.crl https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/certificate/nextcloud.crl - - - name: Verify CRL is from CRT - run: openssl crl -verify -in resources/codesigning/root.crl -CAfile resources/codesigning/root.crt -noout - - - name: Create Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: 'fix(security): Update code signing revocation list' - committer: GitHub - author: nextcloud-command - signoff: true - branch: 'automated/noid/${{ matrix.branches }}-update-code-signing-crl' - title: '[${{ matrix.branches }}] fix(security): Update code signing revocation list' - body: | - Auto-generated update of code signing revocation list from [Appstore](https://github.com/nextcloud/appstore/commits/master/nextcloudappstore/certificate/nextcloud.crl) - labels: | - dependencies - 3. to review - reviewers: mgallien, miaulalala, nickvergessen --- a/.github/workflows/update-min-supported-desktop.yml +++ /dev/null @@ -1,128 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT - -name: Update min supported desktop version - -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * 1" - -permissions: - contents: read - -jobs: - update-minimum-supported-desktop-version: - runs-on: ubuntu-latest-low - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: true - - - name: Download desktop client version file from 5 years ago - id: download - run: | - # Create a temporary directory for the downloaded file - mkdir -p tmp - - # Download the version file from the provided URL - VERSION_FILE_URL="https://github.com/nextcloud/desktop/raw/@%7B5.years.ago%7D/VERSION.cmake" - - # Download the file using curl - curl -s -L "$VERSION_FILE_URL" -o tmp/VERSION.cmake - - if [ ! -f "tmp/VERSION.cmake" ]; then - echo "Failed to download VERSION.cmake file" - exit 1 - fi - - echo "VERSION_FILE=tmp/VERSION.cmake" >> $GITHUB_OUTPUT - echo "Downloaded version file to tmp/VERSION.cmake" - - - name: Extract version info - id: extract-version - run: | - # Path to the downloaded version file - VERSION_FILE="${{ steps.download.outputs.VERSION_FILE }}" - - # Extract major, minor, patch versions - MAJOR=$(grep "VERSION_MAJOR" $VERSION_FILE | grep -o '[0-9]\+') - MINOR=$(grep "VERSION_MINOR" $VERSION_FILE | grep -o '[0-9]\+') - PATCH=$(grep "VERSION_PATCH" $VERSION_FILE | grep -o '[0-9]\+') - - # Construct the version string - VERSION="$MAJOR.$MINOR.$PATCH" - - # Validate format: xx.xx.xx where each x is a digit - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Extracted version '$VERSION' does not match required format (xx.xx.xx)" - exit 1 - fi - - rm -rf tmp - - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted Version: $VERSION" - - - name: Update files with new version ${{ steps.extract-version.outputs.VERSION }} - id: update-files - run: | - VERSION="${{ steps.extract-version.outputs.VERSION }}" - - # Define the files to update - DAV_FILE="apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php" - CONFIG_FILE="config/config.sample.php" - - # Check if files exist - if [ ! -f "$DAV_FILE" ]; then - echo "Error: DAV file not found at $DAV_FILE" - exit 1 - fi - - if [ ! -f "$CONFIG_FILE" ]; then - echo "Error: Config file not found at $CONFIG_FILE" - exit 1 - fi - - # Update the DAV file - replace the version in the specific line - sed -i "s/\('minimum\.supported\.desktop\.version', '\)[0-9]\+\.[0-9]\+\.[0-9]\+'/\1$VERSION'/g" "$DAV_FILE" - echo "Updated $DAV_FILE" - - # Update the config sample file - PREV_VERSION=$(grep "'minimum.supported.desktop.version'" "$CONFIG_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') - sed -i "s/Defaults to \`\`$PREV_VERSION\`\`/Defaults to \`\`$VERSION\`\`/" "$CONFIG_FILE" - sed -i "s/'minimum\.supported\.desktop\.version' => '[0-9]\+\.[0-9]\+\.[0-9]\+'/'minimum.supported.desktop.version' => '$VERSION'/g" "$CONFIG_FILE" - echo "Updated $CONFIG_FILE" - - # Check if any changes were made - if [ -n "$(git diff "$DAV_FILE" "$CONFIG_FILE")" ]; then - echo "CHANGES_MADE=true" >> $GITHUB_OUTPUT - echo "Changes were made to the files" - git diff "$DAV_FILE" "$CONFIG_FILE" - else - echo "CHANGES_MADE=false" >> $GITHUB_OUTPUT - echo "No changes were needed (versions might already be up to date)" - fi - - - name: Create Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 - if: steps.update-files.outputs.CHANGES_MADE == 'true' - with: - token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: "chore: Update minimum supported desktop version" - committer: GitHub - author: nextcloud-command - signoff: true - branch: "automated/noid/update-min-supported-desktop-version" - title: "chore: Update minimum supported desktop version to ${{ steps.extract-version.outputs.VERSION }}" - base: "master" - body: | - Auto-generated update of the minimum supported desktop version using last supported version. - https://github.com/nextcloud/desktop/blob/@%7B5.years.ago%7D/VERSION.cmake - labels: | - client: 💻 desktop - automated - 3. to review - reviewers: '@nextcloud/desktop' --- a/.github/workflows/update-node-dist.yml +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Update Node dist - -on: - workflow_dispatch: - push: - branches: - # implemented since 35, once branched off, add your stable branch here - - master - - stable35 - -permissions: - contents: write - -concurrency: - group: update-node-dist-${{ github.head_ref || github.ref || github.run_id }} - -jobs: - update-node-dist: - runs-on: ubuntu-latest-assets - environment: update-node-dist - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # Needed to allow force push later - persist-credentials: true - token: ${{ secrets.BOT_GITHUB_TOKEN }} - - - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 - id: versions - with: - fallbackNode: '^20' - fallbackNpm: '^9' - - - name: Set up node ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - with: - node-version: ${{ steps.versions.outputs.nodeVersion }} - - - name: Set up npm ${{ steps.versions.outputs.npmVersion }} - run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" - - - name: Setup git - run: | - git config --local user.email "nextcloud-command@users.noreply.github.com" - git config --local user.name "nextcloud-command" - - - name: Get last commit message if it was not a recompile - id: last_commit - run: | - { - echo 'MESSAGE<> $GITHUB_OUTPUT - - - name: Install dependencies & build - if: steps.last_commit.outputs.MESSAGE != '' - env: - CYPRESS_INSTALL_BINARY: 0 - run: | - npm ci - npm run build --if-present - - - name: Check webpack build changes - id: changes - continue-on-error: true - run: | - { - echo 'CHANGED<> "$GITHUB_OUTPUT" - - - name: Add and commit - if: steps.changes.outputs.CHANGED != '' - run: | - git add --force dist/ core/css/ - git commit --signoff -m 'chore(assets): recompile assets' - git push origin ${GITHUB_HEAD_REF} --- a/.github/workflows/update-stable-titles.yml +++ /dev/null @@ -1,69 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: MIT -name: Update PRs titles on stable branches - -on: - pull_request: - types: [opened, edited] - branches: - - "stable*" - -concurrency: - group: stable-pr-title-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - update-pr-title: - runs-on: ubuntu-latest-low - permissions: - pull-requests: write - contents: read - - steps: - - name: Wait for potential title edits - run: sleep 15 - - - name: Get PR details and update title - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - - const baseBranch = pr.base.ref; - const currentTitle = pr.title; - - // Check if this is a stable branch - // Should not happen as we only trigger on stable* branches 🤷‍♀️ - if (!baseBranch.startsWith('stable')) { - console.log(`Not a stable branch: ${baseBranch}`); - return; - } - - const prefix = `[${baseBranch}]`; - - // Check if title already has the correct prefix and no other stable tags - const correctTagRegex = new RegExp(`^\\[${baseBranch}\\]\\s*`); - const hasOtherStableTags = /\[stable[\d.]*\]/.test(currentTitle.replace(correctTagRegex, '')); - - if (correctTagRegex.test(currentTitle) && !hasOtherStableTags) { - console.log(`Title already has correct prefix only: ${currentTitle}`); - return; - } - - // Remove all stable tags and add the correct one - const cleanTitle = currentTitle.replace(/\[stable[\d.]*\]\s*/g, '').trim(); - const newTitle = `${prefix} ${cleanTitle}`; - - console.log(`Updating title from: "${currentTitle}" to: "${newTitle}"`); - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - title: newTitle, - }); --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - --- a/.idea/codeStyleSettings.xml.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2014 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/.tx/config +++ /dev/null @@ -1,207 +0,0 @@ -[main] -host = https://www.transifex.com -lang_map = fi_FI: fi, hu_HU: hu, nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs - -[o:nextcloud:p:nextcloud:r:admin_audit] -file_filter = translationfiles//admin_audit.po -source_file = translationfiles/templates/admin_audit.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:appstore-shipped-with-server] -file_filter = translationfiles//appstore.po -source_file = translationfiles/templates/appstore.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:cloud_federation_api] -file_filter = translationfiles//cloud_federation_api.po -source_file = translationfiles/templates/cloud_federation_api.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:comments] -file_filter = translationfiles//comments.po -source_file = translationfiles/templates/comments.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:contactsinteraction] -file_filter = translationfiles//contactsinteraction.po -source_file = translationfiles/templates/contactsinteraction.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:core] -file_filter = translationfiles//core.po -source_file = translationfiles/templates/core.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:dashboard-shipped-with-server] -file_filter = translationfiles//dashboard.po -source_file = translationfiles/templates/dashboard.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:dav] -file_filter = translationfiles//dav.po -source_file = translationfiles/templates/dav.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:federatedfilesharing] -file_filter = translationfiles//federatedfilesharing.po -source_file = translationfiles/templates/federatedfilesharing.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:federation] -file_filter = translationfiles//federation.po -source_file = translationfiles/templates/federation.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files] -file_filter = translationfiles//files.po -source_file = translationfiles/templates/files.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_encryption] -file_filter = translationfiles//encryption.po -source_file = translationfiles/templates/encryption.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_external] -file_filter = translationfiles//files_external.po -source_file = translationfiles/templates/files_external.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_reminders] -file_filter = translationfiles//files_reminders.po -source_file = translationfiles/templates/files_reminders.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_sharing] -file_filter = translationfiles//files_sharing.po -source_file = translationfiles/templates/files_sharing.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_trashbin] -file_filter = translationfiles//files_trashbin.po -source_file = translationfiles/templates/files_trashbin.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:files_versions] -file_filter = translationfiles//files_versions.po -source_file = translationfiles/templates/files_versions.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:lib] -file_filter = translationfiles//lib.po -source_file = translationfiles/templates/lib.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:lookup_server_connector] -file_filter = translationfiles//lookup_server_connector.po -source_file = translationfiles/templates/lookup_server_connector.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:oauth2] -file_filter = translationfiles//oauth2.po -source_file = translationfiles/templates/oauth2.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:profile] -file_filter = translationfiles//profile.po -source_file = translationfiles/templates/profile.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:provisioning_api] -file_filter = translationfiles//provisioning_api.po -source_file = translationfiles/templates/provisioning_api.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:settings-1] -file_filter = translationfiles//settings.po -source_file = translationfiles/templates/settings.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:sharebymail] -file_filter = translationfiles//sharebymail.po -source_file = translationfiles/templates/sharebymail.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:sharing] -file_filter = translationfiles//sharing.po -source_file = translationfiles/templates/sharing.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:systemtags] -file_filter = translationfiles//systemtags.po -source_file = translationfiles/templates/systemtags.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:theming] -file_filter = translationfiles//theming.po -source_file = translationfiles/templates/theming.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:twofactor_backupcodes] -file_filter = translationfiles//twofactor_backupcodes.po -source_file = translationfiles/templates/twofactor_backupcodes.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:updatenotification] -file_filter = translationfiles//updatenotification.po -source_file = translationfiles/templates/updatenotification.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:user_ldap] -file_filter = translationfiles//user_ldap.po -source_file = translationfiles/templates/user_ldap.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:user_status] -file_filter = translationfiles//user_status.po -source_file = translationfiles/templates/user_status.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:weather_status] -file_filter = translationfiles//weather_status.po -source_file = translationfiles/templates/weather_status.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:webhook_listeners] -file_filter = translationfiles//webhook_listeners.po -source_file = translationfiles/templates/webhook_listeners.pot -source_lang = en -type = PO - -[o:nextcloud:p:nextcloud:r:workflowengine] -file_filter = translationfiles//workflowengine.po -source_file = translationfiles/templates/workflowengine.pot -source_lang = en -type = PO --- "a/tests/data/str\303\244ng\303\251 filename (duplicate #2).txt" +++ /dev/null @@ -1,4 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file --- a/3rdparty/composer.lock +++ b/3rdparty/composer.lock @@ -5011,16 +5011,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.15", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7" + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1f898ee8188adda9417fb52cf8425a8342c254e7", - "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f7b953be7801974a5a0685e1228bbc3b12b35936", + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936", "shasum": "" }, "require": { @@ -5069,7 +5069,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.15" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.19" }, "funding": [ { @@ -5089,7 +5089,7 @@ "type": "tidelift" } ], - "time": "2026-07-29T07:12:33+00:00" + "time": "2026-09-14T17:41:17+00:00" }, { "name": "symfony/mailer", --- a/3rdparty/composer/installed.json +++ b/3rdparty/composer/installed.json @@ -5227,17 +5227,17 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.15", - "version_normalized": "7.4.15.0", + "version": "v7.4.19", + "version_normalized": "7.4.19.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7" + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1f898ee8188adda9417fb52cf8425a8342c254e7", - "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f7b953be7801974a5a0685e1228bbc3b12b35936", + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936", "shasum": "" }, "require": { @@ -5260,7 +5260,7 @@ "symfony/mime": "^6.4|^7.0|^8.0", "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, - "time": "2026-07-29T07:12:33+00:00", + "time": "2026-09-14T17:41:17+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5288,7 +5288,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.15" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.19" }, "funding": [ { --- a/3rdparty/composer/installed.php +++ b/3rdparty/composer/installed.php @@ -764,9 +764,9 @@ 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v7.4.15', - 'version' => '7.4.15.0', - 'reference' => '1f898ee8188adda9417fb52cf8425a8342c254e7', + 'pretty_version' => 'v7.4.19', + 'version' => '7.4.19.0', + 'reference' => 'f7b953be7801974a5a0685e1228bbc3b12b35936', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), --- a/3rdparty/symfony/http-foundation/AcceptHeaderItem.php +++ b/3rdparty/symfony/http-foundation/AcceptHeaderItem.php @@ -50,7 +50,7 @@ public function __toString(): string { $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); - if (\count($this->attributes) > 0) { + if ($this->attributes) { $string .= '; '.HeaderUtils::toString($this->attributes, ';'); } --- a/3rdparty/symfony/http-foundation/BinaryFileResponse.php +++ b/3rdparty/symfony/http-foundation/BinaryFileResponse.php @@ -25,6 +25,13 @@ */ class BinaryFileResponse extends Response { + // Values accepted for the "X-Sendfile-Type" header, mapped to the response header to send + private const SENDFILE_TYPES = [ + 'x-sendfile' => 'X-Sendfile', + 'x-accel-redirect' => 'X-Accel-Redirect', + 'x-lighttpd-send-file' => 'X-LIGHTTPD-send-file', + ]; + protected static bool $trustXSendfileTypeHeader = false; protected File $file; @@ -215,15 +222,17 @@ $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none'); } - if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { + $sendfileType = self::$trustXSendfileTypeHeader ? $request->headers->get('X-Sendfile-Type') : null; + $type = self::SENDFILE_TYPES[strtolower($sendfileType ?? '')] ?? null; + + if (null !== $type) { // Use X-Sendfile, do not send any content. - $type = $request->headers->get('X-Sendfile-Type'); $path = $this->file->getRealPath(); // Fall back to scheme://path for stream wrapped locations. if (false === $path) { $path = $this->file->getPathname(); } - if ('x-accel-redirect' === strtolower($type)) { + if ('X-Accel-Redirect' === $type) { // Do X-Accel-Mapping substitutions. // @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb // @link https://mattbrictson.com/blog/accelerated-rails-downloads --- a/3rdparty/symfony/http-foundation/HeaderBag.php +++ b/3rdparty/symfony/http-foundation/HeaderBag.php @@ -134,13 +134,13 @@ if (\is_array($values)) { $values = array_values($values); - if (true === $replace || !isset($this->headers[$key])) { + if ($replace || !isset($this->headers[$key])) { $this->headers[$key] = $values; } else { $this->headers[$key] = array_merge($this->headers[$key], $values); } } else { - if (true === $replace || !isset($this->headers[$key])) { + if ($replace || !isset($this->headers[$key])) { $this->headers[$key] = [$values]; } else { $this->headers[$key][] = $values; --- a/3rdparty/symfony/http-foundation/HeaderUtils.php +++ b/3rdparty/symfony/http-foundation/HeaderUtils.php @@ -58,9 +58,12 @@ "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) | # token - [^"'.$quotedSeparators.']+ - )+ - (?getLimiters($request); - if (0 === \count($limiters)) { + if (!$limiters) { $limiters = [new NoLimiter()]; } --- a/3rdparty/symfony/http-foundation/Request.php +++ b/3rdparty/symfony/http-foundation/Request.php @@ -181,6 +181,16 @@ private static int $trustedHeaderSet = -1; + /** + * @var array + */ + private static array $trustedHostsLiterals = []; + + /** + * @var string[] + */ + private static array $trustedHostsRegexps = []; + private const FORWARDED_PARAMS = [ self::HEADER_X_FORWARDED_FOR => 'for', self::HEADER_X_FORWARDED_HOST => 'host', @@ -654,8 +664,19 @@ public static function setTrustedHosts(array $hostPatterns): void { self::$trustedHostPatterns = array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns); - // we need to reset trusted hosts on trusted host patterns change - self::$trustedHosts = []; + self::$trustedHostsLiterals = []; + $regexpPatterns = []; + + foreach ($hostPatterns as $hostPattern) { + // constant patterns are matched by a hash lookup in getHost(), where the host is lowercase and free of newlines + if (preg_match('{^\^((?:[a-z0-9_:-]|\\\\[^a-z0-9])++)\$$}Di', $hostPattern, $m)) { + self::$trustedHostsLiterals[strtolower(preg_replace('{\\\\(.)}s', '$1', $m[1]))] = true; + } else { + $regexpPatterns[] = $hostPattern; + } + } + + self::$trustedHostsRegexps = self::compileHostPatterns($regexpPatterns); } /** @@ -1193,17 +1214,15 @@ throw new SuspiciousOperationException(\sprintf('Invalid Host "%s".', $host)); } - if (\count(self::$trustedHostPatterns) > 0) { + if (self::$trustedHostsLiterals || self::$trustedHostsRegexps) { // to avoid host header injection attacks, you should provide a list of trusted host patterns - if (\in_array($host, self::$trustedHosts, true)) { + if (isset(self::$trustedHostsLiterals[$host])) { return $host; } - foreach (self::$trustedHostPatterns as $pattern) { - if (preg_match($pattern, $host)) { - self::$trustedHosts[] = $host; - + foreach (self::$trustedHostsRegexps as $regexp) { + if (preg_match($regexp, $host)) { return $host; } } @@ -2264,4 +2283,31 @@ // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names return '' === preg_replace('/[-a-zA-Z0-9_]++\.?/', '', $host); } + + /** + * Combines host patterns into as few regexps as PCRE can compile. + * + * @return string[] + */ + private static function compileHostPatterns(array $hostPatterns): array + { + if (!$hostPatterns) { + return []; + } + + // the branch reset group keeps capturing groups, back references and inline modifiers local to each pattern + $regexp = \sprintf('{(?|(?:%s))}i', implode(')|(?:', $hostPatterns)); + + if (1 === \count($hostPatterns) || false !== @preg_match($regexp, '')) { + return [$regexp]; + } + + // the combined pattern exceeds the maximum size PCRE accepts, split it in half + $half = intdiv(\count($hostPatterns), 2); + + return array_merge( + self::compileHostPatterns(\array_slice($hostPatterns, 0, $half)), + self::compileHostPatterns(\array_slice($hostPatterns, $half)) + ); + } } --- a/3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php +++ b/3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php @@ -27,6 +27,6 @@ public function matches(Request $request): bool { - return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo())); + return preg_match('{'.$this->regexp.'}s', rawurldecode($request->getPathInfo())); } } --- a/3rdparty/symfony/http-foundation/RequestMatcher/QueryParameterRequestMatcher.php +++ b/3rdparty/symfony/http-foundation/RequestMatcher/QueryParameterRequestMatcher.php @@ -41,6 +41,6 @@ return true; } - return 0 === \count(array_diff_assoc($this->parameters, $request->query->keys())); + return !array_diff_assoc($this->parameters, $request->query->keys()); } } --- a/3rdparty/symfony/http-foundation/Response.php +++ b/3rdparty/symfony/http-foundation/Response.php @@ -961,7 +961,7 @@ $etag = '"'.$etag.'"'; } - $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); + $this->headers->set('ETag', ($weak ? 'W/' : '').$etag); } return $this; @@ -1314,7 +1314,7 @@ */ protected function ensureIEOverSSLCompatibility(Request $request): void { - if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) { + if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && $request->isSecure()) { if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { $this->headers->remove('Cache-Control'); } --- a/3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -38,6 +38,14 @@ return true; } + /** + * @return string + */ + public function create_sid() + { + return session_create_id() ?: throw new \RuntimeException('Unable to create a session ID.'); + } + abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string; abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool; --- a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -34,6 +34,14 @@ return $this->handler->close(); } + /** + * @return string + */ + public function create_sid() + { + return session_create_id() ?: throw new \RuntimeException('Unable to create a session ID.'); + } + public function destroy(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->destroy($sessionId); @@ -46,7 +54,14 @@ public function read(#[\SensitiveParameter] string $sessionId): string { - return $this->marshaller->unmarshall($this->handler->read($sessionId)); + $data = $this->handler->read($sessionId); + + try { + return $this->marshaller->unmarshall($data); + } catch (\DomainException $e) { + // data that cannot be unmarshalled is treated as a missing session, as PHP does with data it cannot decode + return ''; + } } public function write(#[\SensitiveParameter] string $sessionId, string $data): bool --- a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php @@ -38,6 +38,14 @@ $this->writeOnlyHandler = $writeOnlyHandler; } + /** + * @return string + */ + public function create_sid() + { + return session_create_id() ?: throw new \RuntimeException('Unable to create a session ID.'); + } + public function close(): bool { $result = $this->currentHandler->close(); --- a/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php +++ b/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php @@ -73,6 +73,17 @@ return parent::regenerate($destroy, $lifetime); } + public function setId(string $id): void + { + // the id is turned into a file name, so keep it to the charset PHP allows for session ids + // and to the 255 bytes a file name can hold once the ".mocksess" suffix is added + if ('' !== $id && !preg_match('/^[a-zA-Z0-9,-]{1,246}$/D', $id)) { + $id = ''; + } + + parent::setId($id); + } + public function save(): void { if (!$this->started) { --- a/3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -62,6 +62,14 @@ return $this->handler->gc($maxlifetime); } + /** + * @return string + */ + public function create_sid() + { + return session_create_id() ?: throw new \RuntimeException('Unable to create a session ID.'); + } + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); --- a/3rdparty/symfony/http-foundation/UriSigner.php +++ b/3rdparty/symfony/http-foundation/UriSigner.php @@ -192,7 +192,7 @@ return self::STATUS_INVALID; } - if (!$expiration = $params[$this->expirationParameter] ?? false) { + if (null === $expiration = $params[$this->expirationParameter] ?? null) { return self::STATUS_VALID; } --- a/build/.htaccess +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -# SPDX-FileCopyrightText: 2015 ownCloud, Inc. -# SPDX-License-Identifier: AGPL-3.0-only -# line below if for Apache 2.4 - -Require all denied - - -# line below if for Apache 2.2 - -deny from all - - -# section for Apache 2.2 and 2.4 - -IndexIgnore * - --- a/build/autoloaderchecker.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -COMPOSER_COMMAND="php composer.phar" - -if [ -e "composer.phar" ] -then - echo "Composer found: checking for update" - $COMPOSER_COMMAND self-update -else - echo "Composer not found: fetching" - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - php composer-setup.php --2 - php -r "unlink('composer-setup.php');" -fi - - -COMPOSER_VERSION=$($COMPOSER_COMMAND --version | cut -d" " -f3) -COMPOSER_MAJOR_VERSION=$(echo "$COMPOSER_VERSION" | cut -d"." -f1) -COMPOSER_MINOR_VERSION=$(echo "$COMPOSER_VERSION" | cut -d"." -f2) -COMPOSER_PATCH_VERSION=$(echo "$COMPOSER_VERSION" | cut -d"." -f3) - -if ! [ "$COMPOSER_MAJOR_VERSION" -gt 2 -o \( "$COMPOSER_MAJOR_VERSION" -eq 2 -a "$COMPOSER_MINOR_VERSION" -ge 10 \) -o \( "$COMPOSER_MAJOR_VERSION" -eq 2 -a "$COMPOSER_MINOR_VERSION" -eq 9 -a "$COMPOSER_PATCH_VERSION" -ge 2 \) ]; then - echo "composer version >= 2.9.2 required. Version found: $COMPOSER_VERSION" >&2 - exit 1 -fi - -REPODIR=`git rev-parse --show-toplevel` - -#Redump the main autoloader -echo -echo "Regenerating main autoloader" -$COMPOSER_COMMAND dump-autoload -d $REPODIR - -FOUND_COMPOSER_BIN=$(grep --recursive --fixed-strings 'Bamarni\\Composer\\Bin' $REPODIR/lib/composer/composer/) -if [ -n "$FOUND_COMPOSER_BIN" ]; then - echo "The main autoloader contains the composer bin plugin" - echo "Run composer again with --no-dev and commit the result" - exit 1 -fi - -for app in ${REPODIR}/apps/*; do - if git check-ignore ${app} -q ; then - echo - echo "${app} is not shipped. Ignoring autoloader regeneration" - continue - fi - if [[ -d $app ]]; then - echo - echo "Regenerating composer files for ${app}" - $COMPOSER_COMMAND i --no-dev -d ${app}/composer || exit 1 - $COMPOSER_COMMAND dump-autoload -d ${app}/composer || exit 1 - git checkout ${app}/composer/composer/installed.php - fi -done - -files=`git diff --name-only` -composerfile=false -for file in $files -do - if [[ $file == *autoload_classmap* ]] - then - composerfile=true - break - fi -done - -rm composer.phar - -echo -if [ $composerfile = true ] -then - echo "The autoloaders are not up to date" - echo "Please run: bash build/autoloaderchecker.sh" - echo "And commit the result" - exit 1 -else - echo "Autoloader up to date. Carry on" - exit 0 -fi --- a/build/build.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- a/build/ca-bundle-etag.txt +++ /dev/null @@ -1 +0,0 @@ -"2e1e4-65c461475e240" --- a/build/ca-bundle-etag.txt.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/build/demi.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: CC0-1.0 - -# This is a simple helper to execute npm COMMANDs in two directories -# we need this as we cannot use npm workspaces as they break with 2 versions of vue. - -COMMAND="" -FRONTEND="$(dirname $0)/frontend" -FRONTEND_LEGACY="$(dirname $0)/frontend-legacy" - -build_command() { - if [ "install" = "$1" ] || [ "ci" = "$1" ]; then - COMMAND=$@ - elif [ "run" = "$1" ]; then - COMMAND="run --if-present ${@:2}" - else - COMMAND="run --if-present $@" - fi -} - -run_parallel() { - npx concurrently \ - "cd \"$FRONTEND\" && npm $COMMAND" \ - "cd \"$FRONTEND_LEGACY\" && npm $COMMAND" -} - -run_sequentially() { - echo -e "\e[1;34m>> Running 'npm $COMMAND' for Vue 3 based frontend\e[0m" - echo - pushd "$FRONTEND" - set +e - npm $COMMAND - STATUS=$? - set -e - popd - - echo -e "\e[1;34m>> Running 'npm $COMMAND' for Vue 2 based frontend\e[0m" - echo - pushd "$FRONTEND_LEGACY" - npm $COMMAND - popd - - if [ $STATUS -ne 0 ]; then - echo -e "\e[1;31m>> Vue 3 based frontend build failed\e[0m" - exit $STATUS - fi -} - -set -e -if [ "--parallel" = "$1" ]; then - build_command ${@:2} - run_parallel -else - build_command $@ - run_sequentially -fi --- a/build/eslint-baseline-legacy.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue": { - "vue/no-mutating-props": { - "count": 2 - } - }, - "apps/files_sharing/src/components/SharingInput.vue": { - "@nextcloud/no-deprecated-library-props": { - "count": 1 - } - }, - "apps/files_sharing/src/views/SharingDetailsTab.vue": { - "vue/no-mutating-props": { - "count": 31 - } - }, - "apps/files_sharing/src/views/SharingLinkList.vue": { - "vue/no-mutating-props": { - "count": 1 - } - }, - "apps/settings/src/components/Users/NewUserDialog.vue": { - "vue/no-mutating-props": { - "count": 2 - } - }, - "apps/workflowengine/src/components/Check.vue": { - "vue/no-mutating-props": { - "count": 3 - } - }, - "apps/workflowengine/src/components/Rule.vue": { - "vue/multi-word-component-names": { - "count": 1 - }, - "vue/no-mutating-props": { - "count": 1 - } - }, - "core/src/views/Login.vue": { - "vue/multi-word-component-names": { - "count": 1 - } - } -} \ No newline at end of file --- a/build/eslint-baseline.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file --- a/build/files-checker.php +++ /dev/null @@ -1,126 +0,0 @@ -getFilename(); -} - -$additionalFiles = array_diff($actualFiles, $expectedFiles); -$missingFiles = array_diff($expectedFiles, $actualFiles); - -$failed = false; -if (count($additionalFiles) > 0) { - echo sprintf('ERROR: There were %d additional files:', count($additionalFiles)) . PHP_EOL; - echo implode(PHP_EOL, $additionalFiles) . PHP_EOL; - $failed = true; -} -if (count($missingFiles) > 0) { - echo sprintf('ERROR: There were %d missing files:', count($missingFiles)) . PHP_EOL; - echo implode(PHP_EOL, $missingFiles) . PHP_EOL; - $failed = true; -} - -if ($failed) { - echo 'ERROR: Please remove or add those files again or inform the release team about those now files to be included or excluded from the release tar ball.' . PHP_EOL; - exit(1); -} - -echo 'OK: all expected files are present and no additional files are there.' . PHP_EOL; -exit(0); --- a/build/frontend-legacy/REUSE.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 1 -SPDX-PackageName = "nextcloud" -SPDX-PackageSupplier = "Nextcloud " -SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server" - -[[annotations]] -path = ["package.json", "package-lock.json", "tsconfig.json"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" --- a/build/frontend-legacy/WebpackSPDXPlugin.cjs +++ /dev/null @@ -1,244 +0,0 @@ -'use strict' - -/** - * Party inspired by https://github.com/FormidableLabs/webpack-stats-plugin - * - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -const { constants } = require('node:fs') -const fs = require('node:fs/promises') -const path = require('node:path') -const webpack = require('webpack') - -class WebpackSPDXPlugin { - #options - - /** - * @param {object} opts Parameters - * @param {Record} opts.override Override licenses for packages - */ - constructor(opts = {}) { - this.#options = { override: {}, ...opts } - } - - apply(compiler) { - compiler.hooks.thisCompilation.tap('spdx-plugin', (compilation) => { - // `processAssets` is one of the last hooks before frozen assets. - // We choose `PROCESS_ASSETS_STAGE_REPORT` which is the last possible - // stage after which to emit. - compilation.hooks.processAssets.tapPromise( - { - name: 'spdx-plugin', - stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT, - }, - () => this.emitLicenses(compilation), - ) - }) - } - - /** - * Find the nearest package.json - * - * @param {string} dir Directory to start checking - */ - async #findPackage(dir) { - if (!dir || dir === '/' || dir === '.') { - return null - } - - const packageJson = `${dir}/package.json` - try { - await fs.access(packageJson, constants.F_OK) - } catch { - return await this.#findPackage(path.dirname(dir)) - } - - const { private: isPrivatePacket, name } = JSON.parse(await fs.readFile(packageJson)) - // "private" is set in internal package.json which should not be resolved but the parent package.json - // Same if no name is set in package.json - if (isPrivatePacket === true || !name) { - return (await this.#findPackage(path.dirname(dir))) ?? packageJson - } - return packageJson - } - - /** - * Emit licenses found in compilation to '.license' files - * - * @param {webpack.Compilation} compilation Webpack compilation object - * @param {*} callback Callback for old webpack versions - */ - async emitLicenses(compilation, callback) { - const logger = compilation.getLogger('spdx-plugin') - // cache the node packages - const packageInformation = new Map() - - const warnings = new Set() - /** @type {Map>} */ - const sourceMap = new Map() - - for (const chunk of compilation.chunks) { - for (const file of chunk.files) { - if (sourceMap.has(file)) { - sourceMap.get(file).add(chunk) - } else { - sourceMap.set(file, new Set([chunk])) - } - } - } - - for (const [asset, chunks] of sourceMap.entries()) { - /** @type {Set} */ - const modules = new Set() - /** - * @param {webpack.Module} module - */ - const addModule = (module) => { - if (module && !modules.has(module)) { - modules.add(module) - for (const dep of module.dependencies) { - addModule(compilation.moduleGraph.getModule(dep)) - } - } - } - chunks.forEach((chunk) => chunk.getModules().forEach(addModule)) - - const sources = [...modules].map((module) => module.identifier()) - .map((source) => { - const skipped = [ - 'delegated', - 'external', - 'container entry', - 'ignored', - 'remote', - 'data:', - ] - // Webpack sources that we can not infer license information or that is not included (external modules) - if (skipped.some((prefix) => source.startsWith(prefix))) { - return '' - } - // Internal webpack sources - if (source.startsWith('webpack/runtime')) { - return require.resolve('webpack') - } - // Handle webpack loaders - if (source.includes('!')) { - return source.split('!').at(-1) - } - if (source.includes('|')) { - return source - .split('|') - .filter((s) => s.startsWith(path.sep)) - .at(0) - } - return source - }) - .filter((s) => !!s) - .map((s) => s.split('?', 2)[0]) - - /** - * packages used by the current asset - * - * @type {Set} - */ - const packages = new Set() - - // packages is the list of packages used by the asset - for (const sourcePath of sources) { - const pkg = await this.#findPackage(path.dirname(sourcePath)) - if (!pkg) { - logger.warn(`No package for source found (${sourcePath})`) - continue - } - - if (!packageInformation.has(pkg)) { - // Get the information from the package - const { author: packageAuthor, name, version, license: packageLicense, licenses } = JSON.parse(await fs.readFile(pkg)) - // Handle legacy packages - let license = !packageLicense && licenses - ? licenses.map((entry) => entry.type ?? entry).join(' OR ') - : packageLicense - if (license?.includes(' ') && !license?.startsWith('(')) { - license = `(${license})` - } - // Handle both object style and string style author - const author = typeof packageAuthor === 'object' - ? `${packageAuthor.name}` + (packageAuthor.mail ? ` <${packageAuthor.mail}>` : '') - : packageAuthor ?? `${name} developers` - - packageInformation.set(pkg, { - version, - // Fallback to directory name if name is not set - name: name ?? path.basename(path.dirname(pkg)), - author, - license, - }) - } - packages.add(pkg) - } - - let output = 'This file is generated from multiple sources. Included packages:\n' - const authors = new Set() - const licenses = new Set() - for (const packageName of [...packages].sort()) { - const pkg = packageInformation.get(packageName) - const license = this.#options.override[pkg.name] ?? pkg.license - // Emit warning if not already done - if (!license && !warnings.has(pkg.name)) { - logger.warn(`Missing license information for package ${pkg.name}, you should add it to the 'override' option.`) - warnings.add(pkg.name) - } - licenses.add(license || 'unknown') - authors.add(pkg.author) - output += `- ${pkg.name}\n\t- version: ${pkg.version}\n\t- license: ${license}\n` - } - output = `\n\n${output}` - - if (sources.length === 0) { - logger.info(`Checking content of asset ${asset}`) - const sources = [...modules].map((module) => module.identifier()) - for (const source of sources) { - const match = source.match(/asset\/inline\|data:image\/svg\+xml,(.+)/) - if (match) { - const content = decodeURI(match[1]) - // REUSE-IgnoreStart - const [, license] = content.match(/SPDX-License-Identifier:\s*([^\s]+)/) ?? [] - const [, author] = content.match(/SPDX-FileCopyrightText:\s*([^-]+)/) ?? [] - // REUSE-IgnoreEnd - if (author && license) { - authors.add(author) - licenses.add(license) - } - } - } - if (authors.size === 0) { - logger.warn(`Asset ${asset} does not contain any source information`) - continue - } - output = '' - } - - // REUSE-IgnoreStart - for (const author of [...authors].sort()) { - output = `SPDX-FileCopyrightText: ${author}\n${output}` - } - for (const license of [...licenses].sort()) { - output = `SPDX-License-Identifier: ${license}\n${output}` - } - // REUSE-IgnoreEnd - - compilation.emitAsset( - asset.split('?', 2)[0] + '.license', - new webpack.sources.RawSource(output), - ) - } - - if (callback) { - return callback() - } - } -} - -module.exports = WebpackSPDXPlugin --- a/build/frontend-legacy/__mocks__/@nextcloud/auth.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function getCurrentUser() { - return { - uid: 'test', - displayName: 'Test', - isAdmin: false, - } -} - -export function getRequestToken() { - return 'test-token-1234' -} - -export function onRequestTokenUpdate() {} --- a/build/frontend-legacy/__mocks__/@nextcloud/axios.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default { - interceptors: { - response: { - use: () => {}, - }, - request: { - use: () => {}, - }, - }, - get: async () => ({ status: 200, data: {} }), - delete: async () => ({ status: 200, data: {} }), - post: async () => ({ status: 200, data: {} }), - head: async () => ({ status: 200, data: {} }), -} --- a/build/frontend-legacy/__mocks__/@nextcloud/capabilities.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { Capabilities } from '../../apps/files/src/types.ts' - -export function getCapabilities(): Capabilities { - return { - files: { - bigfilechunking: true, - blacklisted_files: [], - forbidden_filename_basenames: [], - forbidden_filename_characters: [], - forbidden_filename_extensions: [], - forbidden_filenames: [], - undelete: true, - version_deletion: true, - version_labeling: true, - versioning: true, - }, - } -} --- a/build/frontend-legacy/__mocks__/@nextcloud/dialogs.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { vi } from 'vitest' - -export const showMessage = vi.fn() -export const showSuccess = vi.fn() -export const showWarning = vi.fn() -export const showInfo = vi.fn() -export const showError = vi.fn() -export const showUndo = vi.fn() --- a/build/frontend-legacy/__mocks__/@nextcloud/initial-state.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function loadState(app: string, key: string, fallback?: any) { - return fallback -} --- a/build/frontend-legacy/__mocks__/css.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default {} --- a/build/frontend-legacy/__mocks__/svg.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default 'SvgMock' --- a/build/frontend-legacy/__mocks__/webdav.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function createClient() {} -export function getPatcher() { - return { - patch: () => {}, - } -} --- a/build/frontend-legacy/__tests__/FileSystemAPIUtils.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import mime from 'mime' -import { basename } from 'node:path' - -class FileSystemEntry { - private _isFile: boolean - private _fullPath: string - - constructor(isFile: boolean, fullPath: string) { - this._isFile = isFile - this._fullPath = fullPath - } - - get isFile() { - return !!this._isFile - } - - get isDirectory() { - return !this.isFile - } - - get name() { - return basename(this._fullPath) - } -} - -export class FileSystemFileEntry extends FileSystemEntry { - private _contents: string - private _lastModified: number - - constructor(fullPath: string, contents: string, lastModified = Date.now()) { - super(true, fullPath) - this._contents = contents - this._lastModified = lastModified - } - - file(success: (file: File) => void) { - const lastModified = this._lastModified - // Faking the mime by using the file extension - const type = mime.getType(this.name) || '' - success(new File([this._contents], this.name, { lastModified, type })) - } -} - -export class FileSystemDirectoryEntry extends FileSystemEntry { - private _entries: FileSystemEntry[] - - constructor(fullPath: string, entries: FileSystemEntry[]) { - super(false, fullPath) - this._entries = entries || [] - } - - createReader() { - let read = false - return { - readEntries: (success: (entries: FileSystemEntry[]) => void) => { - if (read) { - return success([]) - } - read = true - success(this._entries) - }, - } - } -} - -/** - * This mocks the File API's File class - * It will allow us to test the Filesystem API as well as the - * File API in the same test suite. - */ -export class DataTransferItem { - private _type: string - private _entry: FileSystemEntry - - getAsEntry?: () => FileSystemEntry - - constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) { - this._type = type - this._entry = entry - - // Only when the Files API is available we are - // able to get the entry - if (isFileSystemAPIAvailable) { - this.getAsEntry = () => this._entry - } - } - - get kind() { - return 'file' - } - - get type() { - return this._type - } - - getAsFile(): File | null { - if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) { - let file: File | null = null - this._entry.file((f) => { - file = f - }) - return file - } - - // The browser will return an empty File object if the entry is a directory - return new File([], this._entry.name, { type: '' }) - } -} - -export function fileSystemEntryToDataTransferItem(entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem { - return new DataTransferItem( - entry.isFile ? 'text/plain' : 'httpd/unix-directory', - entry, - isFileSystemAPIAvailable, - ) -} --- a/build/frontend-legacy/__tests__/mock-window.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -window.OC = { - ...window.OC, - config: { - version: '32.0.0', - ...(window.OC?.config ?? {}), - }, -} -window.OCA = { ...window.OCA } -window.OCP = { ...window.OCP } - -window._oc_webroot = '' - -// jsdom does not implement `innerText` at all, while the specification defines it -// to fall back to `textContent` for elements that are not being rendered. -// @see https://github.com/jsdom/jsdom/issues/1245 -if (!('innerText' in HTMLElement.prototype)) { - Object.defineProperty(HTMLElement.prototype, 'innerText', { - configurable: true, - get() { - return this.textContent - }, - set(value) { - this.textContent = value - }, - }) -} --- a/build/frontend-legacy/__tests__/setup-global.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ -export function setup() { - process.env.TZ = 'UTC' -} --- a/build/frontend-legacy/__tests__/setup-testing-library.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ -import '@testing-library/jest-dom/vitest' -import 'core-js/stable/index.js' --- a/build/frontend-legacy/apps/files +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files \ No newline at end of file --- a/build/frontend-legacy/apps/files_sharing +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files_sharing \ No newline at end of file --- a/build/frontend-legacy/apps/settings +++ /dev/null @@ -1 +0,0 @@ -../../../apps/settings \ No newline at end of file --- a/build/frontend-legacy/apps/updatenotification +++ /dev/null @@ -1 +0,0 @@ -../../../apps/updatenotification \ No newline at end of file --- a/build/frontend-legacy/apps/weather_status +++ /dev/null @@ -1 +0,0 @@ -../../../apps/weather_status \ No newline at end of file --- a/build/frontend-legacy/apps/workflowengine +++ /dev/null @@ -1 +0,0 @@ -../../../apps/workflowengine \ No newline at end of file --- a/build/frontend-legacy/babel.config.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -module.exports = { - plugins: [ - '@babel/plugin-syntax-dynamic-import', - '@babel/plugin-transform-class-properties', - '@babel/plugin-transform-private-methods', - // We need the bundler entry not the web one - // Jest will otherwise resolve the wrong one - [ - 'module-resolver', - { - alias: { - webdav$: 'webdav/dist/node/index.js', - }, - }, - ], - ], - presets: [ - // https://babeljs.io/docs/en/babel-preset-typescript - '@babel/preset-typescript', - [ - '@babel/preset-env', - { - useBuiltIns: false, - modules: 'auto', - }, - ], - ], -} --- a/build/frontend-legacy/core +++ /dev/null @@ -1 +0,0 @@ -../../core \ No newline at end of file --- a/build/frontend-legacy/eslint.config.mjs +++ /dev/null @@ -1,64 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { recommendedVue2 } from '@nextcloud/eslint-config' -import { defineConfig } from 'eslint/config' -import * as globals from 'globals' - -export default defineConfig([ - { - linterOptions: { - reportUnusedDisableDirectives: 'error', - reportUnusedInlineConfigs: 'error', - }, - }, - - ...recommendedVue2, - - { - name: 'server/custom-webpack-globals', - files: ['**/*.js', '**/*.ts', '**/*.vue'], - languageOptions: { - globals: { - PRODUCTION: 'readonly', - }, - }, - }, - - { - name: 'server/scripts-are-cjs', - files: [ - '*.js', - 'build/**/*.js', - '**/core/src/icons.cjs', - ], - - languageOptions: { - globals: { - ...globals.es2023, - ...globals.node, - }, - }, - - rules: { - 'no-console': 'off', - 'jsdoc/require-jsdoc': 'off', - }, - }, - // customer server ignore files - { - name: 'server/ignored-files', - ignores: [ - '.devcontainer/', - 'composer.json', - '**/*.php', - '3rdparty/', - 'tests/', // PHP tests - '**/js/', - '**/l10n/', // all translations (config only ignored in root) - '**/vendor/', // different vendors - ], - }, -]) --- a/build/frontend-legacy/package-lock.json +++ /dev/null @@ -1,21708 +0,0 @@ -{ - "name": "nextcloud-ui-legacy", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nextcloud-ui-legacy", - "version": "1.0.0", - "hasInstallScript": true, - "license": "AGPL-3.0-or-later", - "dependencies": { - "@chenfengyuan/vue-qrcode": "^1.0.2", - "@mdi/js": "^7.4.47", - "@mdi/svg": "^7.4.47", - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/dialogs": "^7.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/files": "^4.1.0", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/moment": "^1.3.5", - "@nextcloud/password-confirmation": "^6.1.0", - "@nextcloud/paths": "^3.1.0", - "@nextcloud/router": "^3.2.0", - "@nextcloud/sharing": "^1.0.0-beta.4", - "@nextcloud/upload": "^1.12.0", - "@nextcloud/vue": "^8.41.0", - "@simplewebauthn/browser": "^13.3.0", - "@vue/web-component-wrapper": "^1.3.0", - "@vueuse/components": "^11.3.0", - "@vueuse/core": "^11.3.0", - "@vueuse/integrations": "^11.3.0", - "blurhash": "^2.0.5", - "browserslist-useragent-regexp": "^4.1.4", - "camelcase": "^9.0.0", - "cancelable-promise": "^4.3.1", - "color": "^5.0.3", - "core-js": "^3.50.0", - "crypto-browserify": "^3.12.1", - "debounce": "^3.0.0", - "dompurify": "^3.4.16", - "escape-html": "^1.0.3", - "focus-trap": "^8.2.2", - "is-svg": "^6.1.0", - "libphonenumber-js": "^1.13.14", - "lodash": "^4.18.1", - "marked": "^18.0.14", - "moment": "^2.31.0", - "moment-timezone": "^0.6.4", - "p-limit": "^7.3.3", - "p-queue": "^9.3.3", - "path": "^0.12.7", - "pinia": "^2.3.1", - "query-string": "^9.5.1", - "regenerator-runtime": "^0.14.1", - "throttle-debounce": "^5.0.2", - "v-click-outside": "^3.2.0", - "v-tooltip": "^2.1.3", - "vue": "^2.7.16", - "vue-click-outside": "^1.1.0", - "vue-cropperjs": "^4.2.0", - "vue-frag": "^1.4.3", - "vue-infinite-loading": "^2.4.5", - "vue-localstorage": "^0.6.2", - "vue-material-design-icons": "^5.3.1", - "vue-router": "^3.6.5", - "vue2-teleport": "^1.2.2", - "vuedraggable": "^2.24.3", - "vuex": "^3.6.2", - "vuex-router-sync": "^5.0.0", - "webdav": "^5.11.0" - }, - "devDependencies": { - "@babel/node": "^7.29.7", - "@babel/plugin-transform-private-methods": "^7.29.7", - "@babel/preset-typescript": "^7.29.7", - "@nextcloud/babel-config": "^1.3.0", - "@nextcloud/typings": "^1.10.0", - "@nextcloud/webpack-vue-config": "^7.0.4", - "@pinia/testing": "^0.1.7", - "@testing-library/jest-dom": "^7.0.1", - "@testing-library/user-event": "^14.6.7", - "@testing-library/vue": "^5.8.3", - "@types/throttle-debounce": "^5.0.2", - "@vitejs/plugin-vue2": "^2.3.4", - "@vitest/coverage-v8": "^4.1.11", - "@vue/test-utils": "^1.3.5", - "@vue/tsconfig": "~0.6.0", - "babel-loader-exclude-node-modules-except": "^1.2.4", - "babel-plugin-module-resolver": "^5.0.3", - "colord": "^2.10.0", - "exports-loader": "^5.0.0", - "file-loader": "^6.2.0", - "handlebars-loader": "^1.7.3", - "mime": "^4.1.0", - "msw": "^2.15.0", - "patch-package": "^8.0.1", - "raw-loader": "^4.0.2", - "regextras": "^0.8.0", - "sass": "^1.105.0", - "terser-webpack-plugin": "^5.6.1", - "typescript": "^5.9.2", - "vite": "^7.3.6", - "vitest": "^4.1.9", - "vue-loader": "^15.11.1", - "vue-template-compiler": "^2.7.16", - "webpack": "^5.111.1", - "webpack-cli": "^6.0.1", - "webpack-merge": "^6.0.1", - "workbox-webpack-plugin": "^7.4.1" - }, - "engines": { - "node": "^24.0.0", - "npm": "^11.3.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", - "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/helper-replace-supers": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", - "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", - "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", - "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/node": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.29.7.tgz", - "integrity": "sha512-nfdPXz8/mD3/t+1nE1DKwGR14Ccjt5xeF7u3g7sqWnLi4yR6n+9Z0kThIROF8SRM07ZKpEtiWSKpWKxsMiJeew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/register": "^7.29.7", - "commander": "^6.2.0", - "core-js": "^3.48.0", - "node-environment-flags": "^1.0.5", - "regenerator-runtime": "^0.14.0", - "v8flags": "^3.1.1" - }, - "bin": { - "babel-node": "bin/babel-node.js" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", - "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", - "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", - "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", - "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", - "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/plugin-syntax-typescript": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", - "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "@babel/plugin-syntax-jsx": "^7.29.7", - "@babel/plugin-transform-modules-commonjs": "^7.29.7", - "@babel/plugin-transform-typescript": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/register": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", - "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.6", - "source-map-support": "^0.5.16" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@buttercup/fetch": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", - "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", - "license": "MIT", - "optionalDependencies": { - "node-fetch": "^3.3.0" - } - }, - "node_modules/@chenfengyuan/vue-qrcode": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@chenfengyuan/vue-qrcode/-/vue-qrcode-1.0.2.tgz", - "integrity": "sha512-hwy1d4YMJAyEh+V7dLPG8eAKACRvugzSB4ylwb6QNqo84KHTF50/5EJcBYdUhTRPfAqrxG0i6jDAXONWOGyQbQ==", - "license": "MIT", - "dependencies": { - "qrcode": "^1.4.4" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@file-type/xml": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@file-type/xml/-/xml-0.4.4.tgz", - "integrity": "sha512-NhCyXoHlVZ8TqM476hyzwGJ24+D5IPSaZhmrPj7qXnEVb3q6jrFzA3mM9TBpknKSI9EuQeGTKRg2DXGUwvBBoQ==", - "license": "MIT", - "dependencies": { - "sax": "^1.4.1", - "strtok3": "^10.3.4" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "license": "MIT" - }, - "node_modules/@inquirer/ansi": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", - "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/confirm": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", - "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.9", - "@inquirer/type": "^4.0.5" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "11.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", - "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", - "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", - "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.1.tgz", - "integrity": "sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "@jsonjoy.com/fs-node-utils": "4.68.1", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.1.tgz", - "integrity": "sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.68.1", - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "@jsonjoy.com/fs-node-utils": "4.68.1", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.1.tgz", - "integrity": "sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.68.1", - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "@jsonjoy.com/fs-node-utils": "4.68.1", - "@jsonjoy.com/fs-print": "4.68.1", - "@jsonjoy.com/fs-snapshot": "4.68.1", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.1.tgz", - "integrity": "sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.1.tgz", - "integrity": "sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.68.1", - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "@jsonjoy.com/fs-node-utils": "4.68.1" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.1.tgz", - "integrity": "sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "glob-to-regex.js": "^1.0.1" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.1.tgz", - "integrity": "sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.68.1", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.1.tgz", - "integrity": "sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.68.1", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/util": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@linusborg/vue-simple-portal": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@linusborg/vue-simple-portal/-/vue-simple-portal-0.1.5.tgz", - "integrity": "sha512-dq+oubEVW4UabBoQxmH97GiDa+F6sTomw4KcXFHnXEpw69rdkXFCxo1WzwuvWjoLiUVYJTyN1dtlUvTa50VcXg==", - "license": "Apache-2.0", - "dependencies": { - "nanoid": "^3.1.20" - }, - "peerDependencies": { - "vue": "^2.6.6" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", - "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", - "license": "BSD-2-Clause", - "dependencies": { - "unist-util-visit": "^1.4.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "license": "MIT" - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "license": "MIT", - "dependencies": { - "unist-util-is": "^3.0.0" - } - }, - "node_modules/@mdi/js": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", - "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", - "license": "Apache-2.0" - }, - "node_modules/@mdi/svg": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.4.47.tgz", - "integrity": "sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw==", - "license": "Apache-2.0" - }, - "node_modules/@mswjs/interceptors": { - "version": "0.41.8", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", - "integrity": "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nextcloud/auth": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.6.0.tgz", - "integrity": "sha512-VkT87+9UqpPi7O36bVEE4/MxWF8d90VQcuMlvKltsZyLSLkEGrPXgowtD75Y54k60/8SR6mXbeqBwapi8dDUbA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/router": "^3.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/axios": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.6.0.tgz", - "integrity": "sha512-ehcIgyora8DAJ+STG6iFI4e+ufPVFrIA6o0FgMKeKdfyaxRJ9UM7L+n7V+rc/qv8sDiWC/hWIKwFtLw2W5yE4Q==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.6.0", - "axios": "^1.15.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/babel-config": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/babel-config/-/babel-config-1.3.0.tgz", - "integrity": "sha512-qk4mBJahzp2mkiizU9RbeABa6JhqSwR43SXptNQhM3kpxAuP2OAQQhomYnxog/XfFcYExZzOkgRBPlcLEoik0w==", - "dev": true, - "license": "AGPL-3.0-or-later", - "engines": { - "node": "^20 || ^22 || ^24" - }, - "peerDependencies": { - "@babel/core": "^7.27.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.27.2" - } - }, - "node_modules/@nextcloud/browser-storage": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.5.0.tgz", - "integrity": "sha512-usYr4GlJQlK3hgZURvklqWb9ivi7sgsSuFqXrs7s4hl1LTS4enzPrnkQumm6nRsQruf0ITS+OBsK+oELEbvYPA==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^24 || ^22 || ^20" - } - }, - "node_modules/@nextcloud/capabilities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@nextcloud/capabilities/-/capabilities-1.2.1.tgz", - "integrity": "sha512-snZ0/910zzwN6PDsIlx2Uvktr1S5x0ClhDUnfPlCj7ntNvECzuVHNY5wzby22LIkc+9ZjaDKtCwuCt2ye+9p/Q==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/dialogs": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-7.5.0.tgz", - "integrity": "sha512-2gmv0V3ObphxRd7ZvXwTsPvn/XSf2vummdXb2LRFpkoSamWbk+41SWv7cnDQPC+Ve9p46WebDuqyG+wbefkhEw==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@mdi/js": "^7.4.47", - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/files": "^4.0.0", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/paths": "^3.1.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue": "^9.9.0", - "@vueuse/core": "^14.4.0", - "p-queue": "^9.3.3", - "vue": "^3.5.41", - "webdav": "^5.10.0" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/generator": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", - "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^8.0.0", - "@babel/types": "^8.0.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "@types/jsesc": "^2.5.0", - "jsesc": "^3.0.2" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", - "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", - "license": "MIT", - "dependencies": { - "@babel/types": "^8.0.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@ckpack/vue-color": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.6.0.tgz", - "integrity": "sha512-b9kFTKhYbNArfgP1lmnaVm0VNsWdZjqIbyHUYry7mZ+E7JeTQclbjq1+2xWn0SE3wzqRYlXmAVjECPOgteWmMQ==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^3.6.0", - "material-colors": "^1.2.6" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/vue": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.9.0.tgz", - "integrity": "sha512-TmKnBWp6Aiw+cm+WamwX5cMkhkpJqLSUKrTUB5I5Y9RDt2S0TUlwRaTe9vltBIV3qmKParhpZQ3/ewWHgEQlrQ==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@ckpack/vue-color": "^1.6.0", - "@floating-ui/dom": "^1.8.0", - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue-select": "^4.1.0", - "@vuepic/vue-datepicker": "^11.0.3", - "@vueuse/components": "^14.3.0", - "@vueuse/core": "^14.3.0", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^3.0.0", - "dompurify": "^3.4.12", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^5.2.2", - "focus-trap": "^8.2.2", - "linkifyjs": "^4.3.3", - "mdast-util-to-string": "^4.0.0", - "p-queue": "^9.3.1", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^8.0.0", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^4.0.4", - "striptags": "^3.2.0", - "tabbable": "^6.4.0", - "tributejs": "^5.1.3", - "ts-md5": "^2.0.1", - "unified": "^11.0.5", - "unist-builder": "^4.0.0", - "unist-util-visit-parents": "^6.0.2", - "vue": "^3.5.18", - "vue-router": "^5.1.0" - }, - "engines": { - "node": "^20.11.0 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/vue-select": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-4.1.0.tgz", - "integrity": "sha512-jQIu4XuUAuJr6qL/IKa63h3Vv/4OrP9latl2E6kqtffwLBV+qMSU4Gm+vsOfyqBbBSY4i3eeMfdyJi14O1Yqbg==", - "license": "MIT", - "engines": { - "node": "^22 || ^24" - }, - "peerDependencies": { - "vue": "^3" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/compiler-core": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", - "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.41", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/compiler-dom": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", - "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/compiler-sfc": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", - "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/compiler-core": "3.5.41", - "@vue/compiler-dom": "3.5.41", - "@vue/compiler-ssr": "3.5.41", - "@vue/shared": "3.5.41", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.19", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/compiler-ssr": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", - "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/devtools-api": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", - "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.1.5" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/devtools-kit": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", - "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.1.5", - "birpc": "^2.6.1", - "hookable": "^5.5.3", - "perfect-debounce": "^2.0.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/devtools-shared": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", - "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", - "license": "MIT" - }, - "node_modules/@nextcloud/dialogs/node_modules/@vue/shared": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", - "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", - "license": "MIT" - }, - "node_modules/@nextcloud/dialogs/node_modules/@vuepic/vue-datepicker": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-11.0.3.tgz", - "integrity": "sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==", - "license": "MIT", - "dependencies": { - "date-fns": "^4.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "vue": ">=3.3.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/components": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-14.3.0.tgz", - "integrity": "sha512-jnrJrecSfa8H+G6wtAwsCnMtKbKZDSpu5JZDuulZikWrHb6uuS5SyXP6M2b79tofxipm78VWDSzW+58pu1yglA==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/components/node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/components/node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/core": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", - "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.4.0", - "@vueuse/shared": "14.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/core/node_modules/@vueuse/shared": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", - "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/metadata": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", - "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/ast-walker-scope": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", - "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@babel/types": "^7.29.0", - "ast-kit": "^2.2.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@nextcloud/dialogs/node_modules/floating-vue": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", - "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "~1.1.1", - "vue-resize": "^2.0.0-alpha.1" - }, - "peerDependencies": { - "@nuxt/kit": "^3.2.0", - "vue": "^3.2.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/@nextcloud/dialogs/node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", - "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.1.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "license": "MIT" - }, - "node_modules/@nextcloud/dialogs/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/pinia": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz", - "integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "nostics": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@vue/devtools-api": "^8.1.5", - "typescript": ">=5.6.0", - "vue": "^3.5.11" - }, - "peerDependenciesMeta": { - "@vue/devtools-api": { - "optional": false - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/dialogs/node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/rehype-react": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz", - "integrity": "sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/splitpanes": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.1.2.tgz", - "integrity": "sha512-frNchoCv7w0nMQEuaDGgMGMU6jv3NhN6bBGQVfCNgwJdVcYaRmi1dwYDjp7SifAoUFdiQjBRB254LJNidzo15Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antoniandre" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/vue": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", - "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.41", - "@vue/compiler-sfc": "3.5.41", - "@vue/runtime-dom": "3.5.41", - "@vue/server-renderer": "3.5.41", - "@vue/shared": "3.5.41" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/dialogs/node_modules/vue-resize": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", - "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/vue-router": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", - "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^8.0.0", - "@vue-macros/common": "^3.1.3", - "@vue/devtools-api": "^8.1.5", - "ast-walker-scope": "^0.9.0", - "chokidar": "^5.0.0", - "json5": "^2.2.3", - "local-pkg": "^1.2.1", - "magic-string": "^0.30.21", - "mlly": "^1.8.2", - "muggle-string": "^0.4.1", - "nostics": "^1.1.4", - "pathe": "^2.0.3", - "picomatch": "^4.0.5", - "scule": "^1.3.0", - "tinyglobby": "^0.2.17", - "unplugin": "^3.3.0", - "unplugin-utils": "^0.3.2", - "yaml": "^2.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", - "pinia": "^3.0.4 || ^4.0.2", - "vite": "^7.3.0 || ^8.0.0", - "vue": "^3.5.34 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@pinia/colada": { - "optional": true - }, - "@vue/compiler-sfc": { - "optional": true - }, - "pinia": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@nextcloud/event-bus": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-3.3.3.tgz", - "integrity": "sha512-zIfvKmUGkXpVzRKoXrcO9hkoiKDm65fqNxy/XIbIxrQhZByPq3gDkjBpnu3V5Gs8JdYwa73R8DjzV9oH8HYhIg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@types/semver": "^7.7.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/event-bus/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nextcloud/files": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.1.0.tgz", - "integrity": "sha512-C45fxBxU9v5HQ437I+r4EuqG/A9CYqNnYM34ltdxFNSBMjId7DoRJrk5UDA7QsC4eB/CP1xNxFSpkY+g6RPWXA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.1.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "axios-retry": "^4.5.0", - "is-svg": "^6.1.0", - "p-queue": "^9.3.3", - "typescript-event-target": "^1.1.2", - "webdav": "^5.10.0" - }, - "engines": { - "node": "^24.0.0 || >=26.0.0" - } - }, - "node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/l10n": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", - "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", - "dependencies": { - "@nextcloud/router": "^3.0.1", - "@nextcloud/typings": "^1.9.1", - "@types/escape-html": "^1.0.4", - "dompurify": "^3.2.6", - "escape-html": "^1.0.3" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/logger": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-3.0.3.tgz", - "integrity": "sha512-TcbVRL4/O5ffI1RXFmQAFD3gwwT15AAdr1770x+RNqVvfBdoGVyhzOwCIyA5Vfc3fA1iJXFa+rE6buJZSoqlcw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/moment": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.3.5.tgz", - "integrity": "sha512-sQjQ/D40sdedtq4ywuANSxqG0VhIB/i+QtZ6Voz3nyZWhkyyqGxdj9rQu9AyEvas3/Jlspdom5chc+a8jOmHxQ==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/l10n": "^3.4.0", - "moment": "^2.30.1" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/password-confirmation": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/password-confirmation/-/password-confirmation-6.1.0.tgz", - "integrity": "sha512-N2ChXDbPcUcQCoAjj1yc65zfc61yiKpdM3qm/y3Y/y//NG7XWNNINwBg85lqJ2SISgmVStpsQ1d0blpFCoQXkQ==", - "license": "MIT", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/vue": "^9.6.0", - "vue": "^3.5.30" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@ckpack/vue-color": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.6.0.tgz", - "integrity": "sha512-b9kFTKhYbNArfgP1lmnaVm0VNsWdZjqIbyHUYry7mZ+E7JeTQclbjq1+2xWn0SE3wzqRYlXmAVjECPOgteWmMQ==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^3.6.0", - "material-colors": "^1.2.6" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@nextcloud/vue": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.6.0.tgz", - "integrity": "sha512-RZuMnrNwzajx3AbJcGHC49NUORSPHMRbzhHCBlR0Z5N3BvUmy5d7MPVTqsmJXiO5emSCTanJ+rwDeo6HTAX3ng==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@ckpack/vue-color": "^1.6.0", - "@floating-ui/dom": "^1.7.6", - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@vuepic/vue-datepicker": "^11.0.3", - "@vueuse/components": "^14.2.1", - "@vueuse/core": "^14.2.1", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^3.0.0", - "dompurify": "^3.3.3", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^5.2.2", - "focus-trap": "^8.0.0", - "linkifyjs": "^4.3.2", - "p-queue": "^9.1.0", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^8.0.0", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-stringify": "^11.0.0", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^4.0.4", - "striptags": "^3.2.0", - "tabbable": "^6.4.0", - "tributejs": "^5.1.3", - "ts-md5": "^2.0.1", - "unified": "^11.0.5", - "unist-builder": "^4.0.0", - "unist-util-visit": "^5.1.0", - "vue": "^3.5.18", - "vue-router": "^5.0.3", - "vue-select": "^4.0.0-beta.6" - }, - "engines": { - "node": "^20.11.0 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.8", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/devtools-api": { - "version": "7.7.10", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", - "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vue/devtools-kit": "^7.7.10" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/devtools-shared": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.0.tgz", - "integrity": "sha512-h8uCb4Qs8UT8VdTT5yjY6tOJ//qH7EpxToixR0xqejR55t5OdISIg7AJ7eBkhBs8iu1qG5gY3QQNN1DF1EelAA==", - "license": "MIT" - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.30" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", - "csstype": "^3.2.3" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" - }, - "peerDependencies": { - "vue": "3.5.30" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", - "license": "MIT" - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vuepic/vue-datepicker": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-11.0.3.tgz", - "integrity": "sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==", - "license": "MIT", - "dependencies": { - "date-fns": "^4.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "vue": ">=3.3.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vueuse/components": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-14.2.1.tgz", - "integrity": "sha512-wB0SvwJ22mNm1hWCMI1wTWz4x55nDTugT5RIg/KCwlWc1vITWL6ry5VTU3SQzsMD2XcazJK8Be1siIsrBb/Vcw==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "14.2.1", - "@vueuse/shared": "14.2.1" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vueuse/core": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", - "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.2.1", - "@vueuse/shared": "14.2.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vueuse/metadata": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", - "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/@vueuse/shared": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", - "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/floating-vue": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", - "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "~1.1.1", - "vue-resize": "^2.0.0-alpha.1" - }, - "peerDependencies": { - "@nuxt/kit": "^3.2.0", - "vue": "^3.2.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", - "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.1.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "license": "MIT" - }, - "node_modules/@nextcloud/password-confirmation/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/pinia": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", - "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vue/devtools-api": "^7.7.7" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "typescript": ">=4.5.0", - "vue": "^3.5.11" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/rehype-react": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz", - "integrity": "sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/splitpanes": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.0.4.tgz", - "integrity": "sha512-RbysugZhjbCw5fgplvk3hOXr41stahQDtZhHVkhnnJI6H4wlGDhM2kIpbehy7v92duy9GnMa8zIhHigIV1TWtg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antoniandre" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue-resize": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", - "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue-router": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", - "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.28.6", - "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.0.6", - "ast-walker-scope": "^0.8.3", - "chokidar": "^5.0.0", - "json5": "^2.2.3", - "local-pkg": "^1.1.2", - "magic-string": "^0.30.21", - "mlly": "^1.8.0", - "muggle-string": "^0.4.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "scule": "^1.3.0", - "tinyglobby": "^0.2.15", - "unplugin": "^3.0.0", - "unplugin-utils": "^0.3.1", - "yaml": "^2.8.2" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.17", - "pinia": "^3.0.4", - "vue": "^3.5.0" - }, - "peerDependenciesMeta": { - "@pinia/colada": { - "optional": true - }, - "@vue/compiler-sfc": { - "optional": true - }, - "pinia": { - "optional": true - } - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.0.tgz", - "integrity": "sha512-O44X57jjkLKbLEc4OgL/6fEPOOanRJU8kYpCE8qfKlV96RQZcdzrcLI5mxMuVRUeXhHKIHGhCpHacyCk0HyO4w==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.1.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue-router/node_modules/@vue/devtools-kit": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.0.tgz", - "integrity": "sha512-/NZlS4WtGIB54DA/z10gzk+n/V7zaqSzYZOVlg2CfdnpIKdB61bd7JDIMxf/zrtX41zod8E2/bbEBoW/d7x70Q==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.1.0", - "birpc": "^2.6.1", - "hookable": "^5.5.3", - "perfect-debounce": "^2.0.0" - } - }, - "node_modules/@nextcloud/password-confirmation/node_modules/vue-select": { - "version": "4.0.0-beta.6", - "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz", - "integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==", - "license": "MIT", - "peerDependencies": { - "vue": "3.x" - } - }, - "node_modules/@nextcloud/paths": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.1.0.tgz", - "integrity": "sha512-vtFYA/kthaUDzu6KejTOL1OwnOy7/yynq5zdB/UBpYacAWjUX5Ddh4OMWx3rEavkBJ9/QGhrFryNJLjNfe8OQA==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/router": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.2.0.tgz", - "integrity": "sha512-an4gJ7mlDZGtYq8zTxp4Sg+iLlKKGvQ9ZH3Dp6ga/IlDNAP7VeMVGphsY798ORjncAp9+sEf370L5zpsnuxgng==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/typings": "^1.10.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/sharing": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-1.0.0-beta.4.tgz", - "integrity": "sha512-IhfqfgmFSg6LMYfOhhVqeyGAkgp2D80htf3+6hoESoqFFl2672IiurIH3S6oMYCLEcfIw7iXuKZ2MMOAK9i3eQ==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@mdi/svg": "^7.4.47", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/dialogs": "^7.5.0", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/vue": "^9.11.0", - "debounce": "^3.0.0", - "is-svg": "^6.1.0", - "qrcode.vue": "^3.10.0", - "uuid": "^14.0.2", - "vue": "^3.5.0" - }, - "engines": { - "node": "^22.0.0 || ^24.0.0 || >=26" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@ckpack/vue-color": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.6.0.tgz", - "integrity": "sha512-b9kFTKhYbNArfgP1lmnaVm0VNsWdZjqIbyHUYry7mZ+E7JeTQclbjq1+2xWn0SE3wzqRYlXmAVjECPOgteWmMQ==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^3.6.0", - "material-colors": "^1.2.6" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@nextcloud/vue": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.11.0.tgz", - "integrity": "sha512-LRsyU9Mxs0b2xWqbxygps8O7/i1z4QhNQD9/QDni+VqFY3V0N95mAgmKJm6vfoFuxTAMXERp9bjqFn3UV542gw==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@ckpack/vue-color": "^1.6.0", - "@floating-ui/dom": "^1.8.0", - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue-select": "^4.1.0", - "@vuepic/vue-datepicker": "^11.0.3", - "@vueuse/components": "^14.4.0", - "@vueuse/core": "^14.4.0", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^3.0.0", - "dompurify": "^3.4.14", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^5.2.2", - "focus-trap": "^8.2.2", - "linkifyjs": "^4.3.3", - "mdast-util-to-string": "^4.0.0", - "p-queue": "^9.3.3", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^8.0.0", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^4.0.4", - "striptags": "^3.2.0", - "tabbable": "^6.4.0", - "tributejs": "^5.1.3", - "ts-md5": "^2.0.1", - "unified": "^11.0.5", - "unist-builder": "^4.0.0", - "unist-util-visit-parents": "^6.0.2", - "vue": "^3.5.18", - "vue-router": "^5.2.0" - }, - "engines": { - "node": "^20.11.0 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@nextcloud/vue-select": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-4.1.0.tgz", - "integrity": "sha512-jQIu4XuUAuJr6qL/IKa63h3Vv/4OrP9latl2E6kqtffwLBV+qMSU4Gm+vsOfyqBbBSY4i3eeMfdyJi14O1Yqbg==", - "license": "MIT", - "engines": { - "node": "^22 || ^24" - }, - "peerDependencies": { - "vue": "^3" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/devtools-api": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", - "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.2.1" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/devtools-kit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", - "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.2.1", - "birpc": "^2.6.1", - "hookable": "^5.5.3", - "perfect-debounce": "^2.0.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/devtools-shared": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", - "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", - "license": "MIT" - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/reactivity": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", - "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.42" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/runtime-core": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", - "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.42", - "@vue/shared": "3.5.42" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/runtime-dom": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", - "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.42", - "@vue/runtime-core": "3.5.42", - "@vue/shared": "3.5.42", - "csstype": "^3.2.3" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vue/server-renderer": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", - "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.42", - "@vue/runtime-dom": "3.5.42", - "@vue/shared": "3.5.42" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vuepic/vue-datepicker": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-11.0.3.tgz", - "integrity": "sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==", - "license": "MIT", - "dependencies": { - "date-fns": "^4.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "vue": ">=3.3.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vueuse/components": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-14.4.0.tgz", - "integrity": "sha512-USgxljmrGUTjtMJcOCbjU7SlRP6K1i5inM6JgMD8bOBn/oL4dK0XYTtNfWyuo0FgUhtCftIKXi+4vJ8ml+tmqw==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "14.4.0", - "@vueuse/shared": "14.4.0" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vueuse/core": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", - "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.4.0", - "@vueuse/shared": "14.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vueuse/metadata": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", - "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@vueuse/shared": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", - "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/ast-walker-scope": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", - "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@babel/types": "^7.29.0", - "ast-kit": "^2.2.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/@nextcloud/sharing/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@nextcloud/sharing/node_modules/floating-vue": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", - "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "~1.1.1", - "vue-resize": "^2.0.0-alpha.1" - }, - "peerDependencies": { - "@nuxt/kit": "^3.2.0", - "vue": "^3.2.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/@nextcloud/sharing/node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", - "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.1.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "license": "MIT" - }, - "node_modules/@nextcloud/sharing/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@nextcloud/sharing/node_modules/pinia": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz", - "integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "nostics": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@vue/devtools-api": "^8.1.5", - "typescript": ">=5.6.0", - "vue": "^3.5.11" - }, - "peerDependenciesMeta": { - "@vue/devtools-api": { - "optional": false - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/sharing/node_modules/qrcode.vue": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.11.0.tgz", - "integrity": "sha512-PnaO9yqcEVsH9euUmo4Nn0JZMkNwk4u6fDUosKjDTXv2Cgv7rHZhr2D8Iq54clBvSycOESwgUE/rw/tBRBjIVg==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/rehype-react": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz", - "integrity": "sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@nextcloud/sharing/node_modules/splitpanes": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.1.2.tgz", - "integrity": "sha512-frNchoCv7w0nMQEuaDGgMGMU6jv3NhN6bBGQVfCNgwJdVcYaRmi1dwYDjp7SifAoUFdiQjBRB254LJNidzo15Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antoniandre" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/@nextcloud/sharing/node_modules/vue": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", - "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.42", - "@vue/compiler-sfc": "3.5.42", - "@vue/runtime-dom": "3.5.42", - "@vue/server-renderer": "3.5.42", - "@vue/shared": "3.5.42" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@nextcloud/sharing/node_modules/vue-resize": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", - "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/vue-router": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz", - "integrity": "sha512-GDBZzgmILxA/kFnkFbJjQZdZ2QQbngnIMMuoUcjhZIfH1RGMaPjPwX5ASnV38qamuA9uhO0RDjSBHTDNG2uXyQ==", - "license": "MIT", - "dependencies": { - "@vue-macros/common": "^3.1.3", - "@vue/devtools-api": "^8.1.5", - "ast-walker-scope": "^0.9.0", - "chokidar": "^5.0.0", - "confbox": "^0.2.4", - "local-pkg": "^1.2.1", - "magic-string": "^0.30.21", - "mlly": "^1.8.2", - "muggle-string": "^0.4.1", - "nostics": "^1.1.4", - "pathe": "^2.0.3", - "picomatch": "^4.0.5", - "scule": "^1.3.0", - "tinyglobby": "^0.2.17", - "unplugin": "^3.3.0", - "unplugin-utils": "^0.3.2" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", - "pinia": "^3.0.4 || ^4.0.2", - "vite": "^7.3.0 || ^8.0.0", - "vue": "^3.5.34 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@pinia/colada": { - "optional": true - }, - "@vue/compiler-sfc": { - "optional": true - }, - "pinia": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@nextcloud/typings": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", - "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@types/jquery": "3.5.16" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/upload": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@nextcloud/upload/-/upload-1.12.0.tgz", - "integrity": "sha512-KyN3m9IvqLmnXZLRJ8f7NdDuncGRT1KtH7P3bJYDsDPgjG8dd5TCc2vDsw6YfWS95q/QsKKI4ZrjPBZkkRlRAg==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/dialogs": "^6.4.2", - "@nextcloud/files": "^3.12.2", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.1.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "axios": "^1.20.0", - "axios-retry": "^4.5.0", - "crypto-browserify": "^3.12.1", - "p-cancelable": "^4.0.1", - "p-queue": "^9.3.3", - "typescript-event-target": "^1.1.2" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "@nextcloud/vue": "^8.0.0", - "vue": "^2.7.16" - } - }, - "node_modules/@nextcloud/upload/node_modules/@nextcloud/dialogs": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.4.2.tgz", - "integrity": "sha512-xj6fyUdb56StWt1DWeDHYnqK0Ck7EWQLUEyWtXHnneknoOV3x/Y0HPRL5L2PTJaDuVQB8TcVW53JjR38JG9W6Q==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@mdi/js": "^7.4.47", - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/files": "^3.12.2", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "@types/toastify-js": "^1.12.4", - "@vueuse/core": "^11.3.0", - "cancelable-promise": "^4.3.1", - "p-queue": "^9.0.1", - "toastify-js": "^1.12.0", - "vue-frag": "^1.4.3", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "@nextcloud/vue": "^8.24.0", - "vue": "^2.7.16" - } - }, - "node_modules/@nextcloud/upload/node_modules/@nextcloud/dialogs/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, - "node_modules/@nextcloud/upload/node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/upload/node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, - "node_modules/@nextcloud/upload/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/upload/node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/@nextcloud/vue": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.41.0.tgz", - "integrity": "sha512-537YLnGYkXwTzbjDo2OwRW16sP3et9jcrp0KZeGGmTBJMuI9wRR+WfTTuiHva/UnQ+kNMdfU0aq7lvGrme8OEw==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@floating-ui/dom": "^1.8.0", - "@linusborg/vue-simple-portal": "^0.1.5", - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.6.0", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue-select": "^3.26.0", - "@vueuse/components": "^11.0.0", - "@vueuse/core": "^11.0.0", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^2.2.0", - "dompurify": "^3.4.13", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^1.0.0-beta.19", - "focus-trap": "^7.8.0", - "linkify-string": "^4.3.3", - "md5": "^2.3.0", - "mdast-util-to-string": "^4.0.0", - "p-queue": "^8.1.1", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^7.1.2", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^2.4.1", - "string-length": "^5.0.1", - "striptags": "^3.2.0", - "tabbable": "^6.5.0", - "tributejs": "^5.1.3", - "unified": "^11.0.1", - "unist-builder": "^4.0.0", - "unist-util-visit-parents": "^6.0.2", - "vue": "^2.7.16", - "vue-color": "^2.8.1", - "vue-frag": "^1.4.3", - "vue-router": "^3.6.5", - "vue2-datepicker": "^3.11.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue-select": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-3.26.0.tgz", - "integrity": "sha512-UvJExrxzx5pP3lv7j6zrv2yj6B1dXph7sh3lLNPnbJPjPoH/yg58mHNFBcPJrRYMbpy2t3hlC6F7s33KCTr9FA==", - "license": "MIT", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "vue": "2.x" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/initial-state": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz", - "integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/debounce": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", - "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nextcloud/vue/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" - }, - "node_modules/@nextcloud/vue/node_modules/focus-trap": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", - "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", - "license": "MIT", - "dependencies": { - "tabbable": "^6.4.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/p-queue": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", - "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nextcloud/vue/node_modules/p-timeout": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", - "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nextcloud/webpack-vue-config": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@nextcloud/webpack-vue-config/-/webpack-vue-config-7.0.4.tgz", - "integrity": "sha512-JM4gHZZCLGVtxiILjb0RvQQRjBFbau0hQHQyNrLR+wkY3UaLMXkj9P8dT+w8/d6WIOnwLbQFY2Duox6crbgh5Q==", - "dev": true, - "hasInstallScript": true, - "license": "AGPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.22.9", - "babel-loader": "^10.0.0", - "css-loader": "^7.1.1", - "minimizer-webpack-plugin": "^5.6.1", - "node-polyfill-webpack-plugin": "4.0.0", - "sass": "^1.64.2", - "sass-loader": "^17.0.0", - "style-loader": "^4.0.0", - "ts-loader": "^9.4.4", - "vue": "^2.7.16 || ^3.5.13", - "vue-loader": "^15.11.1 || ^17.4.2", - "webpack": "^5.88.2", - "webpack-cli": "^6.0.1", - "webpack-dev-server": "^6.0.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodable/entities": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", - "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/deferred-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", - "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", - "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "@peculiar/asn1-x509-attr": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", - "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", - "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", - "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-cms": "^2.9.4", - "@peculiar/asn1-pkcs8": "^2.9.4", - "@peculiar/asn1-rsa": "^2.9.4", - "@peculiar/asn1-schema": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", - "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", - "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-cms": "^2.9.4", - "@peculiar/asn1-pfx": "^2.9.4", - "@peculiar/asn1-pkcs8": "^2.9.4", - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "@peculiar/asn1-x509-attr": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", - "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", - "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", - "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", - "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.9.4", - "@peculiar/asn1-x509": "^2.9.4", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@peculiar/utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@pinia/testing": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.7.tgz", - "integrity": "sha512-xcDq6Ry/kNhZ5bsUMl7DeoFXwdume1NYzDggCiDUDKoPQ6Mo0eH9VU7bJvBtlurqe6byAntWoX5IhVFqWzRz/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "vue-demi": "^0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "pinia": ">=2.2.6" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/plugin-babel": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", - "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", - "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", - "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "serialize-javascript": "^7.0.3", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@simplewebauthn/browser": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", - "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", - "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "vitest": { - "optional": true - } - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.7", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", - "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@testing-library/vue": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-5.9.0.tgz", - "integrity": "sha512-HWvI4s6FayFLmiqGcEMAMfTSO1SV12NukdoyllYMBobFqfO0TalQmfofMtiO+eRz+Amej8Z26dx4/WYIROzfVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0", - "@testing-library/dom": "^9.0.0", - "@vue/test-utils": "^1.3.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "vue": "^2.6.10", - "vue-template-compiler": "^2.6.10" - } - }, - "node_modules/@testing-library/vue/node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/vue/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { - "version": "3.0.0-pre1", - "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", - "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ejs": "^3.1.10", - "json5": "^2.2.3", - "magic-string": "^0.30.21", - "string.prototype.matchall": "^4.0.12" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/escape-html": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", - "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", - "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/jquery": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", - "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, - "node_modules/@types/jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", - "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.13.0" - } - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", - "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/@types/set-cookie-parser": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", - "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/sizzle": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", - "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", - "license": "MIT" - }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/toastify-js": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", - "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-vue2": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue2/-/plugin-vue2-2.3.4.tgz", - "integrity": "sha512-LgqtRRedJb1KdmgcllwGX0gtlPvOvtR6pITXmqxGwQhBZaAysg0Hd7wvj3sjCsj4+PENWsqS7O+ceYSOgJ+H9g==", - "dev": true, - "engines": { - "node": "^14.18.0 || >= 16.0.0" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "vue": "^2.7.0-0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", - "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.11", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.1.11", - "vitest": "4.1.11" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.11", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.11", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vue-macros/common": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.3.tgz", - "integrity": "sha512-pphnexn8CDKugcA4TYSKlg1XanBYPbILST+eZK9ZGqG8sVbNR5L0kXEpRqs8+iSznosHt/Jo2k1FGl0tnWIpyg==", - "license": "MIT", - "dependencies": { - "@vue/compiler-sfc": "^3.5.22", - "ast-kit": "^2.1.2", - "local-pkg": "^1.1.2", - "magic-string-ast": "^1.0.2", - "unplugin-utils": "^0.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/vue-macros" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.2.25" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", - "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.42", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@vue/compiler-core/node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", - "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.42", - "@vue/shared": "3.5.42" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", - "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/compiler-core": "3.5.42", - "@vue/compiler-dom": "3.5.42", - "@vue/compiler-ssr": "3.5.42", - "@vue/shared": "3.5.42", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.19", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", - "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.42", - "@vue/shared": "3.5.42" - } - }, - "node_modules/@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vue/component-compiler-utils/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, - "node_modules/@vue/devtools-kit": { - "version": "7.7.10", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", - "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vue/devtools-shared": "^7.7.10", - "birpc": "^2.3.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "7.7.10", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", - "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "rfdc": "^1.4.1" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", - "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.41" - } - }, - "node_modules/@vue/reactivity/node_modules/@vue/shared": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", - "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", - "license": "MIT" - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", - "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@vue/runtime-core/node_modules/@vue/shared": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", - "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", - "license": "MIT" - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", - "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.41", - "@vue/runtime-core": "3.5.41", - "@vue/shared": "3.5.41", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", - "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", - "license": "MIT" - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", - "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.41", - "@vue/runtime-dom": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@vue/server-renderer/node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@vue/server-renderer/node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@vue/server-renderer/node_modules/@vue/compiler-core": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", - "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.41", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/server-renderer/node_modules/@vue/compiler-dom": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", - "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", - "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.41", - "@vue/shared": "3.5.41" - } - }, - "node_modules/@vue/server-renderer/node_modules/@vue/shared": { - "version": "3.5.41", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", - "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", - "license": "MIT" - }, - "node_modules/@vue/shared": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", - "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", - "license": "MIT" - }, - "node_modules/@vue/test-utils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz", - "integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dom-event-types": "^1.0.0", - "lodash": "^4.17.15", - "pretty": "^2.0.0" - }, - "peerDependencies": { - "vue": "2.x", - "vue-template-compiler": "^2.x" - } - }, - "node_modules/@vue/tsconfig": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.6.0.tgz", - "integrity": "sha512-MHXNd6lzugsEHvuA6l1GqrF5jROqUon8sP/HInLPnthJiYvB0VvpHMywg7em1dBZfFZNBSkR68qH37zOdRHmCw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": "5.x", - "vue": "^3.3.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/web-component-wrapper": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", - "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", - "license": "MIT" - }, - "node_modules/@vueuse/components": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.3.0.tgz", - "integrity": "sha512-sqaGtWPgobXvZmv3atcjW8YW0ypecFuB286OEKFXaPrLsA5b2Y+xAvHvq5V7d+VJRKt705gCK3BNBjxu3g1PdQ==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - } - }, - "node_modules/@vueuse/core": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", - "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/integrations": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.3.0.tgz", - "integrity": "sha512-5fzRl0apQWrDezmobchoiGTkGw238VWESxZHazfhP3RM7pDSiyXy18QbfYkILoYNTd23HPAfQTJpkUc5QbkwTw==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "async-validator": "^4", - "axios": "^1", - "change-case": "^5", - "drauu": "^0.4", - "focus-trap": "^7", - "fuse.js": "^7", - "idb-keyval": "^6", - "jwt-decode": "^4", - "nprogress": "^0.2", - "qrcode": "^1.5", - "sortablejs": "^1", - "universal-cookie": "^7" - }, - "peerDependenciesMeta": { - "async-validator": { - "optional": true - }, - "axios": { - "optional": true - }, - "change-case": { - "optional": true - }, - "drauu": { - "optional": true - }, - "focus-trap": { - "optional": true - }, - "fuse.js": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "jwt-decode": { - "optional": true - }, - "nprogress": { - "optional": true - }, - "qrcode": { - "optional": true - }, - "sortablejs": { - "optional": true - }, - "universal-cookie": { - "optional": true - } - } - }, - "node_modules/@vueuse/metadata": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", - "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz", - "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==", - "license": "MIT", - "dependencies": { - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", - "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", - "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", - "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "devOptional": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "peer": true, - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/argue-cli": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-2.1.0.tgz", - "integrity": "sha512-dgojXfc4SiqmNwe38PnbT3zJasrz7g62dLAPD+VFT5RJb8W7LGRqw2IFd2ES+plnhsp4HYNJmFqMU1tCThdCww==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz", - "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "is-string": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-kit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", - "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "pathe": "^2.0.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/ast-walker-scope": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", - "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.4", - "ast-kit": "^2.1.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, - "node_modules/babel-loader": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", - "integrity": "sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5.61.0" - } - }, - "node_modules/babel-loader-exclude-node-modules-except": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/babel-loader-exclude-node-modules-except/-/babel-loader-exclude-node-modules-except-1.2.4.tgz", - "integrity": "sha512-rfoi0R9K9tORe4xfFtiqPoBzlMmX/wMxygjSSw5FYHxzoyxmVzxCzASXMTmwfh/3M4VD442TOu67xPNY7m7jEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-plugin-module-resolver": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.3.tgz", - "integrity": "sha512-h8h6H71ZvdLJZxZrYkaeR30BojTaV7O9GfqacY14SNj5CNB8ocL9tydNzTC0JrnNN7vY3eJhwCmkDj7tuEUaqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-babel-config": "^2.1.1", - "glob": "^9.3.3", - "pkg-up": "^3.1.0", - "reselect": "^4.1.7", - "resolve": "^1.22.8" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base-64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/birpc": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/bluebird": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", - "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/blurhash": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bonjour-service": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", - "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "license": "MIT" - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "license": "MIT", - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz", - "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.3", - "browserify-rsa": "^4.1.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.6.1", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.9", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/browserslist-useragent-regexp": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/browserslist-useragent-regexp/-/browserslist-useragent-regexp-4.1.4.tgz", - "integrity": "sha512-DgA+gTG1GCU0HGk/KUgYVpGhKZSt2vpirIGeKHla/zd9ALtM+nA8WAUGXhwp63D8gnA7g5fP7FGjNZOQO3fk7w==", - "funding": [ - "https://ko-fi.com/dangreen" - ], - "license": "MIT", - "dependencies": { - "argue-cli": "^2.1.0", - "easy-table": "^1.2.0", - "picocolors": "^1.0.0", - "regexp-tree": "^0.1.24", - "ua-regexes-lite": "^1.2.1" - }, - "bin": { - "bluare": "dist/cli.js", - "browserslist-useragent-regexp": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "browserslist": ">=4.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "license": "MIT" - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", - "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", - "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cancelable-promise": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cancelable-promise/-/cancelable-promise-4.3.1.tgz", - "integrity": "sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==", - "license": "MIT" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", - "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cipher-base": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", - "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/clamp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", - "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", - "license": "MIT" - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "license": "MIT", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/colord": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", - "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/condense-newlines": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", - "integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-whitespace": "^0.3.0", - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/condense-newlines/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "license": "MIT" - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true, - "peer": true - }, - "node_modules/consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", - "dev": true, - "license": "MIT", - "dependencies": { - "bluebird": "^3.1.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/copy-anything": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.1.5.tgz", - "integrity": "sha512-vePst2VLTSqPglW7Q98VwFpGiGpOHTyXIo81MO5VyLecA5zMbZw/P/V1vmwD1jksa2iJpn/YkDrJ3XhTfzUyhQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/core-js": { - "version": "3.50.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", - "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", - "hasInstallScript": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/cropperjs": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", - "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "license": "MIT", - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-format-parse": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/date-format-parse/-/date-format-parse-0.2.7.tgz", - "integrity": "sha512-/+lyMUKoRogMuTeOVii6lUwjbVlesN9YRYLzZT/g3TEZ3uD9QnpjResujeEqUW+OSNbT7T1+SYdyEkTcRv+KDQ==", - "license": "MIT" - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/debounce": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-3.0.0.tgz", - "integrity": "sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decode-uri-component": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.5.0.tgz", - "integrity": "sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", - "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-event-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dom-event-types/-/dom-event-types-1.1.0.tgz", - "integrity": "sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/domain-browser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-5.7.0.tgz", - "integrity": "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ==", - "dev": true, - "license": "Artistic-2.0", - "peer": true, - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/easy-table": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", - "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "optionalDependencies": { - "wcwidth": "^1.0.1" - } - }, - "node_modules/editorconfig": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "^9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/editorconfig/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "license": "ISC" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/emoji-mart-vue-fast": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-15.0.5.tgz", - "integrity": "sha512-wnxLor8ggpqshoOPwIc33MdOC3A1XFeDLgUwYLPtNPL8VeAtXJAVrnFq1CN5PeCYAFoLo4IufHQZ9CfHD4IZiw==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.18.6", - "core-js": "^3.23.5" - }, - "peerDependencies": { - "vue": ">2.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.25.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", - "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/envinfo": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.15.0.tgz", - "integrity": "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "devOptional": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eta": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", - "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/bgub/eta?sponsor=1" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "license": "MIT", - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/exports-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-5.0.0.tgz", - "integrity": "sha512-W15EyyytBwd30yCCieTCqZSCUvU/o3etj2IUItSMjVQEzAf5xOQx8JL9iMo7ERnuAzIA6eapGSFWl7E9F+Wy9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", - "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/fast-xml-builder": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", - "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.6.2", - "xml-naming": "^0.3.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", - "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^3.0.0", - "fast-xml-builder": "^1.2.0", - "is-unsafe": "^2.0.0", - "path-expression-matcher": "^1.6.2", - "strnum": "^2.4.2", - "xml-naming": "^0.3.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastparse": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-babel-config": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", - "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.3" - } - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "micromatch": "^4.0.2" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/floating-vue": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-1.0.0-beta.19.tgz", - "integrity": "sha512-OcM7z5Ua4XAykqolmvPj3l1s+KqUKj6Xz2t66eqjgaWfNBjtuifmxO5+4rRXakIch/Crt8IH+vKdKcR3jOUaoQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^0.1.10", - "vue-resize": "^1.0.0" - }, - "peerDependencies": { - "vue": "^2.6.10" - } - }, - "node_modules/floating-vue/node_modules/@floating-ui/core": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.3.1.tgz", - "integrity": "sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==", - "license": "MIT" - }, - "node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.1.10.tgz", - "integrity": "sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^0.3.0" - } - }, - "node_modules/focus-trap": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz", - "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==", - "license": "MIT", - "dependencies": { - "tabbable": "^6.5.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true, - "license": "ISC" - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", - "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/graphql": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", - "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars-loader": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/handlebars-loader/-/handlebars-loader-1.7.3.tgz", - "integrity": "sha512-dDb+8D51vE3OTSE2wuGPWRAegtsEuw8Mk8hCjtRu/pNcBfN5q+M8ZG3kVJxBuOeBrVElpFStipGmaxSBTRR1mQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^3.2.2", - "fastparse": "^1.0.0", - "loader-utils": "1.4.x", - "object-assign": "^4.1.0" - }, - "peerDependencies": { - "handlebars": ">= 1.3.0 < 5" - } - }, - "node_modules/handlebars-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/handlebars-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-to-hyperscript": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.3.tgz", - "integrity": "sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.1", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-to-hyperscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime/node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/headers-polyfill": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", - "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/set-cookie-parser": "^2.4.10", - "set-cookie-parser": "^3.0.1" - } - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", - "license": "MIT" - }, - "node_modules/hot-patcher": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", - "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy-middleware": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz", - "integrity": "sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.3", - "httpxy": "^0.5.4", - "is-glob": "^4.0.3", - "is-plain-obj": "^4.1.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^22.15.0 || ^24.0.0 || >=26.0.0" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/httpxy": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.5.5.tgz", - "integrity": "sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.18" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ipaddr.js": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", - "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-absolute-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", - "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-network-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", - "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-svg": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-6.1.0.tgz", - "integrity": "sha512-i7YPdvYuSCYcaLQrKwt8cvKTlwHcdA6Hp8N9SO3Q5jIzo8x6kH3N47W0BvPP7NdxVBmIHx7X9DK36czYYW7lHg==", - "license": "MIT", - "dependencies": { - "@file-type/xml": "^0.4.3" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", - "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-whitespace": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", - "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-beautify": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", - "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.4.2", - "js-cookie": "^3.0.5", - "nopt": "^7.2.1" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/js-cookie": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", - "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", - "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "isarray": "^2.0.5", - "jsonify": "^0.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/json-stable-stringify/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "dev": true, - "license": "Public Domain", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" - } - }, - "node_modules/launch-editor": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", - "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.4" - } - }, - "node_modules/layerr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", - "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.13.14", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.14.tgz", - "integrity": "sha512-llihgCcx0BFLksecLP+x1J+6JDE1GsXS1RN/LoPF6qcwpeQcnjj0lcvZxY8AzbEpYwyZWPZW/nDuqkqzm3amiw==", - "license": "MIT" - }, - "node_modules/linkify-string": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.3.3.tgz", - "integrity": "sha512-k8BwpKz6t4jigzoi5hKUbji0M4kUqhQ7LXIKFRxb2VvG5m0+4YSck+VFLhOxxeiuVu3pLDuSVNRq9+IQKroFIw==", - "license": "MIT", - "peerDependencies": { - "linkifyjs": "==4.3.3" - } - }, - "node_modules/linkifyjs": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", - "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", - "license": "MIT" - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/local-pkg": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", - "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lowlight": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", - "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.0.0", - "highlight.js": "~11.11.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magic-string-ast": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", - "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.19" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/marked": { - "version": "18.0.14", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.14.tgz", - "integrity": "sha512-mBHK6FBHuBAlhgRe88w9F0O1AbwwXJUcQibUbC/QcdTbVGAD7aWza+xt3N6oT/jCZx3/OMeS+8rnuiHZcQ9s7A==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/mdast-squeeze-paragraphs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-6.0.0.tgz", - "integrity": "sha512-6NDbJPTg0M0Ye+TlYwX1KJ1LFbp515P2immRJyJQhc9Na9cetHzSoHNYIQcXpANEAP1sm9yd/CTZU2uHqR5A+w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-newline-to-break": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", - "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-find-and-replace": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/memfs": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.1.tgz", - "integrity": "sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.68.1", - "@jsonjoy.com/fs-fsa": "4.68.1", - "@jsonjoy.com/fs-node": "4.68.1", - "@jsonjoy.com/fs-node-builtins": "4.68.1", - "@jsonjoy.com/fs-node-to-fsa": "4.68.1", - "@jsonjoy.com/fs-node-utils": "4.68.1", - "@jsonjoy.com/fs-print": "4.68.1", - "@jsonjoy.com/fs-snapshot": "4.68.1", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/mime": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", - "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimizer-webpack-plugin": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.10.0.tgz", - "integrity": "sha512-2//60T4S0X1Ug/dqLDOgDaGlZsN1Lv8hf8oB0NOV/KIHSaXlPwXBBIbim79dE2Z7NEs52ES8YHoSv6Lmsk+XNg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.3", - "terser": "^5.51.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@napi-rs/image": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "imagemin": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "sharp": { - "optional": true - }, - "svgo": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/minimizer-webpack-plugin/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/minimizer-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/minimizer-webpack-plugin/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/minimizer-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/minimizer-webpack-plugin/node_modules/terser": { - "version": "5.51.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", - "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/moment": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", - "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.4.tgz", - "integrity": "sha512-JHUK8Mgheach4uMQmIV/pIfrv0ubPiDCPPqc7Mi604je1UblP6Gbm+FfV+Oh88KP03rZqGCZG3t0wmpfdlDv+w==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msw": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz", - "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@inquirer/confirm": "^6.0.11", - "@mswjs/interceptors": "^0.41.3", - "@open-draft/deferred-promise": "^3.0.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.1.1", - "graphql": "^16.13.2", - "headers-polyfill": "^5.0.1", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.11.11", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.1", - "type-fest": "^5.5.0", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/nested-property": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", - "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-fetch/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/node-polyfill-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "assert": "^2.1.0", - "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^5.7.0", - "events": "^3.3.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "punycode": "^2.3.1", - "querystring-es3": "^0.2.1", - "readable-stream": "^4.5.2", - "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "type-fest": "^4.18.2", - "url": "^0.11.3", - "util": "^0.12.5", - "vm-browserify": "^1.1.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "webpack": ">=5" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" - }, - "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/nostics": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.1.4.tgz", - "integrity": "sha512-U4FApICSLCQ0dYiN59pxUCEZC053palQXi57yLaNdMaL6TBY4C4q3qZrJKUGcor2dGv8EhEwt8y5KvU2bo2kFg==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/open": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.1.tgz", - "integrity": "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "default-browser": "^5.4.0", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.2.0", - "wsl-utils": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT" - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/p-limit": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", - "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.2.1" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", - "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/p-retry": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", - "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-network-error": "^1.3.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)", - "peer": true - }, - "node_modules/parse-asn1": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", - "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "pbkdf2": "^3.1.5", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/patch-package": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", - "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^10.0.0", - "json-stable-stringify": "^1.0.2", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "semver": "^7.5.3", - "slash": "^2.0.0", - "tmp": "^0.2.4", - "yaml": "^2.2.2" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "node": ">=14", - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/patch-package/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/patch-package/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "license": "MIT", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", - "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-posix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "license": "ISC" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/path/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" - }, - "node_modules/path/node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/pbkdf2": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", - "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", - "license": "MIT", - "dependencies": { - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "ripemd160": "^2.0.3", - "safe-buffer": "^5.2.1", - "sha.js": "^2.4.12", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinia": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", - "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.3", - "vue-demi": "^0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "typescript": ">=4.4.4", - "vue": "^2.7.0 || ^3.5.11" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkijs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", - "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "@noble/hashes": "1.4.0", - "asn1js": "^3.0.6", - "bytestreamjs": "^2.0.1", - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/powershell-utils": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz", - "integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "license": "MIT", - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", - "integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "condense-newlines": "^0.2.1", - "extend-shallow": "^2.0.1", - "js-beautify": "^1.6.12" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, - "license": "ISC" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", - "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/query-string": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.5.1.tgz", - "integrity": "sha512-/zO3RwuRCMTIcEgq6YMv4OrtEE1XzBG7w5N6zc6ydYnkWYOWsnLI/5894hYEzfESfMOT4cHCsRTIdxsSl1KjGg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.5.0", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "license": "MIT", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-loader": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", - "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/readable-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regextras": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", - "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.14" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/rehype-external-links": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", - "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-is-element": "^3.0.0", - "is-absolute-url": "^4.0.0", - "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-highlight": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", - "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-text": "^4.0.0", - "lowlight": "^3.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-7.2.0.tgz", - "integrity": "sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==", - "license": "MIT", - "dependencies": { - "@mapbox/hast-util-table-cell-style": "^0.2.0", - "@types/hast": "^2.0.0", - "hast-to-hyperscript": "^10.0.0", - "hast-util-whitespace": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=17" - } - }, - "node_modules/rehype-react/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/rehype-react/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/rehype-react/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rehype-react/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-breaks": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", - "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-newline-to-break": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-unlink-protocols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/remark-unlink-protocols/-/remark-unlink-protocols-1.0.0.tgz", - "integrity": "sha512-5j/F28jhFmxeyz8nuJYYIWdR4nNpKWZ8A+tVwnK/0pq7Rjue33CINEYSckSq2PZvedhKUwbn08qyiuGoPLBung==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-squeeze-paragraphs": "^6.0.0", - "unist-util-visit": "^5.0.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rettime": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", - "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/ripemd160": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", - "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", - "license": "MIT", - "dependencies": { - "hash-base": "^3.1.2", - "inherits": "^2.0.4" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ripemd160/node_modules/hash-base": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", - "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/sass": { - "version": "1.105.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.105.0.tgz", - "integrity": "sha512-4EjSuRTyG8M4yRXXY1PT9OUV5zz23xBxJkbIAprohKHqf6HOU4MCpE3U3vAC+iO1gxobBjESNbcj8rYAQZWnyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=20.19.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass-loader": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-17.0.0.tgz", - "integrity": "sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 22.11.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/scule": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", - "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", - "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@peculiar/x509": "^1.14.2", - "pkijs": "^3.3.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.8.0", - "mime-types": "~2.1.35", - "parseurl": "~1.3.3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-index/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/serve-index/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/smob": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", - "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/sortablejs": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", - "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==", - "license": "MIT" - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true, - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/splitpanes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.4.1.tgz", - "integrity": "sha512-kpEo1WuMXuc6QfdQdO2V/fl/trONlkUKp+pputsLTiW9RMtwEvjb4/aYGm2m3+KAzjmb+zLwr4A4SYZu74+pgQ==", - "license": "MIT" - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "license": "MIT", - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/striptags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", - "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==", - "license": "MIT" - }, - "node_modules/strnum": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", - "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "anynum": "^1.0.1" - } - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/style-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", - "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.27.0" - } - }, - "node_modules/style-to-js": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", - "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.11" - } - }, - "node_modules/style-to-js/node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", - "license": "MIT" - }, - "node_modules/style-to-js/node_modules/style-to-object": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", - "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/superjson": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", - "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "copy-anything": "^4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tabbable": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", - "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", - "license": "MIT" - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", - "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/thingies": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", - "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", - "license": "MIT", - "engines": { - "node": ">=12.22" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.30" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toastify-js": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", - "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==", - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/tr46/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/tributejs": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/tributejs/-/tributejs-5.1.3.tgz", - "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==", - "license": "MIT" - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-loader": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ts-md5": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-2.0.1.tgz", - "integrity": "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==", - "engines": { - "node": ">=18" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "peer": true - }, - "node_modules/tsyringe": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", - "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "tslib": "^1.9.3" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/tsyringe/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD", - "peer": true - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/type-fest": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", - "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-event-target": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.2.tgz", - "integrity": "sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==", - "license": "MIT" - }, - "node_modules/ua-regexes-lite": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ua-regexes-lite/-/ua-regexes-lite-1.2.1.tgz", - "integrity": "sha512-ling4WX4ZtxXjmSMHzuI8PGos2brw/6gG3YuVWn5RunHoQjeCokpFeMe/ti+R8E7kOTLE2FqBG4bMdFQLFwcJQ==", - "funding": [ - "https://opencollective.com/browserslist-useragent-regexp", - "https://ko-fi.com/dangreen" - ], - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "license": "MIT" - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", - "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/unist-builder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", - "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unplugin": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", - "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.4", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@farmfe/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "bun-types-no-globals": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "unloader": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/unplugin-utils": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", - "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/unplugin-utils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unplugin/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/v-click-outside": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.2.0.tgz", - "integrity": "sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/v-tooltip": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz", - "integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21", - "popper.js": "^1.16.1", - "vue-resize": "^1.0.1" - } - }, - "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/vue": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", - "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", - "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", - "license": "MIT", - "dependencies": { - "@vue/compiler-sfc": "2.7.16", - "csstype": "^3.1.0" - } - }, - "node_modules/vue-click-outside": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.1.0.tgz", - "integrity": "sha512-pNyvAA9mRXJwPHlHJyjMb4IONSc7khS5lxGcMyE2EIKgNMAO279PWM9Hyq0d5J4FkiSRdmFLwnbjDd5UtPizHQ==", - "license": "MIT" - }, - "node_modules/vue-color": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.2.tgz", - "integrity": "sha512-1qmsxl5GiIjx/jApBbTGr2r4bN/7WRKUTl3tc53vkXb9Ua0rZmiqsdq6VdG1e7dVNTLJahdsRGWcjeU2+98+NA==", - "license": "MIT", - "dependencies": { - "clamp": "^1.0.1", - "lodash.throttle": "^4.0.0", - "material-colors": "^1.0.0", - "tinycolor2": "^1.1.2" - } - }, - "node_modules/vue-cropperjs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/vue-cropperjs/-/vue-cropperjs-4.2.0.tgz", - "integrity": "sha512-dvwCBtjGMiznkNIK2GFd1SQm1x+wmtWg4g4t+NrJSPj/fpHnubXxAUOIvY7lMFeR2lawRLsigCaGZrcXCzuTKA==", - "license": "MIT", - "dependencies": { - "cropperjs": "^1.5.6" - } - }, - "node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/vue-frag": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/vue-frag/-/vue-frag-1.4.3.tgz", - "integrity": "sha512-pQZj03f/j9LRhzz9vKaXTCXUHVYHuAXicshFv76VFqwz4MG3bcb+sPZMAbd0wmw7THjkrTPuoM0EG9TbG8CgMQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/vue-frag?sponsor=1" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, - "node_modules/vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue-infinite-loading": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/vue-infinite-loading/-/vue-infinite-loading-2.4.5.tgz", - "integrity": "sha512-xhq95Mxun060bRnsOoLE2Be6BR7jYwuC89kDe18+GmCLVrRA/dU0jrGb12Xu6NjmKs+iTW0AA6saSEmEW4cR7g==", - "license": "MIT", - "peerDependencies": { - "vue": "^2.6.10" - } - }, - "node_modules/vue-loader": { - "version": "15.11.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", - "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "prettier": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/vue-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/vue-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/vue-localstorage": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/vue-localstorage/-/vue-localstorage-0.6.2.tgz", - "integrity": "sha512-29YQVVkIdoS6BZBCJAyu9d0OR0eKSm5gk5OjsLssV1+NM4zJnf9cxhN1AVeXkUHJLqOonECweuaR8PZ2x307dw==", - "license": "MIT" - }, - "node_modules/vue-material-design-icons": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.3.1.tgz", - "integrity": "sha512-6UNEyhlTzlCeT8ZeX5WbpUGFTTPSbOoTQeoASTv7X4Ylh0pe8vltj+36VMK56KM0gG8EQVoMK/Qw/6evalg8lA==", - "license": "MIT" - }, - "node_modules/vue-resize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz", - "integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, - "node_modules/vue-router": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", - "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==", - "license": "MIT" - }, - "node_modules/vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "node_modules/vue-style-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/vue-style-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue/node_modules/@vue/compiler-sfc": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", - "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", - "dependencies": { - "@babel/parser": "^7.23.5", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/vue2-datepicker": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-3.11.1.tgz", - "integrity": "sha512-6PU/+pnp2mgZAfnSXmbdwj9516XsEvTiw61Q5SNrvvdy8W/FCxk1GAe9UZn/m9YfS5A47yK6XkcjMHbp7aFApA==", - "license": "MIT", - "dependencies": { - "date-format-parse": "^0.2.7" - }, - "peerDependencies": { - "vue": "^2.5.0" - } - }, - "node_modules/vue2-teleport": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vue2-teleport/-/vue2-teleport-1.2.2.tgz", - "integrity": "sha512-HiGkijzS+qcM6OAovlPfdnbVbi+cA2DX8qfFIm0QF4Mmh7nuQUZomGwkQ4Qei5Wy3SwtLeHvVc9CSfk+pzleog==", - "license": "CC0-1.0" - }, - "node_modules/vuedraggable": { - "version": "2.24.3", - "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz", - "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==", - "license": "MIT", - "dependencies": { - "sortablejs": "1.10.2" - } - }, - "node_modules/vuex": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", - "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==", - "license": "MIT", - "peerDependencies": { - "vue": "^2.0.0" - } - }, - "node_modules/vuex-router-sync": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz", - "integrity": "sha512-Mry2sO4kiAG64714X1CFpTA/shUH1DmkZ26DFDtwoM/yyx6OtMrc+MxrU+7vvbNLO9LSpgwkiJ8W+rlmRtsM+w==", - "license": "MIT", - "peerDependencies": { - "vue-router": "^3.0.0", - "vuex": "^3.0.0" - } - }, - "node_modules/watchpack": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", - "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webdav": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.11.0.tgz", - "integrity": "sha512-DbUS8rHvWse55EiHM0q5q1z3eVRCLWW7Va7mUqA2/JJdQHIew4Op2HTglHkBJDOAVrQeQf0I+REUJkIrYqtL+w==", - "license": "MIT", - "dependencies": { - "@buttercup/fetch": "^0.2.1", - "@nodable/entities": "^3.0.0", - "base-64": "^1.0.0", - "byte-length": "^1.0.2", - "entities": "^6.0.1", - "fast-xml-parser": "^5.11.1", - "hot-patcher": "^2.0.1", - "layerr": "^3.0.0", - "md5": "^2.3.0", - "minimatch": "^9.0.9", - "nested-property": "^4.0.0", - "node-fetch": "^3.3.2", - "path-posix": "^1.0.0", - "url-join": "^5.0.0", - "url-parse": "^1.5.10" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/webdav/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/webdav/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/webpack": { - "version": "5.111.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.111.1.tgz", - "integrity": "sha512-cNypaz0RP+S4cvQVi1M/3bRUkvNP0xZpL0TjFx+c6jg64VbxD+2weWDgY9UnjRI6dhZgtaj8DU76GGFcJ79xPQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.25.0", - "es-module-lexer": "^2.1.0", - "events": "^3.2.0", - "graceful-fs": "^4.2.11", - "mime-db": "^1.54.0", - "minimizer-webpack-plugin": "^5.7.0", - "schema-utils": "^4.5.0", - "tapable": "^2.3.0", - "watchpack": "^2.5.2", - "webpack-sources": "^3.5.1" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", - "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.6.1", - "@webpack-cli/configtest": "^3.0.1", - "@webpack-cli/info": "^3.0.1", - "@webpack-cli/serve": "^3.0.1", - "colorette": "^2.0.14", - "commander": "^12.1.0", - "cross-spawn": "^7.0.3", - "envinfo": "^7.14.0", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^6.0.1" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.82.0" - }, - "peerDependenciesMeta": { - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.1.1.tgz", - "integrity": "sha512-JrxAE/HwNmEnTkzkUGHFItHpGalzuEIUDifXhjAkIEHZzHK/ZJFVoTQF5ubZQlgeRireqLdr2lZttrrmOH61IA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "memfs": "^4.56.10", - "mime-types": "^3.0.2", - "range-parser": "^1.2.1", - "schema-utils": "^4.3.3" - }, - "engines": { - "node": ">= 20.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.101.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-6.0.0.tgz", - "integrity": "sha512-q9SD4ItOGhZLeU6EGT10caDZdHjF50Pz1DtkRZZOPsfluMXOkacWKKOtSBSLVkPqKiF67eFUC0rI88U/tSFPEw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^5.0.6", - "@types/express-serve-static-core": "^5.1.1", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^2.2.0", - "@types/ws": "^8.18.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.3.0", - "chokidar": "^5.0.0", - "compression": "^1.8.1", - "connect-history-api-fallback": "^2.0.0", - "express": "^5.2.1", - "graceful-fs": "^4.2.11", - "http-proxy-middleware": "^4.1.1", - "ipaddr.js": "^2.3.0", - "launch-editor": "^2.14.1", - "open": "^11.0.0", - "p-retry": "^8.0.0", - "schema-utils": "^4.3.3", - "selfsigned": "^5.5.0", - "serve-index": "^1.9.2", - "tinyglobby": "^0.2.15", - "webpack-dev-middleware": "^8.0.3", - "ws": "^8.20.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 22.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.101.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", - "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, - "node_modules/webpack/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", - "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.15", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/workbox-background-sync": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", - "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", - "dev": true, - "license": "MIT", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", - "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-build": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", - "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.24.4", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-replace": "^6.0.3", - "@rollup/plugin-terser": "^1.0.0", - "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "eta": "^4.5.1", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^11.0.1", - "pretty-bytes": "^5.3.0", - "rollup": "^4.53.3", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "7.4.1", - "workbox-broadcast-update": "7.4.1", - "workbox-cacheable-response": "7.4.1", - "workbox-core": "7.4.1", - "workbox-expiration": "7.4.1", - "workbox-google-analytics": "7.4.1", - "workbox-navigation-preload": "7.4.1", - "workbox-precaching": "7.4.1", - "workbox-range-requests": "7.4.1", - "workbox-recipes": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1", - "workbox-streams": "7.4.1", - "workbox-sw": "7.4.1", - "workbox-window": "7.4.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", - "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonpointer": "^5.0.1", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/workbox-build/node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/workbox-build/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/workbox-build/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/workbox-build/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/workbox-build/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/workbox-build/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/workbox-build/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/workbox-build/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", - "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-core": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", - "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/workbox-expiration": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", - "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-google-analytics": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", - "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-background-sync": "7.4.1", - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", - "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-precaching": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", - "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-range-requests": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", - "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-recipes": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", - "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-cacheable-response": "7.4.1", - "workbox-core": "7.4.1", - "workbox-expiration": "7.4.1", - "workbox-precaching": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-routing": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", - "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-strategies": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", - "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-streams": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", - "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1" - } - }, - "node_modules/workbox-sw": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", - "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", - "dev": true, - "license": "MIT" - }, - "node_modules/workbox-webpack-plugin": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-7.4.1.tgz", - "integrity": "sha512-Gf8th+7jJbzjABMeZxJDDeKyxmHcsWjXos4vQQRD+6KIk0vtv1k9c6Yf9aletamS3liN3HGojzxbrXuNc+wu4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "7.4.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.91.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/workbox-window": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", - "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "7.4.1" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", - "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wsl-utils/node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-naming": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", - "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} --- a/build/frontend-legacy/package.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "name": "nextcloud-ui-legacy", - "version": "1.0.0", - "private": true, - "description": "Nextcloud Server", - "license": "AGPL-3.0-or-later", - "author": "Nextcloud GmbH and Nextcloud contributors", - "scripts": { - "build": "webpack --node-env production --progress", - "dev": "webpack --node-env development --progress", - "postinstall": "patch-package", - "lint": "eslint --suppressions-location ../eslint-baseline-legacy.json --no-error-on-unmatched-pattern ./apps/*/ ./core/", - "lint:fix": "eslint --suppressions-location ../eslint-baseline-legacy.json --fix --no-error-on-unmatched-pattern ./apps/*/ ./core/", - "test": "vitest run", - "test:coverage": "vitest run --coverage --reporter=default", - "test:update-snapshots": "vitest run --update", - "test:watch": "vitest watch", - "watch": "webpack --node-env development --progress --watch" - }, - "browserslist": [ - "extends @nextcloud/browserslist-config" - ], - "overrides": { - "@vitejs/plugin-vue2": { - "vite": "^7" - }, - "@vueuse/integrations": { - "focus-trap": "^8" - } - }, - "dependencies": { - "@chenfengyuan/vue-qrcode": "^1.0.2", - "@mdi/js": "^7.4.47", - "@mdi/svg": "^7.4.47", - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/dialogs": "^7.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/files": "^4.1.0", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/moment": "^1.3.5", - "@nextcloud/password-confirmation": "^6.1.0", - "@nextcloud/paths": "^3.1.0", - "@nextcloud/router": "^3.2.0", - "@nextcloud/sharing": "^1.0.0-beta.4", - "@nextcloud/upload": "^1.12.0", - "@nextcloud/vue": "^8.41.0", - "@simplewebauthn/browser": "^13.3.0", - "@vue/web-component-wrapper": "^1.3.0", - "@vueuse/components": "^11.3.0", - "@vueuse/core": "^11.3.0", - "@vueuse/integrations": "^11.3.0", - "blurhash": "^2.0.5", - "browserslist-useragent-regexp": "^4.1.4", - "camelcase": "^9.0.0", - "cancelable-promise": "^4.3.1", - "color": "^5.0.3", - "core-js": "^3.50.0", - "crypto-browserify": "^3.12.1", - "debounce": "^3.0.0", - "dompurify": "^3.4.16", - "escape-html": "^1.0.3", - "focus-trap": "^8.2.2", - "is-svg": "^6.1.0", - "libphonenumber-js": "^1.13.14", - "lodash": "^4.18.1", - "marked": "^18.0.14", - "moment": "^2.31.0", - "moment-timezone": "^0.6.4", - "p-limit": "^7.3.3", - "p-queue": "^9.3.3", - "path": "^0.12.7", - "pinia": "^2.3.1", - "query-string": "^9.5.1", - "regenerator-runtime": "^0.14.1", - "throttle-debounce": "^5.0.2", - "v-click-outside": "^3.2.0", - "v-tooltip": "^2.1.3", - "vue": "^2.7.16", - "vue-click-outside": "^1.1.0", - "vue-cropperjs": "^4.2.0", - "vue-frag": "^1.4.3", - "vue-infinite-loading": "^2.4.5", - "vue-localstorage": "^0.6.2", - "vue-material-design-icons": "^5.3.1", - "vue-router": "^3.6.5", - "vue2-teleport": "^1.2.2", - "vuedraggable": "^2.24.3", - "vuex": "^3.6.2", - "vuex-router-sync": "^5.0.0", - "webdav": "^5.11.0" - }, - "devDependencies": { - "@babel/node": "^7.29.7", - "@babel/plugin-transform-private-methods": "^7.29.7", - "@babel/preset-typescript": "^7.29.7", - "@nextcloud/babel-config": "^1.3.0", - "@nextcloud/typings": "^1.10.0", - "@nextcloud/webpack-vue-config": "^7.0.4", - "@pinia/testing": "^0.1.7", - "@testing-library/jest-dom": "^7.0.1", - "@testing-library/user-event": "^14.6.7", - "@testing-library/vue": "^5.8.3", - "@types/throttle-debounce": "^5.0.2", - "@vitejs/plugin-vue2": "^2.3.4", - "@vitest/coverage-v8": "^4.1.11", - "@vue/test-utils": "^1.3.5", - "@vue/tsconfig": "~0.6.0", - "babel-loader-exclude-node-modules-except": "^1.2.4", - "babel-plugin-module-resolver": "^5.0.3", - "colord": "^2.10.0", - "exports-loader": "^5.0.0", - "file-loader": "^6.2.0", - "handlebars-loader": "^1.7.3", - "mime": "^4.1.0", - "msw": "^2.15.0", - "patch-package": "^8.0.1", - "raw-loader": "^4.0.2", - "regextras": "^0.8.0", - "sass": "^1.105.0", - "terser-webpack-plugin": "^5.6.1", - "typescript": "^5.9.2", - "vite": "^7.3.6", - "vitest": "^4.1.9", - "vue-loader": "^15.11.1", - "vue-template-compiler": "^2.7.16", - "webpack": "^5.111.1", - "webpack-cli": "^6.0.1", - "webpack-merge": "^6.0.1", - "workbox-webpack-plugin": "^7.4.1" - }, - "engines": { - "node": "^24.0.0", - "npm": "^11.3.0" - } -} --- a/build/frontend-legacy/patches/@vue+web-component-wrapper+1.3.0.patch +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: MIT -# SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors -# -# https://github.com/vuejs/vue-web-component-wrapper/pull/133 -# https://github.com/vuejs/vue-web-component-wrapper/pull/122 -# -diff --git a/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js b/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js -index 721bf1f..adba8e8 100644 ---- a/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js -+++ b/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js -@@ -222,10 +222,17 @@ function wrap (Vue, Component) { - if (!wrapper._isMounted) { - // initialize attributes - const syncInitialAttributes = () => { -- wrapper.props = getInitialProps(camelizedPropsList); -+ wrapper.props = Object.assign( -+ getInitialProps(camelizedPropsList), -+ wrapper.props -+ ) - hyphenatedPropsList.forEach(key => { -- syncAttribute(this, key); -- }); -+ const camelized = camelize(key) -+ // Maybe setted by Element properties earler -+ if (typeof wrapper.props[camelized] === 'undefined' || this.hasAttribute(key)) { -+ syncAttribute(this, key) -+ } -+ }) - }; - - if (isInitialized) { -diff --git a/node_modules/@vue/web-component-wrapper/types/index.d.ts b/node_modules/@vue/web-component-wrapper/types/index.d.ts -index 8b67b7b..612102c 100644 ---- a/node_modules/@vue/web-component-wrapper/types/index.d.ts -+++ b/node_modules/@vue/web-component-wrapper/types/index.d.ts -@@ -3,6 +3,6 @@ import _Vue, { Component, AsyncComponent } from 'vue' - declare function wrap( - Vue: typeof _Vue, - Component: Component | AsyncComponent --): HTMLElement -+): CustomElementConstructor - - export default wrap --- a/build/frontend-legacy/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "@vue/tsconfig/tsconfig.json", - "include": ["./apps/**/*.ts", "./apps/**/*.vue", "./core/**/*.ts", "./core/**/*.vue", "./*.d.ts"], - "exclude": ["./**/*.cy.ts"], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "types": ["node", "vue", "vue-router"], - "outDir": "./dist/", - "target": "ESNext", - "module": "ESNext", - // Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "noEmit": true, - // Allow ts to import js files - "allowJs": true, - "allowSyntheticDefaultImports": true, - "declaration": false, - "noImplicitAny": false, - "resolveJsonModule": true, - "strict": true, - }, - "vueCompilerOptions": { - "target": 2.7 - }, - "ts-node": { - // these options are overrides used only by ts-node - // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable - "compilerOptions": { - "moduleResolution": "node", - "module": "commonjs", - "verbatimModuleSyntax": false - } - } -} --- a/build/frontend-legacy/vitest.config.mts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ - -import vue from '@vitejs/plugin-vue2' -import { exec } from 'node:child_process' -import { resolve } from 'node:path' -import { promisify } from 'node:util' -import { nodePolyfills } from 'vite-plugin-node-polyfills' -import { defaultExclude, defineConfig } from 'vitest/config' - -const gitIgnore: string[] = [] -// get all files ignored in the apps directory (e.g. if putting `view` app there). -try { - const execAsync = promisify(exec) - const { stdout } = await execAsync('git check-ignore apps/*', { cwd: __dirname }) - gitIgnore.push(...stdout.split('\n').filter(Boolean)) - // eslint-disable-next-line no-console - console.log('Git ignored files excluded from tests: ', gitIgnore) -} catch (error) { - // we can ignore error code 1 as this just means there are no ignored files - if (error && (typeof error !== 'object' || !('code' in error && error.code === 1))) { - // but otherwise something bad is happening and we should re-throw - throw error - } -} - -export default defineConfig({ - root: import.meta.dirname, - // define some dummy globals for the tests - define: { - appName: '"nextcloud"', - appVersion: '"1.0.0"', - }, - plugins: [ - nodePolyfills({ - include: ['fs', 'path'], - protocolImports: false, - }), - vue(), - ], - resolve: { - preserveSymlinks: true, - alias: { - vue$: resolve(__dirname, './node_modules/vue/dist/vue.js'), - }, - }, - test: { - include: ['./{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - environment: 'jsdom', - environmentOptions: { - jsdom: { - url: 'http://nextcloud.local', - }, - }, - coverage: { - include: ['./apps/*/src/**', 'core/src/**'], - exclude: ['**.spec.*', '**.test.*', '**.cy.*', 'core/src/tests/**'], - reporter: ['lcov', 'text'], - reportsDirectory: resolve(import.meta.dirname, '../../coverage/legacy'), - }, - setupFiles: [ - './__tests__/mock-window.js', - './__tests__/setup-testing-library.js', - ], - exclude: [ - ...defaultExclude, - ...gitIgnore, - ], - globalSetup: './__tests__/setup-global.js', - server: { - deps: { - // @see https://github.com/vitest-dev/vitest/issues/7950 - // inline: true, - inline: [/^(?!.*vitest).*$/], - }, - }, - }, -}) --- a/build/frontend-legacy/webpack.common.cjs +++ /dev/null @@ -1,305 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except') -const { readFileSync } = require('fs') -const NodePolyfillPlugin = require('node-polyfill-webpack-plugin') -const path = require('path') -const { VueLoaderPlugin } = require('vue-loader') -const webpack = require('webpack') -const WorkboxPlugin = require('workbox-webpack-plugin') -const modules = require('./webpack.modules.cjs') -const WebpackSPDXPlugin = require('./WebpackSPDXPlugin.cjs') - -const appVersion = readFileSync(path.join(__dirname, '../../version.php')).toString().match(/OC_Version.+\[([0-9]{2})/)?.[1] ?? 'unknown' -const isDev = process.env.NODE_ENV === 'development' - -/** - * @param {Record>} modules - The modules object from webpack.modules.cjs - */ -function formatOutputFromModules(modules) { - // merge all configs into one object, and use AppID to generate the fileNames - // with the following format: - // AppId-fileName: path/to/js-file.js - const moduleEntries = Object.keys(modules).map((moduleKey) => { - const module = modules[moduleKey] - - const entries = Object.keys(module).map((entryKey) => { - const entry = module[entryKey] - return { [`${moduleKey}-${entryKey}`]: entry } - }) - - return Object.assign({}, ...Object.values(entries)) - }) - return Object.assign({}, ...Object.values(moduleEntries)) -} - -/** - * - */ -function modulesToBuild() { - const MODULE = process?.env?.MODULE - if (MODULE) { - if (!modules[MODULE]) { - throw new Error(`No module "${MODULE}" found`) - } - return formatOutputFromModules({ - [MODULE]: modules[MODULE], - }) - } - - return formatOutputFromModules(modules) -} - -const config = { - entry: modulesToBuild(), - output: { - // Step away from the src folder and extract to the js folder - path: path.join(__dirname, '../../dist'), - // Let webpack determine automatically where it's located - publicPath: 'auto', - filename: '[name].js?v=[contenthash]', - chunkFilename: '[name]-[id].js?v=[contenthash]', - // Make sure sourcemaps have a proper path and do not - // leak local paths https://github.com/webpack/webpack/issues/3603 - devtoolNamespace: 'nextcloud', - devtoolModuleFilenameTemplate(info) { - const rootDir = process?.cwd() - const rel = path.relative(rootDir, info.absoluteResourcePath) - return `webpack:///nextcloud/${rel}` - }, - clean: false, - }, - - module: { - rules: [ - { - test: /davclient/, - loader: 'exports-loader', - options: { - type: 'commonjs', - exports: 'dav', - }, - }, - { - test: /\.css$/, - oneOf: [ - { - resourceQuery: /module/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - modules: { - namedExport: false, - localIdentName: '_[local]_[hash:base64:5]', - exportLocalsConvention: 'asIs', - }, - }, - }, - ], - }, - { - use: [ - 'style-loader', - 'css-loader', - ], - }, - ], - }, - { - test: /\.scss$/, - use: ['style-loader', 'css-loader', 'sass-loader'], - }, - { - test: /\.vue$/, - loader: 'vue-loader', - exclude: BabelLoaderExcludeNodeModulesExcept([ - 'vue-material-design-icons', - 'emoji-mart-vue-fast', - ]), - }, - { - test: /\.tsx?$/, - use: [ - 'babel-loader', - { - // Fix TypeScript syntax errors in Vue - loader: 'ts-loader', - options: { - transpileOnly: true, - }, - }, - ], - exclude: BabelLoaderExcludeNodeModulesExcept([]), - }, - { - test: /\.js$/, - loader: 'babel-loader', - // automatically detect necessary packages to - // transpile in the node_modules folder - exclude: BabelLoaderExcludeNodeModulesExcept([ - '@nextcloud/dialogs', - '@nextcloud/event-bus', - 'davclient.js', - 'nextcloud-vue-collections', - 'p-finally', - 'p-limit', - 'p-locate', - 'p-queue', - 'p-timeout', - 'p-try', - 'semver', - 'striptags', - 'toastify-js', - 'v-tooltip', - 'yocto-queue', - ]), - }, - { - test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf)$/, - type: 'asset/inline', - }, - { - test: /\.handlebars/, - loader: 'handlebars-loader', - }, - { - resourceQuery: /raw/, - type: 'asset/source', - }, - ], - }, - - optimization: { - splitChunks: { - automaticNameDelimiter: '-', - minChunks: 3, // minimum number of chunks that must share the module - cacheGroups: { - vendors: { - // split every dependency into one bundle - test: /[\\/]node_modules[\\/]/, - // necessary to keep this name to properly inject it - // see OC_Template.php - name: 'core-common', - chunks: 'all', - }, - }, - }, - }, - - plugins: [ - new VueLoaderPlugin(), - new NodePolyfillPlugin({ - additionalAliases: ['process'], - }), - - new WorkboxPlugin.GenerateSW({ - swDest: 'preview-service-worker.js', - clientsClaim: true, - skipWaiting: true, - exclude: [/.*/], // don't do pre-caching - inlineWorkboxRuntime: true, - sourcemap: false, - - // Increase perfs with less logging - disableDevLogs: true, - - // Define runtime caching rules. - runtimeCaching: [{ - // Match any preview file request - // /apps/files_trashbin/preview?fileId=156380&a=1 - // /core/preview?fileId=155842&a=1 - urlPattern: /^.*\/(apps|core)(\/[a-z-_]+)?\/preview.*/i, - - // Apply a strategy. - handler: 'CacheFirst', - - options: { - // Use a custom cache name. - cacheName: 'previews', - - // Only cache 10000 images. - expiration: { - maxAgeSeconds: 3600 * 24 * 7, // one week - maxEntries: 10000, - }, - }, - }], - }), - - new webpack.DefinePlugin({ PRODUCTION: JSON.stringify(process.env.NODE_ENV === 'production') }), - // Make appName & appVersion available as a constants for '@nextcloud/vue' components - new webpack.DefinePlugin({ appName: JSON.stringify('Nextcloud') }), - new webpack.DefinePlugin({ appVersion: JSON.stringify(appVersion) }), - - // @nextcloud/moment since v1.3.0 uses `moment/min/moment-with-locales.js` - // Which works only in Node.js and is not compatible with Webpack bundling - // It has an unused function `localLocale` that requires locales by invalid relative path `./locale` - // Though it is not used, Webpack tries to resolve it with `require.context` and fails - new webpack.IgnorePlugin({ - resourceRegExp: /^\.\/locale$/, - contextRegExp: /moment\/min$/, - }), - ], - externals: { - OC: 'OC', - OCA: 'OCA', - OCP: 'OCP', - }, - resolve: { - alias: { - // make sure to use the handlebar runtime when importing - handlebars: 'handlebars/runtime', - // allow to import from root (cross reference already migrated apps) - '~*': path.resolve(__dirname, '../../*'), - }, - extensions: ['*', '.ts', '.js', '.vue'], - extensionAlias: { - /** - * Resolve TypeScript files when using fully-specified esm import paths - * https://github.com/webpack/webpack/issues/13252 - */ - '.js': ['.js', '.ts'], - }, - symlinks: false, - fallback: { - fs: false, - }, - }, -} - -// Generate reuse license files if not in development mode -if (!isDev) { - config.plugins.push(new WebpackSPDXPlugin({ - override: { - select2: 'MIT', - '@nextcloud/axios': 'GPL-3.0-or-later', - '@nextcloud/vue': 'AGPL-3.0-or-later', - 'nextcloud-vue-collections': 'AGPL-3.0-or-later', - }, - })) - - config.optimization.minimizer = [{ - apply: (compiler) => { - // Lazy load the Terser plugin - const TerserPlugin = require('terser-webpack-plugin') - new TerserPlugin({ - extractComments: false, - terserOptions: { - format: { - comments: false, - }, - compress: { - passes: 2, - }, - }, - }).apply(compiler) - }, - }] -} - -module.exports = config --- a/build/frontend-legacy/webpack.config.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -const { merge } = require('webpack-merge') -const config = require('./webpack.common.cjs') - -let isDev = false -try { - const buildMode = process.env.NODE_ENV - isDev = buildMode === 'development' -} catch (e) { - console.error('Could not determine build mode', e) -} - -module.exports = merge(config, { - mode: isDev ? 'development' : 'production', - devtool: isDev ? 'cheap-source-map' : 'source-map', -}) --- a/build/frontend-legacy/webpack.modules.cjs +++ /dev/null @@ -1,74 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -const path = require('path') - -module.exports = { - core: { - 'ajax-cron': path.join(__dirname, 'core/src', 'ajax-cron.ts'), - install: path.join(__dirname, 'core/src', 'install.ts'), - login: path.join(__dirname, 'core/src', 'login.js'), - login_flow: path.join(__dirname, 'core/src', 'login-flow.ts'), - main: path.join(__dirname, 'core/src', 'main.js'), - maintenance: path.join(__dirname, 'core/src', 'maintenance.js'), - 'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'), - 'public-page-user-menu': path.resolve(__dirname, 'core/src', 'public-page-user-menu.ts'), - recommendedapps: path.join(__dirname, 'core/src', 'recommendedapps.js'), - 'unified-search': path.join(__dirname, 'core/src', 'unified-search.ts'), - 'legacy-unified-search': path.join(__dirname, 'core/src', 'legacy-unified-search.js'), - 'unsupported-browser': path.join(__dirname, 'core/src', 'unsupported-browser.js'), - 'unsupported-browser-redirect': path.join(__dirname, 'core/src', 'unsupported-browser-redirect.js'), - public: path.join(__dirname, 'core/src', 'public.ts'), - public_share_auth: path.join(__dirname, 'core/src', 'public-share-auth.ts'), - 'twofactor-request-token': path.join(__dirname, 'core/src', 'twofactor-request-token.ts'), - update: path.join(__dirname, 'core/src', 'update.ts'), - }, - files: { - sidebar: path.join(__dirname, 'apps/files/src', 'main-sidebar.ts'), - main: path.join(__dirname, 'apps/files/src', 'main.ts'), - init: path.join(__dirname, 'apps/files/src', 'init.ts'), - search: path.join(__dirname, 'apps/files/src/plugins/search', 'folderSearch.ts'), - 'settings-admin': path.join(__dirname, 'apps/files/src', 'main-settings-admin.ts'), - 'settings-personal': path.join(__dirname, 'apps/files/src', 'main-settings-personal.ts'), - 'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.ts'), - }, - files_sharing: { - additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'), - collaboration: path.join(__dirname, 'apps/files_sharing/src', 'collaborationresourceshandler.js'), - files_sharing_tab: path.join(__dirname, 'apps/files_sharing/src', 'files-sidebar.ts'), - init: path.join(__dirname, 'apps/files_sharing/src', 'init.ts'), - 'init-public': path.join(__dirname, 'apps/files_sharing/src', 'init-public.ts'), - main: path.join(__dirname, 'apps/files_sharing/src', 'main.ts'), - 'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'), - 'public-nickname-handler': path.join(__dirname, 'apps/files_sharing/src', 'public-nickname-handler.ts'), - }, - settings: { - main: path.join(__dirname, 'apps/settings/src', 'main-settings.ts'), - 'vue-settings-admin-overview': path.join(__dirname, 'apps/settings/src', 'main-admin-overview.ts'), - 'vue-settings-admin-basic-settings': path.join(__dirname, 'apps/settings/src', 'main-admin-basic-settings.js'), - 'vue-settings-admin-ai': path.join(__dirname, 'apps/settings/src', 'main-admin-ai.js'), - 'vue-settings-admin-delegation': path.join(__dirname, 'apps/settings/src', 'main-admin-delegation.js'), - 'vue-settings-admin-mail': path.join(__dirname, 'apps/settings/src', 'admin-settings-mail.ts'), - 'vue-settings-admin-security': path.join(__dirname, 'apps/settings/src', 'main-admin-security.js'), - 'vue-settings-admin-settings-presets': path.join(__dirname, 'apps/settings/src', 'main-admin-settings-presets.js'), - 'vue-settings-admin-sharing': path.join(__dirname, 'apps/settings/src', 'admin-settings-sharing.ts'), - 'vue-settings-nextcloud-pdf': path.join(__dirname, 'apps/settings/src', 'main-nextcloud-pdf.js'), - 'vue-settings-profile-contact': path.join(__dirname, 'apps/settings/src', 'main-profile-contact.js'), - 'vue-settings-language-locale': path.join(__dirname, 'apps/settings/src', 'main-language-locale.js'), - 'vue-settings-personal-password': path.join(__dirname, 'apps/settings/src', 'main-personal-password.js'), - 'vue-settings-personal-security': path.join(__dirname, 'apps/settings/src', 'main-personal-security.js'), - 'vue-settings-personal-webauthn': path.join(__dirname, 'apps/settings/src', 'main-personal-webauth.js'), - 'vue-settings-users-management': path.join(__dirname, 'apps/settings/src', 'main-users-management.ts'), - 'declarative-settings-forms': path.join(__dirname, 'apps/settings/src', 'main-declarative-settings-forms.ts'), - }, - updatenotification: { - init: path.join(__dirname, 'apps/updatenotification/src', 'init.ts'), - 'view-changelog-page': path.join(__dirname, 'apps/updatenotification/src', 'view-changelog-page.ts'), - updatenotification: path.join(__dirname, 'apps/updatenotification/src', 'updatenotification.js'), - 'update-notification-legacy': path.join(__dirname, 'apps/updatenotification/src', 'update-notification-legacy.ts'), - }, - workflowengine: { - workflowengine: path.join(__dirname, 'apps/workflowengine/src', 'workflowengine.js'), - }, -} --- a/build/frontend/REUSE.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 1 -SPDX-PackageName = "nextcloud" -SPDX-PackageSupplier = "Nextcloud " -SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server" - -[[annotations]] -path = ["package.json", "package-lock.json", "tsconfig.json"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" --- a/build/frontend/__mocks__/@nextcloud/auth.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function getCurrentUser() { - return { - uid: 'test', - displayName: 'Test', - isAdmin: false, - } -} - -export function getRequestToken() { - return 'test-token-1234' -} - -export function onRequestTokenUpdate() {} --- a/build/frontend/__mocks__/@nextcloud/axios.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default { - interceptors: { - response: { - use: () => {}, - }, - request: { - use: () => {}, - }, - }, - get: async () => ({ status: 200, data: {} }), - delete: async () => ({ status: 200, data: {} }), - post: async () => ({ status: 200, data: {} }), - head: async () => ({ status: 200, data: {} }), -} --- a/build/frontend/__mocks__/@nextcloud/capabilities.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { Capabilities } from '../../apps/files/src/types.ts' - -export function getCapabilities(): Capabilities { - return { - files: { - bigfilechunking: true, - blacklisted_files: [], - forbidden_filename_basenames: [], - forbidden_filename_characters: [], - forbidden_filename_extensions: [], - forbidden_filenames: [], - undelete: true, - version_deletion: true, - version_labeling: true, - versioning: true, - }, - } -} --- a/build/frontend/__mocks__/@nextcloud/dialogs.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { vi } from 'vitest' - -export const showMessage = vi.fn() -export const showSuccess = vi.fn() -export const showWarning = vi.fn() -export const showInfo = vi.fn() -export const showError = vi.fn() -export const showUndo = vi.fn() --- a/build/frontend/__mocks__/@nextcloud/initial-state.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function loadState(app: string, key: string, fallback?: any) { - return fallback -} --- a/build/frontend/__mocks__/css.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default {} --- a/build/frontend/__mocks__/svg.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -export default 'SvgMock' --- a/build/frontend/__mocks__/webdav.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -export function createClient() {} -export function getPatcher() { - return { - patch: () => {}, - } -} --- a/build/frontend/__tests__/FileSystemAPIUtils.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import mime from 'mime' -import { basename } from 'node:path' - -class FileSystemEntry { - private _isFile: boolean - private _fullPath: string - - constructor(isFile: boolean, fullPath: string) { - this._isFile = isFile - this._fullPath = fullPath - } - - get isFile() { - return !!this._isFile - } - - get isDirectory() { - return !this.isFile - } - - get name() { - return basename(this._fullPath) - } -} - -export class FileSystemFileEntry extends FileSystemEntry { - private _contents: string - private _lastModified: number - - constructor(fullPath: string, contents: string, lastModified = Date.now()) { - super(true, fullPath) - this._contents = contents - this._lastModified = lastModified - } - - file(success: (file: File) => void) { - const lastModified = this._lastModified - // Faking the mime by using the file extension - const type = mime.getType(this.name) || '' - success(new File([this._contents], this.name, { lastModified, type })) - } -} - -export class FileSystemDirectoryEntry extends FileSystemEntry { - private _entries: FileSystemEntry[] - - constructor(fullPath: string, entries: FileSystemEntry[]) { - super(false, fullPath) - this._entries = entries || [] - } - - createReader() { - let read = false - return { - readEntries: (success: (entries: FileSystemEntry[]) => void) => { - if (read) { - return success([]) - } - read = true - success(this._entries) - }, - } - } -} - -/** - * This mocks the File API's File class - * It will allow us to test the Filesystem API as well as the - * File API in the same test suite. - */ -export class DataTransferItem { - private _type: string - private _entry: FileSystemEntry - - getAsEntry?: () => FileSystemEntry - - constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) { - this._type = type - this._entry = entry - - // Only when the Files API is available we are - // able to get the entry - if (isFileSystemAPIAvailable) { - this.getAsEntry = () => this._entry - } - } - - get kind() { - return 'file' - } - - get type() { - return this._type - } - - getAsFile(): File | null { - if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) { - let file: File | null = null - this._entry.file((f) => { - file = f - }) - return file - } - - // The browser will return an empty File object if the entry is a directory - return new File([], this._entry.name, { type: '' }) - } -} - -export function fileSystemEntryToDataTransferItem(entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem { - return new DataTransferItem( - entry.isFile ? 'text/plain' : 'httpd/unix-directory', - entry, - isFileSystemAPIAvailable, - ) -} --- a/build/frontend/__tests__/mock-window.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -window.OC = { - ...window.OC, - config: { - version: '32.0.0', - ...(window.OC?.config ?? {}), - }, -} -window.OCA = { ...window.OCA } -window.OCP = { ...window.OCP } - -window._oc_webroot = '' - -// jsdom does not implement `innerText` at all, while the specification defines it -// to fall back to `textContent` for elements that are not being rendered. -// @see https://github.com/jsdom/jsdom/issues/1245 -if (!('innerText' in HTMLElement.prototype)) { - Object.defineProperty(HTMLElement.prototype, 'innerText', { - configurable: true, - get() { - return this.textContent - }, - set(value) { - this.textContent = value - }, - }) -} --- a/build/frontend/__tests__/setup-global.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ - -export function setup() { - process.env.TZ = 'UTC' -} --- a/build/frontend/__tests__/setup-testing-library.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ - -import '@testing-library/jest-dom/vitest' -import 'core-js/stable/index.js' --- a/build/frontend/apps/appstore +++ /dev/null @@ -1 +0,0 @@ -../../../apps/appstore \ No newline at end of file --- a/build/frontend/apps/comments +++ /dev/null @@ -1 +0,0 @@ -../../../apps/comments \ No newline at end of file --- a/build/frontend/apps/dashboard +++ /dev/null @@ -1 +0,0 @@ -../../../apps/dashboard \ No newline at end of file --- a/build/frontend/apps/dav +++ /dev/null @@ -1 +0,0 @@ -../../../apps/dav \ No newline at end of file --- a/build/frontend/apps/encryption +++ /dev/null @@ -1 +0,0 @@ -../../../apps/encryption \ No newline at end of file --- a/build/frontend/apps/federatedfilesharing +++ /dev/null @@ -1 +0,0 @@ -../../../apps/federatedfilesharing \ No newline at end of file --- a/build/frontend/apps/federation +++ /dev/null @@ -1 +0,0 @@ -../../../apps/federation \ No newline at end of file --- a/build/frontend/apps/files_external +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files_external \ No newline at end of file --- a/build/frontend/apps/files_reminders +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files_reminders \ No newline at end of file --- a/build/frontend/apps/files_trashbin +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files_trashbin \ No newline at end of file --- a/build/frontend/apps/files_versions +++ /dev/null @@ -1 +0,0 @@ -../../../apps/files_versions \ No newline at end of file --- a/build/frontend/apps/oauth2 +++ /dev/null @@ -1 +0,0 @@ -../../../apps/oauth2 \ No newline at end of file --- a/build/frontend/apps/profile +++ /dev/null @@ -1 +0,0 @@ -../../../apps/profile \ No newline at end of file --- a/build/frontend/apps/sharebymail +++ /dev/null @@ -1 +0,0 @@ -../../../apps/sharebymail \ No newline at end of file --- a/build/frontend/apps/systemtags +++ /dev/null @@ -1 +0,0 @@ -../../../apps/systemtags \ No newline at end of file --- a/build/frontend/apps/theming +++ /dev/null @@ -1 +0,0 @@ -../../../apps/theming \ No newline at end of file --- a/build/frontend/apps/twofactor_backupcodes +++ /dev/null @@ -1 +0,0 @@ -../../../apps/twofactor_backupcodes \ No newline at end of file --- a/build/frontend/apps/user_ldap +++ /dev/null @@ -1 +0,0 @@ -../../../apps/user_ldap \ No newline at end of file --- a/build/frontend/apps/user_status +++ /dev/null @@ -1 +0,0 @@ -../../../apps/user_status/ \ No newline at end of file --- a/build/frontend/apps/weather_status +++ /dev/null @@ -1 +0,0 @@ -../../../apps/weather_status \ No newline at end of file --- a/build/frontend/package-lock.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "nextcloud-ui", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nextcloud-ui", - "version": "1.0.0", - "license": "AGPL-3.0-or-later", - "engines": { - "node": "^24.0.0", - "npm": "^11.3.0" - } - } - } -} --- a/build/frontend/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "nextcloud-ui", - "version": "1.0.0", - "private": true, - "description": "Nextcloud Server Vue 3 UI", - "license": "AGPL-3.0-or-later", - "author": "Nextcloud GmbH and Nextcloud contributors", - "type": "module", - "scripts": { - "build": "vite build", - "dev": "NODE_ENV=development vite build --mode development", - "lint": "eslint --suppressions-location ../eslint-baseline.json --no-error-on-unmatched-pattern ./apps/*/ ./core/", - "lint:fix": "eslint --suppressions-location ../eslint-baseline.json --fix --no-error-on-unmatched-pattern ./apps/*/ ./core/", - "test": "vitest run", - "test:coverage": "vitest run --coverage --reporter=default", - "test:update-snapshots": "vitest run --update", - "test:watch": "vitest watch", - "watch": "NODE_ENV=development vite build --mode development --watch" - }, - "browserslist": [ - "extends @nextcloud/browserslist-config" - ], - "engines": { - "node": "^24.0.0", - "npm": "^11.3.0" - } -} --- a/build/frontend/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["./apps", "./core"] -} \ No newline at end of file --- a/build/frontend/vite.config.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ - -import { createAppConfig } from '@nextcloud/vite-config' -import { resolve } from 'node:path' - -const modules = { - appstore: { - main: resolve(import.meta.dirname, 'apps/appstore/src', 'main.ts'), - }, - comments: { - 'comments-app': resolve(import.meta.dirname, 'apps/comments/src', 'comments-app.ts'), - 'comments-tab': resolve(import.meta.dirname, 'apps/comments/src', 'files-sidebar.ts'), - init: resolve(import.meta.dirname, 'apps/comments/src', 'init.ts'), - }, - dashboard: { - main: resolve(import.meta.dirname, 'apps/dashboard/src', 'main.js'), - }, - dav: { - 'settings-admin-caldav': resolve(import.meta.dirname, 'apps/dav/src', 'settings-admin.ts'), - 'settings-admin-example-content': resolve(import.meta.dirname, 'apps/dav/src', 'settings-admin-example-content.ts'), - 'settings-personal-availability': resolve(import.meta.dirname, 'apps/dav/src', 'settings-personal-availability.ts'), - }, - encryption: { - encryption: resolve(import.meta.dirname, 'apps/encryption/src', 'encryption.ts'), - settings_admin: resolve(import.meta.dirname, 'apps/encryption/src', 'settings-admin.ts'), - settings_personal: resolve(import.meta.dirname, 'apps/encryption/src', 'settings-personal.ts'), - }, - federation: { - 'settings-admin': resolve(import.meta.dirname, 'apps/federation/src', 'settings-admin.ts'), - }, - federatedfilesharing: { - 'init-files': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'init-files.js'), - 'settings-admin': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-admin.ts'), - 'settings-personal': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-personal.ts'), - }, - files_external: { - auth_rsa: resolve(import.meta.dirname, 'apps/files_external/src', 'auth-rsa.ts'), - - init: resolve(import.meta.dirname, 'apps/files_external/src', 'init-files.ts'), - init_settings: resolve(import.meta.dirname, 'apps/files_external/src', 'init-settings.ts'), - settings: resolve(import.meta.dirname, 'apps/files_external/src', 'settings-main.ts'), - }, - files_reminders: { - init: resolve(import.meta.dirname, 'apps/files_reminders/src', 'files-init.ts'), - }, - files_trashbin: { - init: resolve(import.meta.dirname, 'apps/files_trashbin/src', 'files-init.ts'), - }, - files_versions: { - 'sidebar-tab': resolve(import.meta.dirname, 'apps/files_versions/src', 'sidebar_tab.ts'), - workflow: resolve(import.meta.dirname, 'apps/files_versions/src', 'workflow.ts'), - }, - oauth2: { - 'settings-admin': resolve(import.meta.dirname, 'apps/oauth2/src', 'settings-admin.ts'), - }, - profile: { - main: resolve(import.meta.dirname, 'apps/profile/src', 'main.ts'), - reference: resolve(import.meta.dirname, 'apps/profile/src', 'reference.js'), - }, - sharebymail: { - 'admin-settings': resolve(import.meta.dirname, 'apps/sharebymail/src', 'settings-admin.ts'), - }, - systemtags: { - init: resolve(import.meta.dirname, 'apps/systemtags/src', 'init.ts'), - admin: resolve(import.meta.dirname, 'apps/systemtags/src', 'admin.ts'), - }, - theming: { - 'settings-personal': resolve(import.meta.dirname, 'apps/theming/src', 'settings-personal.ts'), - 'settings-admin': resolve(import.meta.dirname, 'apps/theming/src', 'settings-admin.ts'), - theming: resolve(import.meta.dirname, 'apps/theming/src', 'theming.ts'), - }, - twofactor_backupcodes: { - 'settings-personal': resolve(import.meta.dirname, 'apps/twofactor_backupcodes/src', 'settings-personal.ts'), - }, - user_ldap: { - renewPassword: resolve(import.meta.dirname, 'apps/user_ldap/src', 'renewPassword.ts'), - 'settings-admin': resolve(import.meta.dirname, 'apps/user_ldap/src', 'settings-admin.ts'), - }, - user_status: { - menu: resolve(import.meta.dirname, 'apps/user_status/src', 'menu.js'), - }, - weather_status: { - 'weather-status': resolve(import.meta.dirname, 'apps/weather_status/src', 'weather-status.js'), - }, -} - -// convert modules to modules entries prefied with the app id -const viteModuleEntries = Object.entries(modules) - .map(([appId, entries]) => ( - Object.entries(entries) - .map(([entryName, entryPath]) => [`${appId}-${entryName}`, entryPath]) - )) - .flat(1) - -export default createAppConfig(Object.fromEntries(viteModuleEntries), { - createEmptyCSSEntryPoints: true, - emptyOutputDirectory: { - additionalDirectories: [resolve(import.meta.dirname, '../..', 'dist')], - }, - extractLicenseInformation: { - includeSourceMaps: true, - }, - config: { - root: resolve(import.meta.dirname, '../..'), - resolve: { - preserveSymlinks: true, - }, - build: { - outDir: 'dist', - rollupOptions: { - output: { - entryFileNames: '[name].mjs', - chunkFileNames: '[name]-[hash].chunk.mjs', - assetFileNames({ originalFileNames }) { - const apps = originalFileNames.map((name) => name.match(/apps\/([^/]+)\//)?.[1]) - .filter(Boolean) - const appId = apps.length === 1 ? apps[0] : 'common' - return `${appId}-[name]-[hash][extname]` - }, - experimentalMinChunkSize: 100 * 1024, - /* // with rolldown-vite: - advancedChunks: { - groups: [ - // one group for common dependencies - { name: 'common', test: /[\\/]node_modules[\\/]/ }, - // one group per app with a lower minShareCount to encourage sharing within the app - ...Object.keys(modules).map((name) => ({ - name, - test: new RegExp(`[\\\\/]apps[\\\\/]${name}[\\\\/]`), - minShareCount: 2, - })), - ], - // only include modules in the groups if they are used at least by 3 different chunks - minShareCount: 3, - // only include modules in the groups if they are smaller than 400kb on its own - // maxModuleSize: 400 * 1024, - // define the groups output size (not too small but also not too big!) - minSize: 100 * 1024, - maxSize: 800 * 1024, - }, - }, - experimental: { - strictExecutionOrder: true, - }, - */ - }, - }, - }, - experimental: { - renderBuiltUrl(filename, { hostType }) { - if (hostType === 'css') { - return `./${filename}` - } - return { - runtime: `window.OC.filePath('', '', 'dist/${filename}')`, - } - }, - }, - }, -}) --- a/build/frontend/vitest.config.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: CC0-1.0 - */ - -import vue from '@vitejs/plugin-vue' -import { exec } from 'node:child_process' -import { resolve } from 'node:path' -import { promisify } from 'node:util' -import { defaultExclude, defineConfig } from 'vitest/config' - -const gitIgnore: string[] = [] -// get all files ignored in the apps directory (e.g. if putting `view` app there). -try { - const execAsync = promisify(exec) - const { stdout } = await execAsync('git check-ignore apps/*', { cwd: __dirname }) - gitIgnore.push(...stdout.split('\n').filter(Boolean)) - // eslint-disable-next-line no-console - console.log('Git ignored files excluded from tests: ', gitIgnore) -} catch (error) { - // we can ignore error code 1 as this just means there are no ignored files - if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 1) { - // but otherwise something bad is happening and we should re-throw - throw error - } -} - -export default defineConfig({ - plugins: [vue()], - root: resolve(import.meta.dirname), - // define some dummy globals for the tests - define: { - appName: '"nextcloud"', - appVersion: '"1.0.0"', - }, - resolve: { - preserveSymlinks: true, - }, - server: { - fs: { - // The dependencies are installed in the repository root, which is outside of the vite root, - // so it needs to be allowed explicitly for assets like `*.svg?raw` to be loadable. - allow: [resolve(import.meta.dirname, '../..')], - }, - }, - test: { - include: ['apps/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - env: { - LANG: 'en_US', - TZ: 'UTC', - }, - environment: 'jsdom', - environmentOptions: { - jsdom: { - url: 'http://nextcloud.local', - }, - }, - coverage: { - include: [ - 'apps/*/src/**', - /* 'core/src/**', */ - ], - exclude: ['**.spec.*', '**.test.*', '**.cy.*', 'core/src/tests/**'], - reporter: ['lcov', 'text'], - reportsDirectory: resolve(import.meta.dirname, '../../coverage'), - }, - setupFiles: [ - resolve(import.meta.dirname, '__tests__/mock-window.js'), - resolve(import.meta.dirname, '__tests__/setup-testing-library.js'), - ], - exclude: [ - ...defaultExclude, - ...gitIgnore, - ], - globalSetup: resolve(import.meta.dirname, '__tests__/setup-global.js'), - server: { - deps: { - inline: [/@nextcloud\//], - }, - }, - onUnhandledError(error) { - // TODO: remove when this is fixed: https://github.com/nextcloud-libraries/nextcloud-vue/issues/8090 - if (error.message.includes('`fallbackFocus` was specified but was not a node, or did not return a node')) { - return false - } - }, - }, -}) --- a/build/htaccess-checker.php +++ /dev/null @@ -1,20 +0,0 @@ -/gmi - svg = svg.replace(fillRe, '<$1 fill="#' + color + '"/>') - - // replace any fill or stroke colors - svg = svg.replace(/stroke="#([a-z0-9]{3,6})"/gmi, 'stroke="#' + color + '"') - svg = svg.replace(/fill="#([a-z0-9]{3,6})"/gmi, 'fill="#' + color + '"') - - return svg -} - -/** - * - * @param invert - */ -function generateVariablesAliases(invert = false) { - let css = '' - Object.keys(variables).forEach((variable) => { - if (variable.indexOf('original-') !== -1) { - let finalVariable = variable.replace('original-', '') - if (invert) { - finalVariable = finalVariable.replace('white', 'tempwhite') - .replace('dark', 'white') - .replace('tempwhite', 'dark') - } - css += `${finalVariable}: var(${variable});` - } - }) - return css -} - -/** - * - * @param icon - * @param invert - */ -function formatIcon(icon, invert = false) { - const color1 = invert ? 'white' : 'dark' - const color2 = invert ? 'dark' : 'white' - return ` - .icon-${icon}, - .icon-${icon}-dark { - background-image: var(--icon-${icon}-${color1}); - } - .icon-${icon}-white, - .icon-${icon}.icon-white { - background-image: var(--icon-${icon}-${color2}); - }` -} -/** - * - * @param icon - */ -function formatIconColor(icon) { - const { color } = iconsColor[icon] - return ` - .icon-${icon} { - background-image: var(--icon-${icon}-${color}); - }` -} -/** - * - * @param alias - * @param invert - */ -function formatAlias(alias, invert = false) { - let icon = iconsAliases[alias] - if (invert) { - icon = icon.replace('white', 'tempwhite') - .replace('dark', 'white') - .replace('tempwhite', 'dark') - } - return ` - .${alias} { - background-image: var(--${icon}) - }` -} - -let css = '' -Object.keys(icons).forEach((icon) => { - const path = icons[icon] - - const svg = readFileSync(path, 'utf8') - const darkSvg = colorSvg(svg, '000000') - const whiteSvg = colorSvg(svg, 'ffffff') - - variables[`--original-icon-${icon}-dark`] = Buffer.from(darkSvg, 'utf-8').toString('base64') - variables[`--original-icon-${icon}-white`] = Buffer.from(whiteSvg, 'utf-8').toString('base64') -}) - -Object.keys(iconsColor).forEach((icon) => { - const { path, color } = iconsColor[icon] - - const svg = readFileSync(path, 'utf8') - const coloredSvg = colorSvg(svg, colors[color]) - variables[`--icon-${icon}-${color}`] = Buffer.from(coloredSvg, 'utf-8').toString('base64') -}) - -// ICONS VARIABLES LIST -css += ':root {' -Object.keys(variables).forEach((variable) => { - const data = variables[variable] - css += `${variable}: url(data:image/svg+xml;base64,${data});` -}) -css += '}' - -// DEFAULT THEME -css += 'body {' -css += generateVariablesAliases() -Object.keys(icons).forEach((icon) => { - css += formatIcon(icon) -}) -Object.keys(iconsColor).forEach((icon) => { - css += formatIconColor(icon) -}) -Object.keys(iconsAliases).forEach((alias) => { - css += formatAlias(alias) -}) -css += '}' - -// DARK THEME MEDIA QUERY -css += '@media (prefers-color-scheme: dark) { body {' -css += generateVariablesAliases(true) -css += '}}' - -// DARK THEME -css += '[data-themes*=light] {' -css += generateVariablesAliases() -css += '}' - -// DARK THEME -css += '[data-themes*=dark] {' -css += generateVariablesAliases(true) -css += '}' - -// WRITE CSS -writeFileSync(join(import.meta.dirname, '../dist', 'icons.css'), compileString(css).css) --- a/build/image-optimization.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# -# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -set -e - -OPTIPNG=$(which optipng) -if ! [ -x "$OPTIPNG" ]; then - echo -e "\033[0;31moptipng executable not found, please install\033[0m" >&2 - exit 1 -fi -JPEGOPTIM=$(which jpegoptim) -if ! [ -x "$JPEGOPTIM" ]; then - echo -e "\033[0;31mjpegoptim executable not found, please install\033[0m" >&2 - exit 2 -fi -SCOUR=$(which scour) -if ! [ -x "$SCOUR" ]; then - echo -e "\033[0;31mscour executable not found, please install\033[0m" >&2 - exit 3 -fi - -REQUIRED_SCOUR_VERSION="0.38.2" -SCOUR_VERSION=$(scour --version) -if dpkg --compare-versions $SCOUR_VERSION lt $REQUIRED_SCOUR_VERSION; then - echo "scour version $REQUIRED_SCOUR_VERSION or higher is required, found $SCOUR_VERSION" >&2 - exit 3 -fi - -set +e - -CHECK_DIR='../' -if [[ -d "$1" ]]; then - CHECK_DIR=$1 -fi - -function recursive_optimize_images() { - cd "$1" || return - DIR_NAME=${PWD##*/} - - if [[ "$DIR_NAME" == "3rdparty" ]]; then - echo -e "\033[0;36mIgnoring 3rdparty for image optimization\033[0m" - return - elif [[ "$DIR_NAME" == "build" ]]; then - echo -e "\033[0;36mIgnoring build for image optimization\033[0m" - return - elif [[ "$DIR_NAME" == "node_modules" ]]; then - echo -e "\033[0;36mIgnoring node_modules for image optimization\033[0m" - return - elif [[ "$DIR_NAME" == "tests" ]]; then - echo -e "\033[0;36mIgnoring tests for image optimization\033[0m" - return - elif [[ "$DIR_NAME" == "vendor" ]]; then - echo -e "\033[0;36mIgnoring vendor for image optimization\033[0m" - return - elif [[ "$DIR_NAME" == "vendor-bin" ]]; then - echo -e "\033[0;36mIgnoring vendor-bin for image optimization\033[0m" - return - fi - - # Optimize all PNGs - for png in *.png - do - [[ -e "$png" ]] || break - - $OPTIPNG -o6 -strip all "$png" - done - - # Optimize all JPGs - for jpg in *.jpg - do - [[ -e "$jpg" ]] || break - - $JPEGOPTIM --strip-all "$jpg" - done - - # Optimize all SVGs - for svg in *.svg - do - [[ -e "$svg" ]] || break - - if [[ "$svg" == "default-source.svg" ]]; then - echo -e "\033[0;36mIgnoring $svg image optimization\033[0m" - continue - fi - - mv $svg $svg.opttmp - $SCOUR --create-groups \ - --enable-id-stripping \ - --enable-comment-stripping \ - --shorten-ids \ - --remove-metadata \ - --strip-xml-prolog \ - --no-line-breaks \ - -i $svg.opttmp \ - -o $svg - rm $svg.opttmp - done - - # Check all subfolders - for dir in */ - do - [[ -e "$dir" ]] || break - - if [[ -d "$dir" ]]; then - if git check-ignore $dir -q ; then - echo -e "\033[0;36m$dir is not shipped. Ignoring image optimization\033[0m" - continue - fi - - recursive_optimize_images "$dir" - cd .. - fi - done -} - -recursive_optimize_images "$CHECK_DIR" --- a/build/integration/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2015 ownCloud, Inc. -# SPDX-License-Identifier: AGPL-3.0-only -vendor -output --- a/build/integration/capabilities_features/capabilities.feature +++ /dev/null @@ -1,249 +0,0 @@ -# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc. -# SPDX-License-Identifier: AGPL-3.0-only -Feature: capabilities - Background: - Given using api version "1" - - Scenario: getting capabilities with admin user - Given As an "admin" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing public upload - Given As an "admin" - And parameter "shareapi_allow_public_upload" of app "core" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | EMPTY | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Disabling share api - Given As an "admin" - And parameter "shareapi_enabled" of app "core" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | EMPTY | - | files_sharing | public@@@enabled | EMPTY | - | files_sharing | public@@@upload | EMPTY | - | files_sharing | resharing | EMPTY | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Disabling public links - Given As an "admin" - And parameter "shareapi_allow_links" of app "core" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | EMPTY | - | files_sharing | public@@@upload | EMPTY | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing resharing - Given As an "admin" - And parameter "shareapi_allow_resharing" of app "core" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | resharing | EMPTY | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing federation outgoing - Given As an "admin" - And parameter "outgoing_server2server_share_enabled" of app "files_sharing" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | EMPTY | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing federation incoming - Given As an "admin" - And parameter "incoming_server2server_share_enabled" of app "files_sharing" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | EMPTY | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing password enforce - Given As an "admin" - And parameter "shareapi_enforce_links_password" of app "core" is set to "yes" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | public@@@password@@@enforced | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing public notifications - Given As an "admin" - And parameter "shareapi_allow_public_notification" of app "core" is set to "yes" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | public@@@send_mail | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing expire date - Given As an "admin" - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | public@@@expire_date@@@enabled | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing expire date enforcing - Given As an "admin" - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" - And parameter "shareapi_enforce_expire_date" of app "core" is set to "yes" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | public@@@expire_date@@@enabled | 1 | - | files_sharing | public@@@expire_date@@@enforced | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | 1 | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | - - Scenario: Changing group sharing allowed - Given As an "admin" - And parameter "shareapi_allow_group_sharing" of app "core" is set to "no" - When sending "GET" to "/cloud/capabilities" - Then the HTTP status code should be "200" - And fields of capabilities match with - | capability | path_to_element | value | - | core | pollinterval | 60 | - | core | webdav-root | remote.php/webdav | - | files_sharing | api_enabled | 1 | - | files_sharing | public@@@enabled | 1 | - | files_sharing | public@@@upload | 1 | - | files_sharing | resharing | 1 | - | files_sharing | federation@@@outgoing | 1 | - | files_sharing | federation@@@incoming | 1 | - | files_sharing | group_sharing | EMPTY | - | files | bigfilechunking | 1 | - | files | undelete | 1 | - | files | versioning | 1 | --- a/build/integration/collaboration_features/autocomplete.feature +++ /dev/null @@ -1,328 +0,0 @@ -# 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 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 - When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no" - Then get autocomplete for "auto" - | id | source | - | auto | users | - | autocomplete | users | - | autocomplete2 | users | - Then get autocomplete for "example" - | id | source | - | autocomplete | users | - Then get autocomplete for "autocomplete@example.com" - | id | source | - | autocomplete | users | - When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "yes" - Then get autocomplete for "auto" - | id | source | - | auto | users | - | autocomplete | users | - | autocomplete2 | users | - Then get autocomplete for "example" - | id | source | - | autocomplete | users | - Then get autocomplete for "autocomplete@example.com" - | id | source | - | autocomplete | users | - - Scenario: getting autocomplete 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 autocomplete for "auto" - | id | source | - Then get autocomplete for "example" - | id | source | - Then get autocomplete for "autocomplete@example.com" - | id | source | - When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "yes" - Then get autocomplete for "auto" - | id | source | - | auto | users | - Then get autocomplete for "example" - | id | source | - Then get autocomplete for "autocomplete@example.com" - | id | source | - | autocomplete | users | - - 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 - When parameter "shareapi_restrict_user_enumeration_full_match" of app "core" is set to "no" - Then get email autocomplete for "auto" - | id | source | - Then get email autocomplete for "example" - | id | source | - | leon@example.com | emails | - | user@example.com | emails | - Then get email autocomplete for "autocomplete@example.com" - | id | source | - | autocomplete@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 | - | leon@example.com | emails | - | user@example.com | emails | - Then get email autocomplete for "autocomplete@example.com" - | id | source | - - 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 "auto" - | id | source | - Then get email autocomplete for "example" - | id | source | - | leon@example.com | emails | - | user@example.com | emails | - Then get email autocomplete for "autocomplete@example.com" - | id | source | - | autocomplete@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 | - | leon@example.com | emails | - | user@example.com | emails | - Then get email autocomplete for "autocomplete@example.com" - | id | source | - - 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 | --- a/build/integration/collaboration_features/user_status.feature +++ /dev/null @@ -1,22 +0,0 @@ -# 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" --- a/build/integration/comments_features/comments.feature +++ /dev/null @@ -1,217 +0,0 @@ -# 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 - Given As an "user0" - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - When "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user0" - And the response should contain only "1" comments - - Scenario: Creating a comment on a shared file belonging to another user - Given user "user0" exists - Given user "12345" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | 12345 | - | shareType | 0 | - Given user "12345" accepts last share - When "12345" posts a comment with content "A comment from another user" on the file named "/myFileToComment.txt" it should return "201" - Then As "12345" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "A comment from another user" - And the response should contain a property "oc:actorDisplayName" with value "12345" - And the response should contain only "1" comments - - Scenario: Creating a comment on a non-shared file belonging to another user - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Then "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "404" - - Scenario: Reading comments on a non-shared file belonging to another user - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Then As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "404" - - Scenario: Deleting my own comments on a file belonging to myself - Given user "user0" exists - Given As an "user0" - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - Then the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user0" - And the response should contain only "1" comments - And As "user0" delete the created comment it should return "204" - And As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain only "0" comments - - Scenario: Deleting my own comments on a file shared by somebody else - Given user "user0" exists - Given user "user1" exists - Given As an "user0" - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | user1 | - | shareType | 0 | - And user "user1" accepts last share - Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - And the response should contain only "1" comments - And As "user1" delete the created comment it should return "204" - And As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain only "0" comments - - Scenario: Deleting my own comments on a file unshared by someone else - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | user1 | - | shareType | 0 | - And user "user1" accepts last share - Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - And the response should contain only "1" comments - And As "user0" remove all shares from the file named "/myFileToComment.txt" - And As "user1" delete the created comment it should return "404" - And As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "404" - - Scenario: Edit my own comments on a file belonging to myself - Given user "user0" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user0" - And the response should contain only "1" comments - When As "user0" edit the last created comment and set text to "My edited comment" it should return "207" - Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My edited comment" - And the response should contain a property "oc:actorDisplayName" with value "user0" - - Scenario: Edit my own comments on a file shared by someone with me - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | user1 | - | shareType | 0 | - And user "user1" accepts last share - Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - And the response should contain only "1" comments - Given As "user1" edit the last created comment and set text to "My edited comment" it should return "207" - Then As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My edited comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - - Scenario: Edit my own comments on a file unshared by someone with me - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | user1 | - | shareType | 0 | - And user "user1" accepts last share - When "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - And the response should contain only "1" comments - And As "user0" remove all shares from the file named "/myFileToComment.txt" - When As "user1" edit the last created comment and set text to "My edited comment" it should return "404" - Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - And the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - - Scenario: Edit comments of other users should not be possible - Given user "user0" exists - Given user "user1" exists - Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" - Given as "user0" creating a share with - | path | myFileToComment.txt | - | shareWith | user1 | - | shareType | 0 | - And user "user1" accepts last share - Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" - When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" - Then the response should contain a property "oc:parentId" with value "0" - And the response should contain a property "oc:childrenCount" with value "0" - And the response should contain a property "oc:verb" with value "comment" - And the response should contain a property "oc:actorType" with value "users" - And the response should contain a property "oc:objectType" with value "files" - And the response should contain a property "oc:message" with value "My first comment" - And the response should contain a property "oc:actorDisplayName" with value "user1" - And the response should contain only "1" comments - Then As "user0" edit the last created comment and set text to "My edited comment" it should return "403" --- a/build/integration/config/behat.yml +++ /dev/null @@ -1,299 +0,0 @@ -# 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/ - 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: ../../ - 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 - encryption: - paths: - - "%paths.base%/../encryption_features" - contexts: - - FeatureContext: - baseUrl: http://localhost:8080/ocs/ - admin: - - admin - - admin - regular_user_password: 123456 - - CommandLineContext: - baseUrl: http://localhost:8080 - ocPath: ../../ - federation: - paths: - - "%paths.base%/../federation_features" - contexts: - - FederationContext: - 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 - regular_user_password: 123456 - files_reminders: - paths: - - "%paths.base%/../files_reminders" - contexts: - - FilesRemindersContext: - baseUrl: http://localhost:8080 - admin: - - admin - - admin - regular_user_password: 123456 - capabilities: - paths: - - "%paths.base%/../capabilities_features" - contexts: - - CapabilitiesContext: - 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 - regular_user_password: 123456 - sharees: - paths: - - "%paths.base%/../sharees_features" - contexts: - - ShareesContext: - baseUrl: http://localhost:8080/ocs/ - admin: - - admin - - admin - regular_user_password: 123456 - sharing: - paths: - - "%paths.base%/../sharing_features" - contexts: - - SharingContext: - 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 - regular_user_password: 123456 - - TalkContext - guests: - paths: - - "%paths.base%/../guests_features" - contexts: - - GuestsContext - - SharingContext: - baseUrl: http://localhost:8080/ocs/ - admin: - - admin - - admin - regular_user_password: 123456 - setup: - paths: - - "%paths.base%/../setup_features" - contexts: - - SetupContext: - baseUrl: http://localhost:8080/ocs/ - admin: - - admin - - admin - regular_user_password: 123456 - filesdrop: - paths: - - "%paths.base%/../filesdrop_features" - contexts: - - FilesDropContext: - baseUrl: http://localhost:8080 - admin: - - admin - - admin - regular_user_password: 123456 - ldap: - paths: - - "%paths.base%/../ldap_features" - contexts: - - LDAPContext: - baseUrl: http://localhost:8080 - admin: - - 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 - 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 --- a/build/integration/data/bjoern.vcf +++ /dev/null @@ -1,51 +0,0 @@ -BEGIN:VCARD -VERSION:3.0 -FN:Björn Schießle -ORG:Nextcloud -PHOTO;ENCODING=b;TYPE=jpeg:/9j/4AAQSkZJRgABAQEAbABrAAD//gA7Q1JFQVRPUjogZ2Qt - anBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gOTAK/9sAQwAIBgYHBg - UIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04Mjwu - MzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMj - IyMjIyMjIyMjIyMjIyMjIy/8AAEQgAUABQAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAA - AAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZ - GhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm - Z2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIyc - rS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgME - BQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQ - kjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpz - dHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1N - XW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8/zqMcG0GTZKh3R/wouMYxVG - SFVtyqoDjocVMuovJZym3kYLtLAlumB1Ofc1FYmR9KaWVtzAYH8q54bmEE7mLHay3d2IYUyzcV - 1Vn4VsLaITX8hkkI4j3YFaHg7QJryaOGKNRLcfM0h6ovrXsVh8MNKkt1893Y+vrWt9bHVGGl2e - KyW+mBW8i3iVV43beW9ce1YrJapcPKW2xzfLIGbqM9iORX0PcfC3Qw27Yx+hrkvEvwjs5LR5tJ - kMdzHllRuje1JtbMv2V1dHjmpwzXMKTRxRsOg29UwOme4rFwVToG9T6V0c9pLFC58tYbmBsMnf - IrBlG+X5djscE+WtEVbQwaa3IBnAwcc1u27wuq2lum8qCWlZtoJ9cVSj0+ZnQNBMwI3EIOn1Pa - rOoyvCyJFxEy8MowGHTj24pNpuxMlc2g+m6fNPC6b125V22jCk9PSpAy3NmzIyvu4BQcYrK1Zb - VdPhFrIZJFXdKWGDtbp296v28BstIRN2WTk4P51mtNTOLtqenfCVIpdNldV+dcKSa9bjkk2gYA - ArxHwHqb6F4dvLxVX/AI+NoON38I7D6102geM9R1XX47KRColbCkxFMVfU9SCvFI9MMjkc1VmA - IJrlfE2saro8gW3JbJ+XZHvNLoXiWbVFWK4EvmMM4kh8v8qTdy1C2x494/EcOpXoVdpaQ4I9a8 - /tof8ASTK0yxgYbAOD+Ar0v4laVcSeMXjgT91JGJF+vQiuDisL8k3MaFio2xnPaqv7pxVtGW7i - Vp7JpfswRWK5meQrnn+L1rLvHWSaEqMKiBflk3g/T0+lat3cTajpZtQYC4PGRtYgdK52EORsQE - sfQVFNdzCJ1Uq239non2iMXDgONoyce/pRbxj7BLHtJYkgDOcc+tVLyw8iczB1NqZNm9Dyvvj0 - rqLJbb7Kl2nmBA3BK5ZuOv1qG7IzvY9H+HdlGdGmgvraOFcqNobOdqKu78cZrp7CLRbXV1EDIj - Rn5nd8nntzXm3hCS8fSLoeYU3P8meDnHzVetorW5ZtPmW4kvifNGyIk46ZBqou562HalSWp6Pq - TaezkXRR4nfAbPQ/0qeG0tIYA0JDAdOc4rlkitLDSJBd210Q6hWk8g5bPTjrT/Dy3duZre4ZzE - rfu9/3sehps6ElbczfFNgYrPVNflCsba2Kwp1y5yF/8eINeKQMywmGKaIAg7kL7Sp716l8S9Vv - oymnW0mIZIxI6kcMdxx+RFeUanpUemQvPcJMwuBw6YI3deopK2zPMxc1KfKZhuy9wpVAQrDCgd - arqnkuNodXHO4fw0y3DB96KxwwIxV2PzpZd8xMaE46YBrbYx2NGeB70CVLkOjbVwF2/UfhWzp8 - E8drAv2pZEjYou3GV+ase2uRaW8un3EyoCWfenU/7OfwrR0W+e4gSKOIEoeitkjPc/rWEk7GT2 - N3S/EEtrqjwSTKsAYsBj7zEev4GvR7e+kl06LU9NgiutoKlD94fSvHP7NZ5BqN1EY4I5funjzD - /dH9a63R5NSl8OC+sLnZMsrJOjfdkOchvyIFbRouMed9TrwlTlbS2PTdHvL3UoRcX0EdtHH91e - mKFvIp2LW5DBjwR/OvPbO61zVP9Gnm2wE/MIyfmH17Vs3F0bRfscLD5Y8yFeijstVTpOrUUFuz - rqV7RcuiJfFmnwan5j2xL3MNt5UZzwfm3Ej+VeZvI8ZWO6QvFjLAmvQIrtnkCocv3PpVPU9Btd - ZhdXlkhlUYDof6V6uKynmjH2W6/E8GVZzk3I4O5tUtwxWKNV6fKB8wxu4H0qq6w+YeVKgDb2PT - of8APatnWdGvdI08W8lsJ7QNv89QcD6+lZMhIsx5ezGMFum7HTFeLKnOk7SVmaRfUu+E/C9rqs - 0816kjBMc545rvLLS9O0yEpZ2kajplhktVfQrcWOiQ245cqGc+rMcmtnyfujHAr6TB4CnCKlNX - b/AwqVHJnK3epfbPPh1e3W1to5/IinBzuOT2/rXTeCrWw/sK7h+2wuPNaUgcFUwMHB+lU9UsIb - tkMwysfIUetUrfw9uguYrW6MCgbwjZO4/3R6VeJwvtY2ZpRr+zd0Xr3xB4d061doLx55scKqEf - zxWRBqTatp4ZImg85zuJbJK9v60xPCyNLvmG7HQHnPua2rbTkhCqFGarDYKNGXMh1sTKorMW2i - SCERxjFWIl2scd6kWLHT86MDn+6K7zmLUL/uyjqHVht2tzn2rmfEHw8stShMmmsLGflti58pj9 - P4fqPyrbhkLkv0UcCr8MxZCDXNXoRqL3kOM3F6H/2Q== -UID:6454bec7-6f5b-46f2-ba22-15537ab215d9 -CATEGORIES:Engineering -END:VCARD \ No newline at end of file --- a/build/integration/data/bjoern.vcf.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/build/integration/data/clouds.jpg.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2019 CHUTTERSNAP " -SPDX-License-Identifier: LicenseRef-Unsplash --- a/build/integration/data/coloured-pattern-non-square.png.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/build/integration/data/green-square-256.png.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/build/integration/data/textfile.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a testfile. - -Cheers. \ No newline at end of file --- a/build/integration/data/textfile.txt.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only \ No newline at end of file --- a/build/integration/dav_features/caldav-delegation.feature +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: calendar delegation - Calendar delegation grants another user/principal control of a calendar account, - including all calendars the delegator can access. - - @caldav-delegation - Scenario: admin grants user0 read access to her calendar account - Given user "admin" exists - And user "user0" exists - When "admin" updates property "{DAV:}group-member-set" to href "/remote.php/dav/principals/users/user0" of principal "users/admin/calendar-proxy-read" on the endpoint "/remote.php/dav/principals/" - Then The CalDAV response should be multi status - And The CalDAV response should contain an href "/remote.php/dav/principals/users/admin/calendar-proxy-read" - And The CalDAV response should contain a property "{DAV:}group-member-set" - - @caldav-delegation - Scenario: admin grants write access to her calendar account - Given user "admin" exists - And user "user0" exists - When "admin" updates property "{DAV:}group-member-set" to href "/remote.php/dav/principals/users/user0" of principal "users/admin/calendar-proxy-write" on the endpoint "/remote.php/dav/principals/" - Then The CalDAV response should be multi status - And The CalDAV response should contain an href "/remote.php/dav/principals/users/admin/calendar-proxy-write" - And The CalDAV response should contain a property "{DAV:}group-member-set" - - Scenario: Admin cannot grant User1 access to User0's calendar account - Given user "admin" exists - And user "user0" exists - And user "user1" exists - When "admin" updates property "{DAV:}group-member-set" to href "/remote.php/dav/principals/users/user1" of principal "users/user0/calendar-proxy-write" on the endpoint "/remote.php/dav/principals/" - Then The CalDAV HTTP status code should be "404" \ No newline at end of file --- a/build/integration/dav_features/caldav.feature +++ /dev/null @@ -1,94 +0,0 @@ -# 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 - When "admin" requests calendar "user0/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" - - Scenario: Accessing a not shared calendar of another user - Given user "user0" exists - Given "admin" creates a calendar named "MyCalendar" - Given The CalDAV HTTP status code should be "201" - 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 "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 - Given "admin" creates a calendar named "MyCalendar" - Given The CalDAV HTTP status code should be "201" - 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 "Calendar with name 'MyCalendar' could not be found" - - Scenario: Accessing a not existing calendar of another user - Given user "user0" exists - 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" - - Scenario: Accessing a not existing calendar of another user via the legacy endpoint - Given user "user0" exists - 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" - - Scenario: Accessing a not existing calendar of myself - Given user "user0" exists - 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" - - Scenario: Creating a new calendar - When "admin" creates a calendar named "MyCalendar" - Then The CalDAV HTTP status code should be "201" - And "admin" requests calendar "admin/MyCalendar" on the endpoint "/remote.php/dav/calendars/" - Then The CalDAV HTTP status code should be "207" - - Scenario: Propfind on public calendar endpoint without calendars - When "admin" creates a calendar named "MyCalendar" - Then The CalDAV HTTP status code should be "201" - And "admin" publicly shares the calendar named "MyCalendar" - 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 - - 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" --- a/build/integration/dav_features/carddav.feature +++ /dev/null @@ -1,83 +0,0 @@ -# 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 - When "admin" requests addressbook "user0/MyAddressbook" with statuscode "404" on the endpoint "/remote.php/dav/addressbooks/users/" - And The CardDAV exception is "Sabre\DAV\Exception\NotFound" - And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" - - Scenario: Accessing a not shared addressbook of another user - Given user "user0" exists - Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" - When "user0" requests addressbook "admin/MyAddressbook" with statuscode "404" on the endpoint "/remote.php/dav/addressbooks/users/" - And The CardDAV exception is "Sabre\DAV\Exception\NotFound" - And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" - - Scenario: Accessing a not existing addressbook of another user via legacy endpoint - Given user "user0" exists - When "admin" requests addressbook "user0/MyAddressbook" with statuscode "404" on the endpoint "/remote.php/carddav/addressbooks/" - And The CardDAV exception is "Sabre\DAV\Exception\NotFound" - And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" - - Scenario: Accessing a not shared addressbook of another user via legacy endpoint - Given user "user0" exists - Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" - When "user0" requests addressbook "admin/MyAddressbook" with statuscode "404" on the endpoint "/remote.php/carddav/addressbooks/" - And The CardDAV exception is "Sabre\DAV\Exception\NotFound" - And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" - - Scenario: Accessing a not existing addressbook of myself - Given user "user0" exists - When "user0" requests addressbook "admin/MyAddressbook" with statuscode "404" on the endpoint "/remote.php/dav/addressbooks/users/" - And The CardDAV exception is "Sabre\DAV\Exception\NotFound" - And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" - - Scenario: Creating a new addressbook - When "admin" creates an addressbook named "MyAddressbook" with statuscode "201" - Then "admin" requests addressbook "admin/MyAddressbook" with statuscode "207" on the endpoint "/remote.php/dav/addressbooks/users/" - - Scenario: Accessing ones own contact - Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" - Given "admin" uploads the contact "bjoern.vcf" to the addressbook "MyAddressbook" - When Downloading the contact "bjoern.vcf" from addressbook "MyAddressbook" as user "admin" - Then The following HTTP headers should be set - |Content-Disposition|attachment; filename*=UTF-8''bjoern.vcf; filename="bjoern.vcf"| - |Content-Type|text/vcard; charset=utf-8| - |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| - - Scenario: Exporting the picture of ones own contact - Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" - Given "admin" uploads the contact "bjoern.vcf" to the addressbook "MyAddressbook" - When Exporting the picture of contact "bjoern.vcf" from addressbook "MyAddressbook" as user "admin" - Then The following HTTP headers should be set - |Content-Disposition|attachment; filename=bjoern.vcf.jpg| - |Content-Type|image/jpeg| - |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| - - 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/" --- a/build/integration/dav_features/dav-v2-public.feature +++ /dev/null @@ -1,142 +0,0 @@ -# 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: Finalizing a public chunked upload with COPY is not allowed - Given using new dav path - And user "user0" exists - And As an "user0" - And user "user0" created a folder "/public-upload" - And User "user0" uploads file with content "original content" to "/public-upload/target.txt" - And as "user0" creating a share with - | path | public-upload | - | shareType | 3 | - | publicUpload | true | - And creating a new public chunking upload with id "chunking-public-copy" - And uploading new public chunk file "1" with "AAAAA" to id "chunking-public-copy" - When copying new public chunk file with id "chunking-public-copy" to "/target.txt" - Then the HTTP status code should be "405" - And Downloading file "/public-upload/target.txt" as "user0" - Then Downloaded content should be "original content" - - Scenario: Finalizing a public chunked upload with MOVE overwrites the target - Given using new dav path - And user "user0" exists - And As an "user0" - And user "user0" created a folder "/public-upload" - And User "user0" uploads file with content "original content" to "/public-upload/target.txt" - And as "user0" creating a share with - | path | public-upload | - | shareType | 3 | - | publicUpload | true | - And creating a new public chunking upload with id "chunking-public-move" - And uploading new public chunk file "1" with "AAAAA" to id "chunking-public-move" - When moving new public chunk file with id "chunking-public-move" to "/target.txt" - Then the HTTP status code should be "204" - And Downloading file "/public-upload/target.txt" as "user0" - Then Downloaded content should be "AAAAA" - - 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 - - # After the link share was created the initiator gets a second, read-only path to the - # shared folder. Nodes are ordered by path descending, so "/z-child" is found before - # "/parent/z-child" and the shareable path has to be picked explicitly. - Scenario: Downloading a file from a public share of a folder the initiator can also reach without share permission - Given using new dav path - And As an "admin" - And user "user0" exists - And user "user1" exists - And As an "user0" - And user "user0" created a folder "/parent" - And user "user0" created a folder "/parent/z-child" - And User "user0" uploads file with content "shared content" to "/parent/z-child/text.txt" - And folder "/parent" of user "user0" is shared with user "user1" with permissions 31 - And user "user1" accepts last share - And as "user1" creating a share with - | path | parent/z-child | - | shareType | 3 | - | permissions | 1 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And save the last share data as "publicLink" - And folder "/parent/z-child" of user "user0" is shared with user "user1" with permissions 1 - And user "user1" accepts last share - And restore the last share data from "publicLink" - And As an "user0" - Given using new public dav path - When Downloading public file "/text.txt" - Then the HTTP status code should be "200" - And Downloaded content should be "shared content" --- a/build/integration/dav_features/dav-v2.feature +++ /dev/null @@ -1,162 +0,0 @@ -# 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" --- a/build/integration/dav_features/file-versions.feature +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -Feature: file-versions - Background: - Given using new dav path - - # Regression test for file version downloads returning "404 File not found: versions in 'root'". - # The versions/trash bin collections are attached to the DAV root lazily during beforeMethod, - # so an early GET handler that resolved the request path too eagerly aborted the request. - # This exercises the full plugin stack: a real previous version must be downloadable via DAV. - Scenario: Download a previous version of a file via the versions DAV endpoint - Given user "admin" uploads file with content "first version" and mtime "1111111111" to "/versioned.txt" - And user "admin" uploads file with content "second version" and mtime "2222222222" to "/versioned.txt" - When user "admin" downloads version "1111111111" of file "/versioned.txt" - Then the HTTP status code should be "200" - And Downloaded content should be "first version" --- a/build/integration/dav_features/principal-property-search.feature +++ /dev/null @@ -1,13 +0,0 @@ -# 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 "/remote.php/dav/principals/users/user0/" --- a/build/integration/dav_features/webdav-related.feature +++ /dev/null @@ -1,767 +0,0 @@ -# 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" - - Scenario: Unauthenticated call old dav path - Given using old dav path - When connecting to dav endpoint - Then the HTTP status code should be "401" - And there are no duplicate headers - And The following headers should be set - |WWW-Authenticate|Basic realm="Nextcloud", charset="UTF-8"| - - Scenario: Unauthenticated call new dav path - Given using new dav path - When connecting to dav endpoint - Then the HTTP status code should be "401" - And there are no duplicate headers - And The following headers should be set - |WWW-Authenticate|Basic realm="Nextcloud", charset="UTF-8"| - - Scenario: Moving a file - Given using old dav path - And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" moves file "/welcome.txt" to "/FOLDER/welcome.txt" - Then the HTTP status code should be "201" - And Downloaded content when downloading file "/FOLDER/welcome.txt" with range "bytes=0-6" should be "Welcome" - - Scenario: Moving and overwriting a file old way - Given using old dav path - And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" moves file "/welcome.txt" to "/textfile0.txt" - 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" - 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 | 0 | - | permissions | 1 | - | shareWith | user0 | - And user "user0" accepts last share - And As an "user0" - 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" - - Scenario: Moving a file to overwrite a file in a folder with no permissions - Given using old 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 | 0 | - | permissions | 1 | - | shareWith | user0 | - And user "user0" accepts last share - And User "user1" copies file "/welcome.txt" to "/testshare/overwritethis.txt" - And As an "user0" - When User "user0" moves file "/textfile0.txt" to "/testshare/overwritethis.txt" - Then the HTTP status code should be "403" - And Downloaded content when downloading file "/testshare/overwritethis.txt" with range "bytes=0-6" should be "Welcome" - - Scenario: Copying a file - Given using old dav path - And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" copies file "/welcome.txt" to "/FOLDER/welcome.txt" - Then the HTTP status code should be "201" - And Downloaded content when downloading file "/FOLDER/welcome.txt" with range "bytes=0-6" should be "Welcome" - - Scenario: Copying and overwriting a file - Given using old dav path - And As an "admin" - And user "user0" exists - And As an "user0" - When User "user0" copies file "/welcome.txt" to "/textfile1.txt" - Then the HTTP status code should be "204" - And Downloaded content when downloading file "/textfile1.txt" with range "bytes=0-6" should be "Welcome" - - Scenario: Copying a file to a folder with no permissions - Given using old 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 | 0 | - | permissions | 1 | - | shareWith | user0 | - And user "user0" accepts last share - And As an "user0" - When User "user0" copies file "/textfile0.txt" to "/testshare/textfile0.txt" - Then the HTTP status code should be "403" - And Downloading file "/testshare/textfile0.txt" - And the HTTP status code should be "404" - - Scenario: Copying a file to overwrite a file into a folder with no permissions - Given using old 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 | 0 | - | permissions | 1 | - | shareWith | user0 | - And user "user0" accepts last share - And User "user1" copies file "/welcome.txt" to "/testshare/overwritethis.txt" - And As an "user0" - When User "user0" copies file "/textfile0.txt" to "/testshare/overwritethis.txt" - Then the HTTP status code should be "403" - And Downloaded content when downloading file "/testshare/overwritethis.txt" with range "bytes=0-6" should be "Welcome" - - Scenario: download a file with range - Given using old dav path - And As an "admin" - When Downloading file "/welcome.txt" with range "bytes=52-78" - Then Downloaded content should be "example file for developers" - - Scenario: Upload forbidden if quota is 0 - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user0" has a quota of "0" - When User "user0" uploads file "data/textfile.txt" to "/asdf.txt" - Then the HTTP status code should be "507" - - Scenario: Retrieving folder quota when no quota is set - Given using old dav path - And As an "admin" - And user "user0" exists - When user "user0" has unlimited quota - Then as "user0" gets properties of folder "/" with - |{DAV:}quota-available-bytes| - And the single response should contain a property "{DAV:}quota-available-bytes" with value "-3" - - Scenario: Retrieving folder quota when quota is set - Given using old dav path - And As an "admin" - And user "user0" exists - When user "user0" has a quota of "10 MB" - Then as "user0" gets properties of folder "/" with - |{DAV:}quota-available-bytes| - And the single response should contain a property "{DAV:}quota-available-bytes" with value "10485421" - - Scenario: Retrieving folder quota of shared folder with quota when no quota is set for recipient - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" has unlimited quota - 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 - Then as "user0" gets properties of folder "/testquota" with - |{DAV:}quota-available-bytes| - And the single response should contain a property "{DAV:}quota-available-bytes" with value "10485421" - - Scenario: Uploading a file as recipient using webdav having quota - Given using old 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: Retrieving folder quota when quota is set and a file was uploaded - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user0" has a quota of "1 KB" - And user "user0" adds a file of 93 bytes to "/prueba.txt" - When as "user0" gets properties of folder "/" with - |{DAV:}quota-available-bytes| - Then the single response should contain a property "{DAV:}quota-available-bytes" with value "592" - - Scenario: Retrieving folder quota when quota is set and a file was received - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user1" exists - And user "user1" has a quota of "1 KB" - And user "user0" adds a file of 93 bytes to "/user0.txt" - And file "user0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When as "user1" gets properties of folder "/" with - |{DAV:}quota-available-bytes| - Then the single response should contain a property "{DAV:}quota-available-bytes" with value "685" - - Scenario: download a public shared file with range - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Downloading last public shared file with range "bytes=52-78" - Then Downloaded content should be "example file for developers" - - Scenario: download a public shared file inside a folder with range - Given user "user0" exists - And As an "user0" - When creating a share with - | path | PARENT | - | shareType | 3 | - And Downloading last public shared file inside a folder "/parent.txt" with range "bytes=1-8" - Then Downloaded content should be "extcloud" - - Scenario: Downloading a file on the old endpoint should serve security headers - Given using old 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 old backend - Given Logging in using web as "admin" - When Sending a "GET" to "/remote.php/webdav/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 old backend - Given Logging in using web as "admin" - When Sending a "GET" to "/remote.php/webdav/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 old backend - Given Logging in using web as "admin" - When Sending a "PROPFIND" to "/remote.php/webdav/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 old backend - Given Logging in using web as "admin" - When Sending a "PROPFIND" to "/remote.php/webdav/welcome.txt" with requesttoken - Then the HTTP status code should be "207" - - 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" - When as "user0" gets properties of folder "/test" with - |{http://owncloud.org/ns}share-types| - Then the response should contain an empty property "{http://owncloud.org/ns}share-types" - - Scenario: A file that is shared to a user has a share-types property - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/test" - And as "user0" creating a share with - | path | test | - | shareType | 0 | - | permissions | 31 | - | shareWith | user1 | - When as "user0" gets properties of folder "/test" with - |{http://owncloud.org/ns}share-types| - Then the response should contain a share-types property with - | 0 | - - Scenario: A file that is shared to a group has a share-types property - Given user "user0" exists - And group "group1" exists - And user "user0" created a folder "/test" - And as "user0" creating a share with - | path | test | - | shareType | 1 | - | permissions | 31 | - | shareWith | group1 | - When as "user0" gets properties of folder "/test" with - |{http://owncloud.org/ns}share-types| - Then the response should contain a share-types property with - | 1 | - - Scenario: A file that is shared by link has a share-types property - Given user "user0" exists - And user "user0" created a folder "/test" - And as "user0" creating a share with - | path | test | - | shareType | 3 | - | permissions | 31 | - When as "user0" gets properties of folder "/test" with - |{http://owncloud.org/ns}share-types| - Then the response should contain a share-types property with - | 3 | - - Scenario: A file that is shared by user,group and link has a share-types property - Given user "user0" exists - And user "user1" exists - And group "group2" exists - And user "user0" created a folder "/test" - And as "user0" creating a share with - | path | test | - | shareType | 0 | - | permissions | 31 | - | shareWith | user1 | - And as "user0" creating a share with - | path | test | - | shareType | 1 | - | permissions | 31 | - | shareWith | group2 | - And as "user0" creating a share with - | path | test | - | shareType | 3 | - | permissions | 31 | - When as "user0" gets properties of folder "/test" with - |{http://owncloud.org/ns}share-types| - Then the response should contain a share-types property with - | 0 | - | 1 | - | 3 | - - Scenario: Upload chunked file asc with new chunking - Given using new dav path - And user "user0" exists - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42" - And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt" - When As an "user0" - And Downloading file "/myChunkedFile.txt" - Then Downloaded content should be "AAAAABBBBBCCCCC" - - Scenario: Upload chunked file desc with new chunking - Given using new dav path - And user "user0" exists - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt" - When As an "user0" - And Downloading file "/myChunkedFile.txt" - Then Downloaded content should be "AAAAABBBBBCCCCC" - - Scenario: Upload chunked file random with new chunking - Given using new dav path - And user "user0" exists - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt" - When As an "user0" - And Downloading file "/myChunkedFile.txt" - Then Downloaded content should be "AAAAABBBBBCCCCC" - - Scenario: Cannot create a chunked upload in another user's uploads folder - Given using new dav path - And user "user0" exists - And user "user1" exists - And As an "user1" - When user "user1" creates a new chunking upload with id "chunking-42" in the uploads folder for "user0" - Then the HTTP status code should be "403" - - Scenario: A disabled user cannot use webdav - Given user "userToBeDisabled" exists - And As an "admin" - And assure user "userToBeDisabled" is disabled - When Downloading file "/welcome.txt" as "userToBeDisabled" - Then the HTTP status code should be "503" - - Scenario: Copying files into a folder with edit permissions - Given using dav path "remote.php/webdav" - And user "user0" exists - And user "user1" exists - And As an "user1" - And user "user1" created a folder "/testcopypermissionsAllowed" - And as "user1" creating a share with - | path | testcopypermissionsAllowed | - | shareType | 0 | - | permissions | 31 | - | shareWith | user0 | - And user "user0" accepts last share - And User "user0" uploads file with content "copytest" to "/copytest.txt" - When User "user0" copies file "/copytest.txt" to "/testcopypermissionsAllowed/copytest.txt" - Then the HTTP status code should be "201" - - Scenario: Copying files into a folder without edit permissions - Given using dav path "remote.php/webdav" - And user "user0" exists - And user "user1" exists - And As an "user1" - And user "user1" created a folder "/testcopypermissionsNotAllowed" - And as "user1" creating a share with - | path | testcopypermissionsNotAllowed | - | shareType | 0 | - | permissions | 1 | - | shareWith | user0 | - And user "user0" accepts last share - And User "user0" uploads file with content "copytest" to "/copytest.txt" - When User "user0" copies file "/copytest.txt" to "/testcopypermissionsNotAllowed/copytest.txt" - Then the HTTP status code should be "403" - - Scenario: Uploading a file as recipient with limited permissions - 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 "/testfolder" - And as "user1" creating a share with - | path | testfolder | - | shareType | 0 | - | permissions | 23 | - | shareWith | user0 | - And user "user0" accepts last share - And As an "user0" - And User "user0" uploads file "data/textfile.txt" to "/testfolder/asdf.txt" - And As an "user1" - When User "user1" deletes file "/testfolder/asdf.txt" - Then the HTTP status code should be "204" - - Scenario: Creating a folder - Given using old dav path - And user "user0" exists - And user "user0" created a folder "/test_folder" - When as "user0" gets properties of folder "/test_folder" with - |{DAV:}resourcetype| - Then the single response should contain a property "{DAV:}resourcetype" with value "{DAV:}collection" - - Scenario: Creating a folder with special chars - Given using old dav path - And user "user0" exists - And user "user0" created a folder "/test_folder:5" - When as "user0" gets properties of folder "/test_folder:5" with - |{DAV:}resourcetype| - Then the single response should contain a property "{DAV:}resourcetype" with value "{DAV:}collection" - - Scenario: Removing everything of a folder - Given using old dav path - And As an "admin" - And user "user0" exists - And As an "user0" - And User "user0" moves file "/welcome.txt" to "/FOLDER/welcome.txt" - And user "user0" created a folder "/FOLDER/SUBFOLDER" - And User "user0" copies file "/textfile0.txt" to "/FOLDER/SUBFOLDER/testfile0.txt" - When User "user0" deletes everything from folder "/FOLDER/" - Then user "user0" should see following elements - | /FOLDER/ | - | /PARENT/ | - | /PARENT/parent.txt | - | /textfile0.txt | - | /textfile1.txt | - | /textfile2.txt | - | /textfile3.txt | - | /textfile4.txt | - - Scenario: Removing everything of a folder using new dav path - Given using new dav path - And As an "admin" - And user "user0" exists - And As an "user0" - And User "user0" moves file "/welcome.txt" to "/FOLDER/welcome.txt" - And user "user0" created a folder "/FOLDER/SUBFOLDER" - And User "user0" copies file "/textfile0.txt" to "/FOLDER/SUBFOLDER/testfile0.txt" - When User "user0" deletes everything from folder "/FOLDER/" - Then user "user0" should see following elements - | /FOLDER/ | - | /PARENT/ | - | /PARENT/parent.txt | - | /textfile0.txt | - | /textfile1.txt | - | /textfile2.txt | - | /textfile3.txt | - | /textfile4.txt | - - Scenario: Checking file id after a move using new endpoint - Given using new dav path - And user "user0" exists - And User "user0" stores id of file "/textfile0.txt" - When User "user0" moves file "/textfile0.txt" to "/FOLDER/textfile0.txt" - Then User "user0" checks id of file "/FOLDER/textfile0.txt" - - Scenario: Checking file id after a move overwrite using new chunking endpoint - Given using new dav path - And user "user0" exists - And User "user0" copies file "/textfile0.txt" to "/existingFile.txt" - And User "user0" stores id of file "/existingFile.txt" - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - 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 "/existingFile.txt" - Then User "user0" checks id of file "/existingFile.txt" - - Scenario: Renaming a folder to a backslash encoded should return an error using old endpoint - Given using old dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/%5C" - Then the HTTP status code should be "400" - - Scenario: Renaming a folder beginning with a backslash encoded should return an error using old endpoint - Given using old dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/%5Ctestshare" - Then the HTTP status code should be "400" - - Scenario: Renaming a folder including a backslash encoded should return an error using old endpoint - Given using old dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/hola%5Chola" - Then the HTTP status code should be "400" - - Scenario: Renaming a folder to a backslash encoded should return an error using new endpoint - Given using new dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/%5C" - Then the HTTP status code should be "400" - - Scenario: Renaming a folder beginning with a backslash encoded should return an error using new endpoint - Given using new dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/%5Ctestshare" - Then the HTTP status code should be "400" - - Scenario: Renaming a folder including a backslash encoded should return an error using new endpoint - Given using new dav path - And user "user0" exists - And user "user0" created a folder "/testshare" - When User "user0" moves folder "/testshare" to "/hola%5Chola" - Then the HTTP status code should be "400" - - Scenario: Upload file via new chunking endpoint with wrong size header - Given using new dav path - And user "user0" exists - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - 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 5 - Then the HTTP status code should be "400" - - Scenario: Upload file via new chunking endpoint with correct size header - Given using new dav path - And user "user0" exists - And user "user0" creates a new chunking upload with id "chunking-42" - And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42" - And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42" - 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 - - @requires-s3 - Scenario: Cannot overwrite a file the user may not update with new chunking v2 - Given using new dav path - And user "user0" exists - And user "user1" exists - And As an "user1" - And user "user1" created a folder "/testshare" - And User "user1" copies file "/welcome.txt" to "/testshare/test.txt" - And as "user1" creating a share with - | path | testshare | - | shareType | 0 | - | permissions | 5 | - | shareWith | user0 | - And user "user0" accepts last share - And As an "user0" - And user "user0" creates a file locally with "3" x 5 MB chunks - When user "user0" creates a new chunking v2 upload with id "chunking-update" and destination "/testshare/test.txt" - Then the HTTP status code should be "403" - And Downloaded content when downloading file "/testshare/test.txt" with range "bytes=0-6" should be "Welcome" --- a/build/integration/encryption_features/encryption.feature +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: encryption - Background: - Given using api version "1" - And using new dav path - And invoking occ with "app:enable encryption" - And the command was successful - And invoking occ with "encryption:enable-master-key" with input "y" - And the command was successful - And invoking occ with "encryption:enable" - And the command was successful - - Scenario: Upload and download a file spanning several encrypted blocks - Given user "user0" exists - And As an "user0" - When User "user0" adds a file of 20000 bytes to "/big.bin" - Then the HTTP status code should be "201" - And File "/big.bin" should have prop "d:getcontentlength" equal to "20000" - When Downloading file "/big.bin" - Then the HTTP status code should be "200" - - Scenario: Copy a file spanning several encrypted blocks - Given user "user0" exists - And As an "user0" - And User "user0" adds a file of 20000 bytes to "/big.bin" - When User "user0" copies file "/big.bin" to "/copy.bin" - Then the HTTP status code should be "201" - And File "/copy.bin" should have prop "d:getcontentlength" equal to "20000" - When Downloading file "/copy.bin" - Then the HTTP status code should be "200" - - Scenario: Copy a file over an existing file - Given user "user0" exists - And As an "user0" - And User "user0" uploads file with content "the source content" to "/source.txt" - And User "user0" uploads file with content "the target content" to "/target.txt" - When User "user0" copies file "/source.txt" to "/target.txt" - Then the HTTP status code should be "204" - When Downloading file "/target.txt" - Then the HTTP status code should be "200" - And Downloaded content should be "the source content" - - Scenario: Copy a file that was written several times - Given user "user0" exists - And As an "user0" - And User "user0" uploads file with content "the first content" to "/source.txt" - And User "user0" uploads file with content "the second content" to "/source.txt" - When User "user0" copies file "/source.txt" to "/copy.txt" - Then the HTTP status code should be "201" - When Downloading file "/copy.txt" - Then the HTTP status code should be "200" - And Downloaded content should be "the second content" - - # With "part_file_in_storage" disabled the part file is written to the user - # home while the target lives on another storage, so the upload has to read the - # part file back to move it over. A part file never has a file cache entry, so - # both the encrypted version and the unencrypted size of the written blocks - # have to be known without one. - @local_storage - Scenario: Upload to an external storage while the part file is kept in the user home - Given invoking occ with "config:system:set part_file_in_storage --value false --type boolean" - And the command was successful - And user "user0" exists - And As an "user0" - When User "user0" uploads file "data/textfile.txt" to "/local_storage/textfile.txt" - Then the HTTP status code should be "201" - When Downloading file "/local_storage/textfile.txt" - Then the HTTP status code should be "200" - And Downloaded content should start with "This is a testfile." - When User "user0" adds a file of 20000 bytes to "/local_storage/big.bin" - Then the HTTP status code should be "201" - And File "/local_storage/big.bin" should have prop "d:getcontentlength" equal to "20000" - When Downloading file "/local_storage/big.bin" - Then the HTTP status code should be "200" --- a/build/integration/features/auth.feature +++ /dev/null @@ -1,115 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -# SPDX-FileCopyrightText: 2016 ownCloud, Inc. -# SPDX-License-Identifier: AGPL-3.0-only -Feature: auth - - Background: - Given user "user0" exists - Given a new restricted client token is added - Given a new unrestricted client token is added - Given the cookie jar is reset - - # FILES APP - Scenario: access files app anonymously - When requesting "/index.php/apps/files" with "GET" - Then the HTTP status code should be "401" - - Scenario: access files app with basic auth - When requesting "/index.php/apps/files" with "GET" using basic auth - Then the HTTP status code should be "200" - - Scenario: access files app with unrestricted basic token auth - When requesting "/index.php/apps/files" with "GET" using unrestricted basic token auth - Then the HTTP status code should be "200" - Then requesting "/remote.php/files/welcome.txt" with "GET" using browser session - Then the HTTP status code should be "200" - - Scenario: access files app with restricted basic token auth - When requesting "/index.php/apps/files" with "GET" using restricted basic token auth - Then the HTTP status code should be "200" - Then requesting "/remote.php/files/welcome.txt" with "GET" using browser session - Then the HTTP status code should be "404" - - Scenario: access files app with an unrestricted client token - When requesting "/index.php/apps/files" with "GET" using an unrestricted client token - Then the HTTP status code should be "200" - - Scenario: access files app with browser session - Given a new browser session is started - When requesting "/index.php/apps/files" with "GET" using browser session - Then the HTTP status code should be "200" - - # WebDAV - Scenario: using WebDAV anonymously - When requesting "/remote.php/webdav" with "PROPFIND" - Then the HTTP status code should be "401" - - Scenario: using WebDAV with basic auth - When requesting "/remote.php/webdav" with "PROPFIND" using basic auth - Then the HTTP status code should be "207" - - Scenario: using WebDAV with unrestricted basic token auth - When requesting "/remote.php/webdav" with "PROPFIND" using unrestricted basic token auth - Then the HTTP status code should be "207" - - Scenario: using WebDAV with restricted basic token auth - When requesting "/remote.php/webdav" with "PROPFIND" using restricted basic token auth - Then the HTTP status code should be "207" - - Scenario: using old WebDAV endpoint with unrestricted client token - When requesting "/remote.php/webdav" with "PROPFIND" using an unrestricted client token - Then the HTTP status code should be "207" - - Scenario: using new WebDAV endpoint with unrestricted client token - When requesting "/remote.php/dav/" with "PROPFIND" using an unrestricted client token - Then the HTTP status code should be "207" - - Scenario: using WebDAV with browser session - Given a new browser session is started - When requesting "/remote.php/webdav" with "PROPFIND" using browser session - Then the HTTP status code should be "207" - - # OCS - Scenario: using OCS anonymously - When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" - Then the OCS status code should be "997" - - Scenario: using OCS with basic auth - When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using basic auth - Then the OCS status code should be "100" - - Scenario: using OCS with token auth - When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using unrestricted basic token auth - Then the OCS status code should be "100" - - Scenario: using OCS with an unrestricted client token - When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using an unrestricted client token - Then the OCS status code should be "100" - - Scenario: using OCS with browser session - Given a new browser session is started - When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using browser session - Then the OCS status code should be "100" - - # REMEMBER ME - Scenario: remember login - Given a new remembered browser session is started - When the session cookie expires - And requesting "/index.php/apps/files" with "GET" using browser session - Then the HTTP status code should be "200" - - # AUTH TOKENS - Scenario: Creating an auth token with regular auth token should not work - When requesting "/index.php/apps/files" with "GET" using restricted basic token auth - Then the HTTP status code should be "200" - When the CSRF token is extracted from the previous response - When a new unrestricted client token is added using restricted basic token auth - Then the HTTP status code should be "403" - - Scenario: Creating a restricted auth token with regular login should work - When a new restricted client token is added - Then the HTTP status code should be "200" - - Scenario: Creating an unrestricted auth token with regular login should work - When a new unrestricted client token is added - Then the HTTP status code should be "200" --- a/build/integration/features/avatar.feature +++ /dev/null @@ -1,117 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: avatar - - Background: - Given user "user0" exists - - Scenario: get default user avatar - 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 512 - And last avatar is not a single color - - Scenario: get default user avatar as an anonymous user - When user "anonymous" 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 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 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 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: delete user avatar - Given Logging in using web as "user0" - And logged in user posts 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 | - 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 - 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 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 512 - And last avatar is not a single color - - Scenario: get default guest 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 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 512 - And last avatar is not a single color - - Scenario: Get avatar for users with cloudID formatted user id - Given user "user@example.tld" exists - Given Logging in using web as "user@example.tld" - When logged in user posts avatar from file "data/green-square-256.png" - And user "user@example.tld" gets avatar for user "user@example.tld" - 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 "user@example.tld" - 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 --- a/build/integration/features/bootstrap/Activity.php +++ /dev/null @@ -1,32 +0,0 @@ -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]); - } - } -} --- a/build/integration/features/bootstrap/AppConfiguration.php +++ /dev/null @@ -1,127 +0,0 @@ -currentUser; - $this->currentUser = 'admin'; - - $this->modifyServerConfig($app, $parameter, $value); - - $this->currentUser = $user; - } - - /** - * @param string $app - * @param string $parameter - * @param string $value - */ - protected function modifyServerConfig($app, $parameter, $value) { - $body = new \Behat\Gherkin\Node\TableNode([['value', $value]]); - $this->sendingToWith('post', "/apps/testing/api/v1/app/{$app}/{$parameter}", $body); - $this->theHTTPStatusCodeShouldBe('200'); - if ($this->apiVersion === 1) { - $this->theOCSStatusCodeShouldBe('100'); - } - } - - /** - * @param string $app - * @param string $parameter - * @param string $value - */ - protected function deleteServerConfig($app, $parameter) { - $this->sendingTo('DELETE', "/apps/testing/api/v1/app/{$app}/{$parameter}"); - $this->theHTTPStatusCodeShouldBe('200'); - if ($this->apiVersion === 1) { - $this->theOCSStatusCodeShouldBe('100'); - } - } - - protected function setStatusTestingApp($enabled) { - $this->sendingTo(($enabled ? 'post' : 'delete'), '/cloud/apps/testing'); - $this->theHTTPStatusCodeShouldBe('200'); - if ($this->apiVersion === 1) { - $this->theOCSStatusCodeShouldBe('100'); - } - - $this->sendingTo('get', '/cloud/apps?filter=enabled'); - $this->theHTTPStatusCodeShouldBe('200'); - if ($enabled) { - Assert::assertStringContainsString('testing', $this->response->getBody()->getContents()); - } else { - Assert::assertStringNotContainsString('testing', $this->response->getBody()->getContents()); - } - } - - abstract protected function resetAppConfigs(); - - /** - * @BeforeScenario - * - * Enable the testing app before the first scenario of the feature and - * reset the configs before each scenario - * @param BeforeScenarioScope $event - */ - public function prepareParameters(BeforeScenarioScope $event) { - $user = $this->currentUser; - $this->currentUser = 'admin'; - - $scenarios = $event->getFeature()->getScenarios(); - if ($event->getScenario() === reset($scenarios)) { - $this->setStatusTestingApp(true); - } - - $this->resetAppConfigs(); - - $this->currentUser = $user; - } - - /** - * @AfterScenario - * - * Reset the values after the last scenario of the feature and disable the testing app - * @param AfterScenarioScope $event - */ - public function undoChangingParameters(AfterScenarioScope $event) { - $scenarios = $event->getFeature()->getScenarios(); - if ($event->getScenario() === end($scenarios)) { - $user = $this->currentUser; - $this->currentUser = 'admin'; - - $this->resetAppConfigs(); - - $this->setStatusTestingApp(false); - $this->currentUser = $user; - } - } -} --- a/build/integration/features/bootstrap/Auth.php +++ /dev/null @@ -1,257 +0,0 @@ -client = new Client(); - $this->responseXml = ''; - $this->cookieJar = new CookieJar(); - } - - /** - * @When requesting :url with :method - */ - public function requestingWith($url, $method) { - $this->sendRequest($url, $method); - } - - private function sendRequest($url, $method, $authHeader = null, $useCookies = false) { - $fullUrl = substr($this->baseUrl, 0, -5) . $url; - try { - if ($useCookies) { - $options = [ - 'cookies' => $this->cookieJar, - ]; - } else { - $options = []; - } - if ($authHeader) { - $options['headers'] = [ - 'Authorization' => $authHeader - ]; - } - $options['headers']['OCS_APIREQUEST'] = 'true'; - $options['headers']['requesttoken'] = $this->requestToken; - $this->response = $this->client->request($method, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } catch (ServerException $ex) { - $this->response = $ex->getResponse(); - } - } - - /** - * @When the CSRF token is extracted from the previous response - */ - public function theCsrfTokenIsExtractedFromThePreviousResponse() { - $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $this->response->getBody()->getContents()), 0, 89); - } - - /** - * @param bool $loginViaWeb - * @return object - */ - private function createClientToken($loginViaWeb = true) { - if ($loginViaWeb) { - $this->loggingInUsingWebAs('user0'); - } - - $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens'; - $client = new Client(); - $options = [ - 'auth' => [ - 'user0', - $loginViaWeb ? '123456' : $this->restrictedClientToken, - ], - 'form_params' => [ - 'requesttoken' => $this->requestToken, - 'name' => md5(microtime()), - ], - 'cookies' => $this->cookieJar, - ]; - - try { - $this->response = $client->request('POST', $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } catch (ServerException $e) { - $this->response = $e->getResponse(); - } - return json_decode($this->response->getBody()->getContents()); - } - - /** - * @Given a new restricted client token is added - */ - public function aNewRestrictedClientTokenIsAdded() { - $tokenObj = $this->createClientToken(); - $newCreatedTokenId = $tokenObj->deviceToken->id; - $fullUrl = substr($this->baseUrl, 0, -5) . '/index.php/settings/personal/authtokens/' . $newCreatedTokenId; - $client = new Client(); - $options = [ - 'auth' => ['user0', '123456'], - 'headers' => [ - 'requesttoken' => $this->requestToken, - ], - 'json' => [ - 'name' => md5(microtime()), - 'scope' => [ - 'filesystem' => false, - ], - ], - 'cookies' => $this->cookieJar, - ]; - $this->response = $client->request('PUT', $fullUrl, $options); - $this->restrictedClientToken = $tokenObj->token; - } - - /** - * @Given a new unrestricted client token is added - */ - public function aNewUnrestrictedClientTokenIsAdded() { - $this->unrestrictedClientToken = $this->createClientToken()->token; - } - - /** - * @When a new unrestricted client token is added using restricted basic token auth - */ - public function aNewUnrestrictedClientTokenIsAddedUsingRestrictedBasicTokenAuth() { - $this->createClientToken(false); - } - - /** - * @When requesting :url with :method using basic auth - * - * @param string $url - * @param string $method - */ - public function requestingWithBasicAuth($url, $method) { - $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:123456')); - } - - /** - * @When requesting :url with :method using unrestricted basic token auth - * - * @param string $url - * @param string $method - */ - public function requestingWithUnrestrictedBasicTokenAuth($url, $method) { - $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:' . $this->unrestrictedClientToken), true); - } - - /** - * @When requesting :url with :method using restricted basic token auth - * - * @param string $url - * @param string $method - */ - public function requestingWithRestrictedBasicTokenAuth($url, $method) { - $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:' . $this->restrictedClientToken), true); - } - - /** - * @When requesting :url with :method using an unrestricted client token - * - * @param string $url - * @param string $method - */ - public function requestingWithUsingAnUnrestrictedClientToken($url, $method) { - $this->sendRequest($url, $method, 'Bearer ' . $this->unrestrictedClientToken); - } - - /** - * @When requesting :url with :method using a restricted client token - * - * @param string $url - * @param string $method - */ - public function requestingWithUsingARestrictedClientToken($url, $method) { - $this->sendRequest($url, $method, 'Bearer ' . $this->restrictedClientToken); - } - - /** - * @When requesting :url with :method using browser session - * - * @param string $url - * @param string $method - */ - public function requestingWithBrowserSession($url, $method) { - $this->sendRequest($url, $method, null, true); - } - - /** - * @Given a new browser session is started - * - * @param bool $remember - */ - public function aNewBrowserSessionIsStarted($remember = false) { - $baseUrl = substr($this->baseUrl, 0, -5); - $loginUrl = $baseUrl . '/login'; - // Request a new session and extract CSRF token - $client = new Client(); - $response = $client->get($loginUrl, [ - 'cookies' => $this->cookieJar, - ]); - $this->extracRequestTokenFromResponse($response); - - // Login and extract new token - $client = new Client(); - $response = $client->post( - $loginUrl, [ - 'form_params' => [ - 'user' => 'user0', - 'password' => '123456', - 'rememberme' => $remember ? '1' : '0', - 'requesttoken' => $this->requestToken, - ], - 'cookies' => $this->cookieJar, - 'headers' => [ - 'Origin' => $baseUrl, - ], - ] - ); - $this->extracRequestTokenFromResponse($response); - } - - /** - * @Given a new remembered browser session is started - */ - public function aNewRememberedBrowserSessionIsStarted() { - $this->aNewBrowserSessionIsStarted(true); - } - - /** - * @Given the cookie jar is reset - */ - public function theCookieJarIsReset() { - $this->cookieJar = new CookieJar(); - } - - /** - * @When the session cookie expires - */ - public function whenTheSessionCookieExpires() { - $this->cookieJar->clearSessionCookies(); - } -} --- a/build/integration/features/bootstrap/Avatar.php +++ /dev/null @@ -1,213 +0,0 @@ -lastAvatar = null; - } - - private function getLastAvatar() { - $this->lastAvatar = ''; - - $body = $this->response->getBody(); - while (!$body->eof()) { - $this->lastAvatar .= $body->read(8192); - } - $body->close(); - } - - /** - * @When user :user gets avatar for user :userAvatar - * - * @param string $user - * @param string $userAvatar - */ - public function userGetsAvatarForUser(string $user, string $userAvatar) { - $this->userGetsAvatarForUserWithSize($user, $userAvatar, '128'); - } - - /** - * @When user :user gets avatar for user :userAvatar with size :size - * - * @param string $user - * @param string $userAvatar - * @param string $size - */ - public function userGetsAvatarForUserWithSize(string $user, string $userAvatar, string $size) { - $this->asAn($user); - $this->sendingToDirectUrl('GET', '/index.php/avatar/' . $userAvatar . '/' . $size); - $this->theHTTPStatusCodeShouldBe('200'); - - $this->getLastAvatar(); - } - - /** - * @When user :user gets avatar for guest :guestAvatar - * - * @param string $user - * @param string $guestAvatar - */ - public function userGetsAvatarForGuest(string $user, string $guestAvatar) { - $this->asAn($user); - $this->sendingToDirectUrl('GET', '/index.php/avatar/guest/' . $guestAvatar . '/128'); - $this->theHTTPStatusCodeShouldBe('201'); - - $this->getLastAvatar(); - } - - /** - * @When logged in user posts avatar from file :source - * - * @param string $source - */ - public function loggedInUserPostsAvatarFromFile(string $source) { - $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); - - $this->sendingAToWithRequesttoken('POST', '/index.php/avatar', - [ - 'multipart' => [ - [ - 'name' => 'files[]', - 'contents' => $file - ] - ] - ]); - $this->theHTTPStatusCodeShouldBe('200'); - } - - /** - * @When logged in user posts avatar from internal path :path - * - * @param string $path - */ - public function loggedInUserPostsAvatarFromInternalPath(string $path) { - $this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path); - $this->theHTTPStatusCodeShouldBe('200'); - } - - /** - * @When logged in user deletes the user avatar - */ - public function loggedInUserDeletesTheUserAvatar() { - $this->sendingAToWithRequesttoken('DELETE', '/index.php/avatar'); - $this->theHTTPStatusCodeShouldBe('200'); - } - - /** - * @Then last avatar is a square of size :size - * - * @param string size - */ - public function lastAvatarIsASquareOfSize(string $size) { - [$width, $height] = getimagesizefromstring($this->lastAvatar); - - 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() { - Assert::assertEquals(null, $this->getColorFromLastAvatar()); - } - - /** - * @Then last avatar is a single :color color - * - * @param string $color - * @param string $size - */ - public function lastAvatarIsASingleColor(string $color) { - $expectedColor = $this->hexStringToRgbColor($color); - $colorFromLastAvatar = $this->getColorFromLastAvatar(); - - Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar), - $this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color); - } - - private function hexStringToRgbColor($hexString) { - // Strip initial "#" - $hexString = substr($hexString, 1); - - $rgbColorInt = hexdec($hexString); - - // RGBA hex strings are not supported; the given string is assumed to be - // an RGB hex string. - return [ - 'red' => ($rgbColorInt >> 16) & 0xFF, - 'green' => ($rgbColorInt >> 8) & 0xFF, - 'blue' => $rgbColorInt & 0xFF, - 'alpha' => 0 - ]; - } - - private function rgbColorToHexString($rgbColor) { - $rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']); - - return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT); - } - - private function getColorFromLastAvatar() { - $image = imagecreatefromstring($this->lastAvatar); - - $firstPixelColorIndex = imagecolorat($image, 0, 0); - $firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex); - - for ($i = 0; $i < imagesx($image); $i++) { - for ($j = 0; $j < imagesx($image); $j++) { - $currentPixelColorIndex = imagecolorat($image, $i, $j); - $currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex); - - // The colors are compared with a small allowed delta, as even - // on solid color images the resizing can cause some small - // artifacts that slightly modify the color of certain pixels. - if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) { - return null; - } - } - } - - return $firstPixelColor; - } - - 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)) { - return true; - } - - return false; - } - - private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { - if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) - && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { - return true; - } - - return false; - } -} --- a/build/integration/features/bootstrap/BasicStructure.php +++ /dev/null @@ -1,570 +0,0 @@ -baseUrl = $baseUrl; - $this->adminUser = $admin; - $this->regularUser = $regular_user_password; - $this->localBaseUrl = $this->baseUrl; - $this->remoteBaseUrl = $this->baseUrl; - $this->currentServer = 'LOCAL'; - $this->cookieJar = new CookieJar(); - - // in case of ci deployment we take the server url from the environment - $testServerUrl = getenv('TEST_SERVER_URL'); - if ($testServerUrl !== false) { - $this->baseUrl = $testServerUrl; - $this->localBaseUrl = $testServerUrl; - } - - // federated server url from the environment - $testRemoteServerUrl = getenv('TEST_SERVER_FED_URL'); - if ($testRemoteServerUrl !== false) { - $this->remoteBaseUrl = $testRemoteServerUrl; - } - } - - /** - * @BeforeScenario - */ - public function resetAuthState(): void { - $this->currentUser = ''; - $this->currentServer = 'LOCAL'; - $this->baseUrl = $this->localBaseUrl; - $this->apiVersion = 1; - $this->requestToken = null; - $this->cookieJar = new CookieJar(); - } - - /** - * @Given /^using api version "(\d+)"$/ - */ - public function usingApiVersion(int $version): void { - $this->apiVersion = (int)$version; - } - - /** - * @Given /^As an "([^"]*)"$/ - */ - public function asAn(string $user): void { - $this->currentUser = $user; - } - - /** - * @Given /^Using server "(LOCAL|REMOTE)"$/ - * @return string Previous used server - */ - public function usingServer(string $server): string { - $previousServer = $this->currentServer; - if ($server === 'LOCAL') { - $this->baseUrl = $this->localBaseUrl; - $this->currentServer = 'LOCAL'; - return $previousServer; - } else { - $this->baseUrl = $this->remoteBaseUrl; - $this->currentServer = 'REMOTE'; - return $previousServer; - } - } - - /** - * @When /^sending "([^"]*)" to "([^"]*)"$/ - */ - public function sendingTo(string $verb, string $url): void { - $this->sendingToWith($verb, $url, null); - } - - /** - * Parses the xml or json answer to get ocs response which doesn't match with - * http one in v1 of the api. - */ - public function getOCSResponseCode(ResponseInterface $response): int { - if ($response === null) { - throw new \RuntimeException('No response available'); - } - - $body = (string)$response->getBody(); - if (str_starts_with($body, '<')) { - $body = simplexml_load_string($body); - if ($body === false) { - throw new \RuntimeException('Could not parse OCS response, body is not valid XML'); - } - return (int)$body->meta[0]->statuscode; - } - - return json_decode($body, true)['ocs']['meta']['statuscode']; - } - - /** - * This function is needed to use a vertical fashion in the gherkin tables. - */ - public function simplifyArray(array $arrayOfArrays): array { - $a = array_map(function ($subArray) { - return $subArray[0]; - }, $arrayOfArrays); - return $a; - } - - /** - * @When /^sending "([^"]*)" to "([^"]*)" with$/ - */ - public function sendingToWith(string $verb, string $url, ?TableNode $body): void { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } elseif (strpos($this->currentUser, 'anonymous') !== 0) { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIRequest' => 'true' - ]; - if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } - - // TODO: Fix this hack! - if ($verb === 'PUT' && $body === null) { - $options['form_params'] = [ - 'foo' => 'bar', - ]; - } - - try { - $this->response = $client->request($verb, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } catch (ServerException $ex) { - $this->response = $ex->getResponse(); - } - } - - protected function sendRequestForJSON(string $verb, string $url, TableNode|array|null $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(); - } - } - - /** - * @When /^sending "([^"]*)" with exact url to "([^"]*)"$/ - * @param string $verb - * @param string $url - */ - public function sendingToDirectUrl($verb, $url) { - $this->sendingToWithDirectUrl($verb, $url, null); - } - - public function sendingToWithDirectUrl($verb, $url, $body) { - $fullUrl = substr($this->baseUrl, 0, -5) . $url; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } elseif (strpos($this->currentUser, 'anonymous') !== 0) { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } - - try { - $this->response = $client->request($verb, $fullUrl, $options); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } - } - - public function isExpectedUrl($possibleUrl, $finalPart) { - $baseUrlChopped = substr($this->baseUrl, 0, -4); - $endCharacter = strlen($baseUrlChopped) + strlen($finalPart); - return (substr($possibleUrl, 0, $endCharacter) === "$baseUrlChopped" . "$finalPart"); - } - - /** - * @Then /^the OCS status code should be "([^"]*)"$/ - * @param int $statusCode - */ - public function theOCSStatusCodeShouldBe($statusCode) { - Assert::assertEquals($statusCode, $this->getOCSResponseCode($this->response)); - } - - /** - * @Then /^the HTTP status code should be "([^"]*)"$/ - * @param int $statusCode - */ - public function theHTTPStatusCodeShouldBe($statusCode) { - Assert::assertEquals($statusCode, $this->response->getStatusCode()); - } - - /** - * @Then /^the Content-Type should be "([^"]*)"$/ - * @param string $contentType - */ - public function theContentTypeShouldbe($contentType) { - Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]); - } - - /** - * @param ResponseInterface $response - */ - private function extracRequestTokenFromResponse(ResponseInterface $response) { - $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89); - } - - /** - * @Given Logging in using web as :user - * @param string $user - */ - public function loggingInUsingWebAs($user) { - $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( - $loginUrl, - [ - 'cookies' => $this->cookieJar, - ] - ); - $this->extracRequestTokenFromResponse($response); - - // Login and extract new token - $password = ($user === 'admin') ? 'admin' : '123456'; - $client = new Client(); - $response = $client->post( - $loginUrl, - [ - 'form_params' => [ - 'user' => $user, - 'password' => $password, - 'requesttoken' => $this->requestToken, - ], - 'cookies' => $this->cookieJar, - 'headers' => [ - 'Origin' => $baseUrl, - ], - ] - ); - $this->extracRequestTokenFromResponse($response); - } - - /** - * @When Sending a :method to :url with requesttoken - * @param string $method - * @param string $url - * @param TableNode|array|null $body - */ - public function sendingAToWithRequesttoken($method, $url, $body = null) { - $baseUrl = substr($this->baseUrl, 0, -5); - - $options = [ - 'cookies' => $this->cookieJar, - 'headers' => [ - 'requesttoken' => $this->requestToken - ], - ]; - - if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - } elseif ($body) { - $options = array_merge_recursive($options, $body); - } - - $client = new Client(); - try { - $this->response = $client->request( - $method, - $baseUrl . $url, - $options - ); - } catch (ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When Sending a :method to :url without requesttoken - * @param string $method - * @param string $url - */ - public function sendingAToWithoutRequesttoken($method, $url) { - $baseUrl = substr($this->baseUrl, 0, -5); - - $client = new Client(); - try { - $this->response = $client->request( - $method, - $baseUrl . $url, - [ - 'cookies' => $this->cookieJar - ] - ); - } catch (ClientException $e) { - $this->response = $e->getResponse(); - } - } - - public static function removeFile($path, $filename) { - if (file_exists("$path" . "$filename")) { - unlink("$path" . "$filename"); - } - } - - /** - * @Given User :user modifies text of :filename with text :text - * @param string $user - * @param string $filename - * @param string $text - */ - public function modifyTextOfFile($user, $filename, $text) { - self::removeFile($this->getDataDirectory() . "/$user/files", "$filename"); - file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename", "$text"); - } - - private function getDataDirectory() { - // Based on "runOcc" from CommandLine trait - $args = ['config:system:get', 'datadirectory']; - $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); - - return trim($lastStdOut); - } - - /** - * @Given file :filename is created :times times in :user user data - * @param string $filename - * @param string $times - * @param string $user - */ - public function fileIsCreatedTimesInUserData($filename, $times, $user) { - for ($i = 0; $i < $times; $i++) { - file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename-$i", "content-$i"); - } - } - - public function createFileSpecificSize($name, $size) { - $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'); - fwrite($file, $text); - fclose($file); - } - - /** - * @Given file :filename of size :size is created in local storage - * @param string $filename - * @param string $size - */ - public function fileIsCreatedInLocalStorageWithSize($filename, $size) { - $this->createFileSpecificSize("local_storage/$filename", $size); - } - - /** - * @Given file :filename with text :text is created in local storage - * @param string $filename - * @param string $text - */ - public function fileIsCreatedInLocalStorageWithText($filename, $text) { - $this->createFileWithText("local_storage/$filename", $text); - } - - /** - * @When Sleep for :seconds seconds - * @param int $seconds - */ - public function sleepForSeconds($seconds) { - sleep((int)$seconds); - } - - /** - * @BeforeSuite - */ - public static function createPHPUnitConfiguration(): void { - (new \PHPUnit\TextUI\Configuration\Builder())->build([]); - } - - /** - * @BeforeSuite - */ - public static function addFilesToSkeleton() { - for ($i = 0; $i < 5; $i++) { - 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/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/CHILD/' . 'child.txt', "Nextcloud test text file\n"); - } - - /** - * @AfterSuite - */ - public static function removeFilesFromSkeleton() { - for ($i = 0; $i < 5; $i++) { - self::removeFile('../../core/skeleton/', 'textfile' . "$i" . '.txt'); - } - 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/', 'parent.txt'); - if (is_dir('../../core/skeleton/PARENT')) { - rmdir('../../core/skeleton/PARENT'); - } - } - - /** - * @BeforeScenario @local_storage - */ - public static function removeFilesFromLocalStorageBefore() { - $dir = './work/local_storage/'; - $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); - $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($ri as $file) { - $file->isDir() ? rmdir($file) : unlink($file); - } - } - - /** - * @AfterScenario @local_storage - */ - public static function removeFilesFromLocalStorageAfter() { - $dir = './work/local_storage/'; - $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); - $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($ri as $file) { - $file->isDir() ? rmdir($file) : unlink($file); - } - } - - /** - * @Given /^cookies are reset$/ - */ - public function cookiesAreReset() { - $this->cookieJar = new CookieJar(); - } - - /** - * @Then The following headers should be set - * @param TableNode $table - * @throws \Exception - */ - public function theFollowingHeadersShouldBeSet(TableNode $table) { - foreach ($table->getTable() as $header) { - $headerName = $header[0]; - $expectedHeaderValue = $header[1]; - $returnedHeader = $this->response->getHeader($headerName)[0]; - if ($returnedHeader !== $expectedHeaderValue) { - throw new \Exception( - sprintf( - "Expected value '%s' for header '%s', got '%s'", - $expectedHeaderValue, - $headerName, - $returnedHeader - ) - ); - } - } - } -} --- a/build/integration/features/bootstrap/CalDavContext.php +++ /dev/null @@ -1,430 +0,0 @@ -baseUrl = substr($testServerUrl, 0, -5); - } - } - - /** @BeforeScenario */ - public function setUpScenario() { - $this->client = new Client(); - $this->responseXml = ''; - } - - /** @AfterScenario */ - public function afterScenario() { - foreach (['MyCalendar', 'MyCalendar2'] as $calendarName) { - try { - $this->client->delete( - $this->baseUrl . '/remote.php/dav/calendars/admin/' . $calendarName, - [ - 'auth' => ['admin', 'admin'], - 'headers' => ['X-NC-CalDAV-No-Trashbin' => '1'], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - } - } - } - - /** @AfterScenario @caldav-delegation */ - public function afterDelegationScenario() { - foreach (['calendar-proxy-read', 'calendar-proxy-write'] as $proxyType) { - try { - $propPatch = new \Sabre\DAV\Xml\Request\PropPatch(); - $propPatch->properties = ['{DAV:}group-member-set' => new \Sabre\DAV\Xml\Property\Href([])]; - $xml = new \Sabre\Xml\Service(); - $body = $xml->write('{DAV:}propertyupdate', $propPatch, '/'); - $this->client->request( - 'PROPPATCH', - $this->baseUrl . '/remote.php/dav/principals/users/admin/' . $proxyType, - [ - 'headers' => ['Content-Type' => 'application/xml; charset=UTF-8'], - 'body' => $body, - 'auth' => ['admin', 'admin'], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - } - } - } - - /** - * @When :user requests calendar :calendar on the endpoint :endpoint - * @param string $user - * @param string $calendar - * @param string $endpoint - */ - public function requestsCalendar($user, $calendar, $endpoint) { - $davUrl = $this->baseUrl . $endpoint . $calendar; - - $password = ($user === 'admin') ? 'admin' : '123456'; - try { - $this->response = $this->client->request( - 'PROPFIND', - $davUrl, - [ - 'auth' => [ - $user, - $password, - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @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' => '', - '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 contain an href :href - * @throws \Exception - */ - public function theCaldavResponseShouldContainAnHref(string $href): void { - /** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */ - $multiStatus = $this->responseXml['value']; - foreach ($multiStatus->getResponses() as $response) { - if ($response->getHref() === $href) { - return; - } - } - throw new \Exception( - sprintf( - 'Expected href %s not found in response', - $href, - ) - ); - } - - /** - * @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 - */ - public function theCaldavHttpStatusCodeShouldBe($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(); - } - } - - /** - * @Then The exception is :message - * @param string $message - * @throws \Exception - */ - public function theExceptionIs($message) { - $result = $this->responseXml['value'][0]['value']; - - if ($message !== $result) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - $message, - $result - ) - ); - } - } - - /** - * @Then The error message is :message - * @param string $message - * @throws \Exception - */ - public function theErrorMessageIs($message) { - $result = $this->responseXml['value'][1]['value']; - - if ($message !== $result) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - $message, - $result - ) - ); - } - } - - /** - * @Given :user creates a calendar named :name - * @param string $user - * @param string $name - */ - public function createsACalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; - $password = ($user === 'admin') ? 'admin' : '123456'; - - $this->response = $this->client->request( - 'MKCALENDAR', - $davUrl, - [ - 'body' => 'test1#21213D', - 'auth' => [ - $user, - $password, - ], - ] - ); - } - - /** - * @Then :user publicly shares the calendar named :name - * - * @param string $user - * @param string $name - */ - public function publiclySharesTheCalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; - $password = ($user === 'admin') ? 'admin' : '123456'; - - $this->response = $this->client->request( - 'POST', - $davUrl, - [ - 'body' => '', - 'auth' => [ - $user, - $password, - ], - 'headers' => [ - 'Content-Type' => 'application/xml; charset=UTF-8', - ], - ] - ); - } - - /** - * @Then There should be :amount calendars in the response body - * - * @param string $amount - */ - public function t($amount) { - $jsonEncoded = json_encode($this->responseXml); - $arrayElement = json_decode($jsonEncoded, true); - $actual = count($arrayElement['value']) - 1; - if ($actual !== (int)$amount) { - throw new InvalidArgumentException( - sprintf( - 'Expected %s got %s', - $amount, - $actual - ) - ); - } - } - - /** - * @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' => 'test1#21213D', - '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, '/'); - - try { - $this->response = $this->client->request( - 'PROPPATCH', - $davUrl, - [ - 'headers' => [ - 'Content-Type' => 'application/xml; charset=UTF-8', - ], - 'body' => $body, - 'auth' => [ - $user, - $password, - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } -} --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ /dev/null @@ -1,58 +0,0 @@ -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']); - $answeredValue = $capabilitiesXML->{$row['capability']}; - for ($i = 0; $i < count($path_to_element); $i++) { - $answeredValue = $answeredValue->{$path_to_element[$i]}; - } - $answeredValue = (string)$answeredValue; - Assert::assertEquals( - $row['value'] === 'EMPTY' ? '' : $row['value'], - $answeredValue, - 'Failed field ' . $row['capability'] . ' ' . $row['path_to_element'] - ); - } - } - - protected function resetAppConfigs() { - $this->deleteServerConfig('core', 'shareapi_enabled'); - $this->deleteServerConfig('core', 'shareapi_allow_links'); - $this->deleteServerConfig('core', 'shareapi_allow_public_upload'); - $this->deleteServerConfig('core', 'shareapi_allow_resharing'); - $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); - $this->deleteServerConfig('files_sharing', 'incoming_server2server_share_enabled'); - $this->deleteServerConfig('core', 'shareapi_enforce_links_password'); - $this->deleteServerConfig('core', 'shareapi_allow_public_notification'); - $this->deleteServerConfig('core', 'shareapi_default_expire_date'); - $this->deleteServerConfig('core', 'shareapi_enforce_expire_date'); - $this->deleteServerConfig('core', 'shareapi_allow_group_sharing'); - } -} --- a/build/integration/features/bootstrap/CardDavContext.php +++ /dev/null @@ -1,351 +0,0 @@ -baseUrl = substr($testServerUrl, 0, -5); - } - } - - /** @BeforeScenario */ - public function setUpScenario() { - $this->client = new Client(); - $this->responseXml = ''; - } - - /** @AfterScenario */ - public function afterScenario() { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/admin/MyAddressbook'; - try { - $this->client->delete( - $davUrl, - [ - 'auth' => [ - 'admin', - 'admin', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - } - } - - /** - * @When :user requests addressbook :addressBook with statuscode :statusCode on the endpoint :endpoint - * @param string $user - * @param string $addressBook - * @param int $statusCode - * @param string $endpoint - * @throws \Exception - */ - public function requestsAddressbookWithStatuscodeOnTheEndpoint($user, $addressBook, $statusCode, $endpoint) { - $davUrl = $this->baseUrl . $endpoint . $addressBook; - - $password = ($user === 'admin') ? 'admin' : '123456'; - try { - $this->response = $this->client->request( - 'PROPFIND', - $davUrl, - [ - 'auth' => [ - $user, - $password, - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - - if ((int)$statusCode !== $this->response->getStatusCode()) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - (int)$statusCode, - $this->response->getStatusCode() - ) - ); - } - - $body = $this->response->getBody()->getContents(); - if (substr($body, 0, 1) === '<') { - $reader = new Sabre\Xml\Reader(); - $reader->xml($body); - $this->responseXml = $reader->parse(); - } - } - - /** - * @Given :user creates an addressbook named :addressBook with statuscode :statusCode - * @param string $user - * @param string $addressBook - * @param int $statusCode - * @throws \Exception - */ - public function createsAnAddressbookNamedWithStatuscode($user, $addressBook, $statusCode) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook; - $password = ($user === 'admin') ? 'admin' : '123456'; - - $this->response = $this->client->request( - 'MKCOL', - $davUrl, - [ - 'body' => ' - - - - , - ,' . $addressBook . ' - - - ', - 'auth' => [ - $user, - $password, - ], - 'headers' => [ - 'Content-Type' => 'application/xml;charset=UTF-8', - ], - ] - ); - - if ($this->response->getStatusCode() !== (int)$statusCode) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - (int)$statusCode, - $this->response->getStatusCode() - ) - ); - } - } - - /** - * @When The CardDAV exception is :message - * @param string $message - * @throws \Exception - */ - public function theCarddavExceptionIs($message) { - $result = $this->responseXml['value'][0]['value']; - - if ($message !== $result) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - $message, - $result - ) - ); - } - } - - /** - * @When The CardDAV error message is :arg1 - * @param string $message - * @throws \Exception - */ - public function theCarddavErrorMessageIs($message) { - $result = $this->responseXml['value'][1]['value']; - - if ($message !== $result) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - $message, - $result - ) - ); - } - } - - /** - * @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; - $password = ($user === 'admin') ? 'admin' : '123456'; - - $this->response = $this->client->request( - 'PUT', - $davUrl, - [ - 'body' => file_get_contents(__DIR__ . '/../../data/' . $fileName), - 'auth' => [ - $user, - $password, - ], - 'headers' => [ - 'Content-Type' => 'application/xml;charset=UTF-8', - ], - ] - ); - - if ($this->response->getStatusCode() !== 201) { - throw new \Exception( - sprintf( - 'Expected %s got %s', - 201, - $this->response->getStatusCode() - ) - ); - } - } - - /** - * @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'; - $password = ($user === 'admin') ? 'admin' : '123456'; - - try { - $this->response = $this->client->request( - 'GET', - $davUrl, - [ - 'auth' => [ - $user, - $password, - ], - 'headers' => [ - 'Content-Type' => 'application/xml;charset=UTF-8', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @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; - $password = ($user === 'admin') ? 'admin' : '123456'; - - try { - $this->response = $this->client->request( - 'GET', - $davUrl, - [ - 'auth' => [ - $user, - $password, - ], - 'headers' => [ - 'Content-Type' => 'application/xml;charset=UTF-8', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @Then The following HTTP headers should be set - * @param \Behat\Gherkin\Node\TableNode $table - * @throws \Exception - */ - public function theFollowingHttpHeadersShouldBeSet(\Behat\Gherkin\Node\TableNode $table) { - foreach ($table->getTable() as $header) { - $headerName = $header[0]; - $expectedHeaderValue = $header[1]; - $returnedHeader = $this->response->getHeader($headerName)[0]; - if ($returnedHeader !== $expectedHeaderValue) { - throw new \Exception( - sprintf( - "Expected value '%s' for header '%s', got '%s'", - $expectedHeaderValue, - $headerName, - $returnedHeader - ) - ); - } - } - } - - /** - * @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' => ' - - - - , - ,' . $addressbook . ' - - - ', - '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(); - } - } -} --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ /dev/null @@ -1,209 +0,0 @@ -baseUrl = substr($testServerUrl, 0, -5); - } - } - - /** @BeforeScenario */ - public function setUpScenario() { - $this->client = new Client(); - } - - /** @AfterScenario */ - public function tearDownScenario() { - } - - /** - * @param string $userName - * @return string - */ - private function getPasswordForUser($userName) { - if ($userName === 'admin') { - return 'admin'; - } - return '123456'; - } - - /** - * @When user :user uploads file :source to :destination with checksum :checksum - * @param string $user - * @param string $source - * @param string $destination - * @param string $checksum - */ - public function userUploadsFileToWithChecksum($user, $source, $destination, $checksum) { - $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); - try { - $this->response = $this->client->put( - $this->baseUrl . '/remote.php/webdav' . $destination, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user) - ], - 'body' => $file, - 'headers' => [ - 'OC-Checksum' => $checksum - ] - ] - ); - } catch (\GuzzleHttp\Exception\ServerException $e) { - // 4xx and 5xx responses cause an exception - $this->response = $e->getResponse(); - } - } - - /** - * @Then The webdav response should have a status code :statusCode - * @param int $statusCode - * @throws \Exception - */ - public function theWebdavResponseShouldHaveAStatusCode($statusCode) { - if ((int)$statusCode !== $this->response->getStatusCode()) { - throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode()); - } - } - - /** - * @When user :user request the checksum of :path via propfind - * @param string $user - * @param string $path - */ - public function userRequestTheChecksumOfViaPropfind($user, $path) { - $this->response = $this->client->request( - 'PROPFIND', - $this->baseUrl . '/remote.php/webdav' . $path, - [ - 'body' => ' - - - - -', - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ] - ] - ); - } - - /** - * @Then The webdav checksum should match :checksum - * @param string $checksum - * @throws \Exception - */ - public function theWebdavChecksumShouldMatch($checksum) { - $service = new Sabre\Xml\Service(); - $parsed = $service->parse($this->response->getBody()->getContents()); - - /* - * Fetch the checksum array - * Maybe we want to do this a bit cleaner ;) - */ - $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']); - } - } - - /** - * @When user :user downloads the file :path - * @param string $user - * @param string $path - */ - public function userDownloadsTheFile($user, $path) { - $this->response = $this->client->get( - $this->baseUrl . '/remote.php/webdav' . $path, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ] - ] - ); - } - - /** - * @Then The header checksum should match :checksum - * @param string $checksum - * @throws \Exception - */ - public function theHeaderChecksumShouldMatch($checksum) { - if ($this->response->getHeader('OC-Checksum')[0] !== $checksum) { - throw new \Exception("Expected $checksum, got " . $this->response->getHeader('OC-Checksum')[0]); - } - } - - /** - * @Given User :user copied file :source to :destination - * @param string $user - * @param string $source - * @param string $destination - */ - public function userCopiedFileTo($user, $source, $destination) { - $this->response = $this->client->request( - 'MOVE', - $this->baseUrl . '/remote.php/webdav' . $source, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'headers' => [ - 'Destination' => $this->baseUrl . '/remote.php/webdav' . $destination, - ], - ] - ); - } - - /** - * @Then The webdav checksum should be empty - */ - public function theWebdavChecksumShouldBeEmpty() { - $service = new Sabre\Xml\Service(); - $parsed = $service->parse($this->response->getBody()->getContents()); - - /* - * Fetch the checksum array - * Maybe we want to do this a bit cleaner ;) - */ - $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); - } - } - - /** - * @Then The OC-Checksum header should not be there - */ - public function theOcChecksumHeaderShouldNotBeThere() { - if ($this->response->hasHeader('OC-Checksum')) { - throw new \Exception('Expected no checksum header but got ' . $this->response->getHeader('OC-Checksum')[0]); - } - } -} --- a/build/integration/features/bootstrap/CollaborationContext.php +++ /dev/null @@ -1,206 +0,0 @@ -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 = 'myaddressbook'; - $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 = <<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_user_id'); - $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); - } -} --- a/build/integration/features/bootstrap/CommandLine.php +++ /dev/null @@ -1,112 +0,0 @@ -runOcc($args); - } - - /** - * @Given /^invoking occ with "([^"]*)" with input "([^"]+)"$/ - */ - public function invokingTheCommandWith(string $cmd, string $inputString): void { - $args = explode(' ', $cmd); - $this->runOcc($args, $inputString); - } - - /** - * Find exception texts in stderr - */ - public function findExceptions(): array { - $exceptions = []; - $captureNext = false; - // the exception text usually appears after an "[Exception"] row - foreach (explode("\n", $this->lastStdErr) as $line) { - if (preg_match('/\[Exception\]/', $line)) { - $captureNext = true; - continue; - } - if ($captureNext) { - $exceptions[] = trim($line); - $captureNext = false; - } - } - - return $exceptions; - } - - /** - * @Then /^the command was successful$/ - */ - public function theCommandWasSuccessful(): void { - $exceptions = $this->findExceptions(); - if ($this->lastCode !== 0) { - $msg = 'The command was not successful, exit code was ' . $this->lastCode . '.'; - if (!empty($exceptions)) { - $msg .= ' Exceptions: ' . implode(', ', $exceptions); - } - throw new \Exception($msg); - } elseif (!empty($exceptions)) { - $msg = 'The command was successful but triggered exceptions: ' . implode(', ', $exceptions); - throw new \Exception($msg); - } - } - - /** - * @Then /^the command failed with exit code ([0-9]+)$/ - */ - public function theCommandFailedWithExitCode($exitCode): void { - if ($this->lastCode !== (int)$exitCode) { - throw new \Exception('The command was expected to fail with exit code ' . $exitCode . ' but got ' . $this->lastCode); - } - } - - /** - * @Then /^the command failed with exception text "([^"]*)"$/ - */ - public function theCommandFailedWithException(string $exceptionText): void { - $exceptions = $this->findExceptions(); - if (empty($exceptions)) { - throw new \Exception('The command did not throw any exceptions'); - } - - if (!in_array($exceptionText, $exceptions)) { - throw new \Exception('The command did not throw any exception with the text "' . $exceptionText . '"'); - } - } - - /** - * @Then /^the command output contains the text "([^"]*)"$/ - */ - public function theCommandOutputContainsTheText(string $text): void { - Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout.'); - } - - /** - * @Then /^the command output does not contain the text "([^"]*)"$/ - */ - public function theCommandOutputDoesNotContainTheText(string $text): void { - Assert::assertStringNotContainsString($text, $this->lastStdOut, 'The command did output the not expected text on stdout.'); - } - - /** - * @Then /^the command error output contains the text "([^"]*)"$/ - */ - public function theCommandErrorOutputContainsTheText(string $text): void { - Assert::assertStringContainsString($text, $this->lastStdErr, 'The command did not output the expected text on stderr.'); - } -} --- a/build/integration/features/bootstrap/CommandLineContext.php +++ /dev/null @@ -1,126 +0,0 @@ -ocPath = rtrim($ocPath, '/') . '/'; - } - - /** - * @Given Maintenance mode is enabled - */ - public function maintenanceModeIsEnabled() { - $this->runOcc(['maintenance:mode', '--on']); - } - - /** - * @Then Maintenance mode is disabled - */ - public function maintenanceModeIsDisabled() { - $this->runOcc(['maintenance:mode', '--off']); - } - - /** @BeforeScenario */ - public function gatherContexts(BeforeScenarioScope $scope) { - $environment = $scope->getEnvironment(); - // this should really be "WebDavContext" - try { - $this->featureContext = $environment->getContext('FeatureContext'); - } catch (ContextNotFoundException) { - $this->featureContext = $environment->getContext('DavFeatureContext'); - } - } - - private function findLastTransferFolderForUser($sourceUser, $targetUser) { - $foundPaths = []; - $results = $this->featureContext->listFolder($targetUser, '', 1); - foreach ($results as $path => $data) { - $path = rawurldecode($path); - $parts = explode(' ', $path); - if (basename($parts[0]) !== 'Transferred') { - continue; - } - if (isset($parts[2]) && $parts[2] === $sourceUser) { - // store timestamp as key - $foundPaths[] = [ - 'date' => strtotime(trim($parts[4], '/')), - 'path' => $path, - ]; - } - } - - if (empty($foundPaths)) { - return null; - } - - usort($foundPaths, function ($a, $b) { - return $a['date'] - $b['date']; - }); - - $davPath = rtrim($this->featureContext->getDavFilesPath($targetUser), '/'); - - $foundPath = end($foundPaths)['path']; - // strip dav path - return substr($foundPath, strlen($davPath) + 1); - } - - /** - * @When /^transferring ownership from "([^"]+)" to "([^"]+)"$/ - */ - public function transferringOwnership($user1, $user2) { - if ($this->runOcc(['files:transfer-ownership', $user1, $user2]) === 0) { - $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2); - } else { - // failure - $this->lastTransferPath = null; - } - } - - /** - * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)"$/ - */ - public function transferringOwnershipPath($path, $user1, $user2) { - $path = '--path=' . $path; - if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2]) === 0) { - $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2); - } else { - // failure - $this->lastTransferPath = null; - } - } - - /** - * @When /^using received transfer folder of "([^"]+)" as dav path$/ - */ - public function usingTransferFolderAsDavPath($user) { - $davPath = $this->featureContext->getDavFilesPath($user); - $davPath = rtrim($davPath, '/') . $this->lastTransferPath; - $this->featureContext->usingDavPath($davPath); - } - - /** - * @Then /^transfer folder name contains "([^"]+)"$/ - */ - public function transferFolderNameContains($text) { - Assert::assertStringContainsString($text, $this->lastTransferPath); - } -} --- a/build/integration/features/bootstrap/CommentsContext.php +++ /dev/null @@ -1,307 +0,0 @@ -baseUrl = substr($testServerUrl, 0, -5); - } - } - - /** - * 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(); - try { - $client->delete( - $this->baseUrl . '/remote.php/webdav/myFileToComment.txt', - [ - 'auth' => [ - 'user0', - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $e->getResponse(); - } - } - - /** - * @param string $path - * @return int - */ - private function getFileIdForPath($path) { - $url = $this->baseUrl . '/remote.php/webdav/' . $path; - $context = stream_context_create([ - 'http' => [ - 'method' => 'PROPFIND', - 'header' => "Authorization: Basic dXNlcjA6MTIzNDU2\r\nContent-Type: application/x-www-form-urlencoded", - 'content' => ' - - - - -' - ] - ]); - - $response = file_get_contents($url, false, $context); - preg_match_all('/\(.*)\<\/oc:fileid\>/', $response, $matches); - return (int)$matches[1][0]; - } - - /** - * @When :user posts a comment with content :content on the file named :fileName it should return :statusCode - * @param string $user - * @param string $content - * @param string $fileName - * @param int $statusCode - * @throws \Exception - */ - public function postsACommentWithContentOnTheFileNamedItShouldReturn($user, $content, $fileName, $statusCode) { - $fileId = $this->getFileIdForPath($fileName); - $this->fileId = (int)$fileId; - $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $fileId . '/'; - - $client = new \GuzzleHttp\Client(); - try { - $res = $client->post( - $url, - [ - 'body' => '{"actorId":"user0","actorDisplayName":"user0","actorType":"users","verb":"comment","message":"' . $content . '","creationDateTime":"Thu, 18 Feb 2016 17:04:18 GMT","objectType":"files"}', - 'auth' => [ - $user, - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $res = $e->getResponse(); - } - - if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); - } - } - - /** - * @Then As :user load all the comments of the file named :fileName it should return :statusCode - * @param string $user - * @param string $fileName - * @param int $statusCode - * @throws \Exception - */ - public function asLoadloadAllTheCommentsOfTheFileNamedItShouldReturn($user, $fileName, $statusCode) { - $fileId = $this->getFileIdForPath($fileName); - $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $fileId . '/'; - - try { - $client = new \GuzzleHttp\Client(); - $res = $client->request( - 'REPORT', - $url, - [ - 'body' => ' - - 200 - 0 - -', - 'auth' => [ - $user, - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $res = $e->getResponse(); - } - - if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); - } - - if ($res->getStatusCode() === 207) { - $service = new Sabre\Xml\Service(); - $this->response = $service->parse($res->getBody()->getContents()); - $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); - } - } - - /** - * @Given As :user sending :verb to :url with - * @param string $user - * @param string $verb - * @param string $url - * @param \Behat\Gherkin\Node\TableNode $body - * @throws \Exception - */ - public function asUserSendingToWith($user, $verb, $url, \Behat\Gherkin\Node\TableNode $body) { - $client = new \GuzzleHttp\Client(); - $options = []; - $options['auth'] = [$user, '123456']; - $fd = $body->getRowsHash(); - $options['form_params'] = $fd; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $client->request($verb, $this->baseUrl . '/ocs/v1.php/' . $url, $options); - } - - /** - * @Then As :user delete the created comment it should return :statusCode - * @param string $user - * @param int $statusCode - * @throws \Exception - */ - public function asDeleteTheCreatedCommentItShouldReturn($user, $statusCode) { - $url = $this->baseUrl . '/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId; - - $client = new \GuzzleHttp\Client(); - try { - $res = $client->delete( - $url, - [ - 'auth' => [ - $user, - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $res = $e->getResponse(); - } - - if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); - } - } - - /** - * @Then the response should contain a property :key with value :value - * @param string $key - * @param string $value - * @throws \Exception - */ - public function theResponseShouldContainAPropertyWithValue($key, $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)) { - if ($singleKey['value'] === $value) { - $found = true; - } - } - } - if ($found === false) { - throw new \Exception("Cannot find property $key with $value"); - } - } - - /** - * @Then the response should contain only :number comments - * @param int $number - * @throws \Exception - */ - public function theResponseShouldContainOnlyComments($number) { - $count = 0; - if ($this->response !== null) { - $count = count($this->response); - } - if ($count !== (int)$number) { - throw new \Exception("Found more comments than $number (" . $count . ')'); - } - } - - /** - * @Then As :user edit the last created comment and set text to :text it should return :statusCode - * @param string $user - * @param string $text - * @param int $statusCode - * @throws \Exception - */ - public function asEditTheLastCreatedCommentAndSetTextToItShouldReturn($user, $text, $statusCode) { - $client = new \GuzzleHttp\Client(); - $options = []; - $options['auth'] = [$user, '123456']; - $options['body'] = ' - - - - ' . $text . ' - - -'; - try { - $res = $client->request('PROPPATCH', $this->baseUrl . '/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $res = $e->getResponse(); - } - - if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); - } - } -} --- a/build/integration/features/bootstrap/ContactsMenu.php +++ /dev/null @@ -1,51 +0,0 @@ -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']); - } -} --- a/build/integration/features/bootstrap/ConversionsContext.php +++ /dev/null @@ -1,62 +0,0 @@ -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)); - } -} --- a/build/integration/features/bootstrap/DavFeatureContext.php +++ /dev/null @@ -1,26 +0,0 @@ -deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); - } -} --- a/build/integration/features/bootstrap/Download.php +++ /dev/null @@ -1,155 +0,0 @@ -downloadedFile = null; - } - - /** - * @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', "/remote.php/dav/files/$user/$folder?accept=zip&files=[" . $entries . ']'); - $this->theHTTPStatusCodeShouldBe('200'); - } - - private function getDownloadedFile() { - $this->downloadedFile = ''; - - /** @var StreamInterface */ - $body = $this->response->getBody(); - while (!$body->eof()) { - $this->downloadedFile .= $body->read(8192); - } - $body->close(); - } - - /** - * @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' - ); - } - - /** - * @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' - ); - } - - /** - * @Then the downloaded zip file contains a file named :fileName with the contents of :sourceFileName from :user data - */ - public function theDownloadedZipFileContainsAFileNamedWithTheContentsOfFromData($fileName, $sourceFileName, $user) { - $fileHeaderRegExp = '/'; - $fileHeaderRegExp .= "\x50\x4B\x03\x04"; // Local file header signature - $fileHeaderRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size" - $fileHeaderRegExp .= preg_quote(pack('v', strlen($fileName)), '/'); // File name length - $fileHeaderRegExp .= '(.{2,2})'; // Get "extra field length" - $fileHeaderRegExp .= preg_quote($fileName, '/'); // File name - $fileHeaderRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match - - // assertRegExp is not used to prevent the whole file from being printed - // 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' - ); - - $extraFieldLength = unpack('vextraFieldLength', $matches[1])['extraFieldLength']; - $expectedFileContents = file_get_contents($this->getDataDirectory() . "/$user/files" . $sourceFileName); - - $fileHeaderAndContentRegExp = '/'; - $fileHeaderAndContentRegExp .= "\x50\x4B\x03\x04"; // Local file header signature - $fileHeaderAndContentRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size" - $fileHeaderAndContentRegExp .= preg_quote(pack('v', strlen($fileName)), '/'); // File name length - $fileHeaderAndContentRegExp .= '.{2,2}'; // Ignore "extra field length" - $fileHeaderAndContentRegExp .= preg_quote($fileName, '/'); // File name - $fileHeaderAndContentRegExp .= '.{' . $extraFieldLength . ',' . $extraFieldLength . '}'; // Ignore "extra field" - $fileHeaderAndContentRegExp .= preg_quote($expectedFileContents, '/'); // File contents - $fileHeaderAndContentRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match - - // assertRegExp is not used to prevent the whole file from being printed - // 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' - ); - } - - /** - * @Then the downloaded zip file contains a folder named :folderName - */ - public function theDownloadedZipFileContainsAFolderNamed($folderName) { - $folderHeaderRegExp = '/'; - $folderHeaderRegExp .= "\x50\x4B\x03\x04"; // Local file header signature - $folderHeaderRegExp .= '.{22,22}'; // Ignore from "version needed to extract" to "uncompressed size" - $folderHeaderRegExp .= preg_quote(pack('v', strlen($folderName)), '/'); // File name length - $folderHeaderRegExp .= '.{2,2}'; // Ignore "extra field length" - $folderHeaderRegExp .= preg_quote($folderName, '/'); // File name - $folderHeaderRegExp .= '/s'; // PCRE_DOTALL, so all characters (including bytes that happen to be new line characters) match - - // assertRegExp is not used to prevent the whole file from being printed - // in case of error. - Assert::assertEquals( - 1, preg_match($folderHeaderRegExp, $this->downloadedFile), - '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' - ); - } -} --- a/build/integration/features/bootstrap/ExternalStorage.php +++ /dev/null @@ -1,123 +0,0 @@ -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); - } -} --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ /dev/null @@ -1,163 +0,0 @@ -server interaction - * The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt - */ - -class fakeSMTP { - public $logFile = false; - public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready'; - - public function __construct($fd) { - $this->mail = []; - $this->mail['ipaddress'] = false; - $this->mail['emailSender'] = ''; - $this->mail['emailRecipients'] = []; - $this->mail['emailSubject'] = false; - $this->mail['rawEmail'] = false; - $this->mail['emailHeaders'] = false; - $this->mail['emailBody'] = false; - - $this->fd = $fd; - } - - public function receive() { - $hasValidFrom = false; - $hasValidTo = false; - $receivingData = false; - $header = true; - $this->reply('220 ' . $this->serverHello); - $this->mail['ipaddress'] = $this->detectIP(); - while ($data = fgets($this->fd)) { - $data = preg_replace('@\r\n@', "\n", $data); - - if (!$receivingData) { - $this->log($data); - } - - if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) { - if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) { - $this->mail['emailSender'] = $match[1]; - $this->reply('250 2.1.0 Ok'); - $hasValidFrom = true; - } else { - $this->reply('551 5.1.7 Bad sender address syntax'); - } - } elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) { - if (!$hasValidFrom) { - $this->reply('503 5.5.1 Error: need MAIL command'); - } else { - if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) { - array_push($this->mail['emailRecipients'], $match[1]); - $this->reply('250 2.1.5 Ok'); - $hasValidTo = true; - } else { - $this->reply('501 5.1.3 Bad recipient address syntax ' . $match[1]); - } - } - } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) { - $this->reply('250 2.0.0 Ok'); - $hasValidFrom = false; - $hasValidTo = false; - } 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]); - } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) { - if (!$hasValidTo) { - $this->reply('503 5.5.1 Error: need RCPT command'); - } else { - $this->reply('354 Ok Send data ending with .'); - $receivingData = true; - } - } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) { - $this->reply('250 HELO ' . $this->mail['ipaddress']); - } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) { - break; - } elseif (!$receivingData) { - //~ $this->reply('250 Ok'); - $this->reply('502 5.5.2 Error: command not recognized'); - } 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)); - $splitmail = explode("\n\n", $this->mail['rawEmail'], 2); - if (count($splitmail) === 2) { - $this->mail['emailHeaders'] = $splitmail[0]; - $this->mail['emailBody'] = $splitmail[1]; - $headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders'])); - $headerlines = explode("\n", $headers); - for ($i = 0; $i < count($headerlines); $i++) { - if (preg_match('/^Subject: (.*)/i', $headerlines[$i], $matches)) { - $this->mail['emailSubject'] = trim($matches[1]); - } - } - } else { - $this->mail['emailBody'] = $splitmail[0]; - } - set_time_limit(5); // Just run the exit to prevent open threads / abuse - } elseif ($receivingData) { - $this->mail['rawEmail'] .= $data; - } - } - /* Say good bye */ - $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); - } - } - - private function reply($s) { - $this->log("REPLY:$s"); - fwrite($this->fd, $s . "\r\n"); - } - - private function detectIP() { - $raw = explode(':', stream_socket_get_name($this->fd, true)); - return $raw[0]; - } - - private function validateEmail($email) { - return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email)); - } - - private function generateRandom($length = 8) { - $password = ''; - $possible = '2346789BCDFGHJKLMNPQRTVWXYZ'; - $maxlength = strlen($possible); - $i = 0; - for ($i = 0; $i < $length; $i++) { - $char = substr($possible, mt_rand(0, $maxlength - 1), 1); - if (!strstr($password, $char)) { - $password .= $char; - } - } - return $password; - } -} - -$socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr); -if (!$socket) { - exit(); -} - -while ($fd = stream_socket_accept($socket)) { - $fakeSMTP = new fakeSMTP($fd); - $fakeSMTP->receive(); -} - -fclose($socket); --- a/build/integration/features/bootstrap/FeatureContext.php +++ /dev/null @@ -1,35 +0,0 @@ -deleteServerConfig('bruteForce', 'whitelist_0'); - $this->deleteServerConfig('bruteForce', 'whitelist_1'); - $this->deleteServerConfig('bruteforcesettings', 'apply_allowlist_to_ratelimit'); - $this->deleteServerConfig('core', 'shareapi_exclude_groups'); - $this->deleteServerConfig('core', 'shareapi_exclude_groups_list'); - $this->deleteServerConfig('dav', 'system_addressbook_exposed'); - } -} --- a/build/integration/features/bootstrap/FederationContext.php +++ /dev/null @@ -1,209 +0,0 @@ -/dev/null & echo $!'); - } - - /** - * @BeforeScenario - */ - public function cleanupRemoteStorages(): void { - // 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)"$/ - * - * @param 'LOCAL'|'REMOTE' $sharerServer "LOCAL" or "REMOTE" - * @param 'LOCAL'|'REMOTE' $shareeServer - */ - public function federateSharing(string $sharerUser, string $sharerServer, string $sharerPath, string $shareeUser, string $shareeServer): void { - if ($shareeServer === 'REMOTE') { - $shareWith = "$shareeUser@" . substr($this->remoteBaseUrl, 0, -4); - } else { - $shareWith = "$shareeUser@" . substr($this->localBaseUrl, 0, -4); - } - $previous = $this->usingServer($sharerServer); - $this->createShare($sharerUser, $sharerPath, 6, $shareWith, null, null, null); - $this->usingServer($previous); - } - - /** - * @Given /^User "([^"]*)" from server "(LOCAL|REMOTE)" shares "([^"]*)" with group "([^"]*)" from server "(LOCAL|REMOTE)"$/ - * - * @param 'LOCAL'|'REMOTE' $sharerServer "LOCAL" or "REMOTE" - * @param 'LOCAL'|'REMOTE' $shareeServer - */ - public function federateGroupSharing(string $sharerUser, string $sharerServer, string $sharerPath, string $shareeGroup, string $shareeServer): void { - if ($shareeServer === 'REMOTE') { - $shareWith = "$shareeGroup@" . substr($this->remoteBaseUrl, 0, -4); - } else { - $shareWith = "$shareeGroup@" . substr($this->localBaseUrl, 0, -4); - } - $previous = $this->usingServer($sharerServer); - $this->createShare($sharerUser, $sharerPath, 9, $shareWith, null, null, null); - $this->usingServer($previous); - } - - /** - * @Then remote share :count is returned with - */ - public function remoteShareXIsReturnedWith(int $number, TableNode $body): void { - $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$/ - */ - public function acceptLastPendingShare(string $user, string $server): void { - $previous = $this->usingServer($server); - $this->asAn($user); - $this->sendingToWith('GET', '/apps/files_sharing/api/v1/remote_shares/pending', null); - $this->theHTTPStatusCodeShouldBe('200'); - $this->theOCSStatusCodeShouldBe('100'); - $shares = simplexml_load_string($this->response->getBody())->data[0]->element; - $share_id = $shares[count($shares) - 1]->id; - $this->sendingToWith('POST', "/apps/files_sharing/api/v1/remote_shares/pending/{$share_id}", null); - $this->theHTTPStatusCodeShouldBe('200'); - $this->theOCSStatusCodeShouldBe('100'); - $this->usingServer($previous); - - $this->lastAcceptedRemoteShareId = $share_id; - } - - /** - * @When /^user "([^"]*)" deletes last accepted remote share$/ - */ - public function deleteLastAcceptedRemoteShare(string $user): void { - $this->asAn($user); - $this->sendingToWith('DELETE', '/apps/files_sharing/api/v1/remote_shares/' . $this->lastAcceptedRemoteShareId, null); - } - - /** - * @When /^remote server is stopped$/ - */ - public function remoteServerIsStopped(): void { - if (self::$phpFederatedServerPid === '') { - return; - } - - exec('kill ' . self::$phpFederatedServerPid); - - self::$phpFederatedServerPid = ''; - } - - /** - * @BeforeScenario @TrustedFederation - */ - public function theServersAreTrustingEachOther(): void { - $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(): void { - $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(): void { - $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'); - } -} --- a/build/integration/features/bootstrap/FilesDropContext.php +++ /dev/null @@ -1,91 +0,0 @@ -lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data[0]->token; - } - - $base = rtrim(substr($this->baseUrl, 0, -4), '/'); - $fullUrl = $base . str_replace('//', '/', "/public.php/dav/files/$token/$path"); - - $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest', - ]; - - if ($nickname) { - $options['headers']['X-NC-NICKNAME'] = $nickname; - } - - $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); - - try { - $this->response = $client->request('PUT', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @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, $nickname = null) { - $client = new Client(); - $options = []; - if (count($this->lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data[0]->token; - } - - $base = rtrim(substr($this->baseUrl, 0, -4), '/'); - $fullUrl = $base . str_replace('//', '/', "/public.php/dav/files/$token/$folder"); - - $options['headers'] = [ - '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); - } -} --- a/build/integration/features/bootstrap/FilesRemindersContext.php +++ /dev/null @@ -1,95 +0,0 @@ -getFileIdForPath($this->currentUser, $path); - $this->sendRequestForJSON( - 'PUT', - '/apps/files_reminders/api/v1/' . $fileId, - ['dueDate' => $dueDate], - ['OCS-APIREQUEST' => 'true'] - ); - } - - /** - * @Then the user sees the reminder for :path is set to :dueDate - */ - public function retrievingTheReminderForFile($path, $dueDate) { - $fileId = $this->getFileIdForPath($this->currentUser, $path); - $this->sendRequestForJSON( - 'GET', - '/apps/files_reminders/api/v1/' . $fileId, - null, - ['OCS-APIREQUEST' => 'true'] - ); - $response = $this->getDueDateFromOCSResponse(); - Assert::assertEquals($dueDate, $response); - } - - /** - * @Then the user sees the reminder for :path is not set - */ - public function retrievingTheReminderForFileIsNotSet($path) { - $fileId = $this->getFileIdForPath($this->currentUser, $path); - $this->sendRequestForJSON( - 'GET', - '/apps/files_reminders/api/v1/' . $fileId, - null, - ['OCS-APIREQUEST' => 'true'] - ); - $response = $this->getDueDateFromOCSResponse(); - Assert::assertNull($response); - } - - /** - * @When the user removes the reminder for :path - */ - public function removingTheReminderForFile($path) { - $fileId = $this->getFileIdForPath($this->currentUser, $path); - $this->sendRequestForJSON( - 'DELETE', - '/apps/files_reminders/api/v1/' . $fileId, - null, - ['OCS-APIREQUEST' => 'true'] - ); - } - - /** - * Check the due date from OCS response - */ - private function getDueDateFromOCSResponse(): ?string { - if ($this->response === null) { - throw new \RuntimeException('No response available'); - } - - $body = (string)$this->response->getBody(); - if (str_starts_with($body, '<')) { - $body = simplexml_load_string($body); - if ($body === false) { - throw new \RuntimeException('Could not parse OCS response, body is not valid XML'); - } - return $body->data->dueDate; - } - - $body = json_decode($body, true); - return $body['ocs']['data']['dueDate'] ?? null; - } -} --- a/build/integration/features/bootstrap/GuestsContext.php +++ /dev/null @@ -1,87 +0,0 @@ - ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..', $env); - self::$lastStdOut = stream_get_contents($pipes[1]); - self::$lastCode = proc_close($process); - - return self::$lastCode; - } - - #[\Behat\Step\Given('/^user "([^"]*)" is a guest account user$/')] - public function createGuestUser(string $email): void { - self::runOcc([ - 'user:delete', - $email, - ]); - - $lastCode = self::runOcc([ - 'config:app:set', - 'guests', - 'hash_user_ids', - '--value=false', - '--type=boolean', - ]); - \PHPUnit\Framework\Assert::assertEquals(0, $lastCode); - - $lastCode = self::runOcc([ - 'guests:add', - // creator user - 'admin', - // email - $email, - '--display-name', - $email . '-displayname', - '--password-from-env', - ], [ - 'OC_PASS' => self::TEST_PASSWORD, - ]); - \PHPUnit\Framework\Assert::assertEquals(0, $lastCode, 'Guest creation succeeded for ' . $email); - } - -} --- a/build/integration/features/bootstrap/LDAPContext.php +++ /dev/null @@ -1,198 +0,0 @@ -configID === null) { - return; - } - $this->disableLDAPConfiguration(); # via occ in case of big config issues - $this->asAn('admin'); - $this->sendingTo('DELETE', $this->apiUrl . '/' . $this->configID); - } - - /** - * @Given /^the response should contain a tag "([^"]*)"$/ - */ - public function theResponseShouldContainATag($arg1) { - $configID = simplexml_load_string($this->response->getBody())->data[0]->$arg1; - Assert::assertInstanceOf(SimpleXMLElement::class, $configID[0]); - } - - /** - * @Given /^creating an LDAP configuration at "([^"]*)"$/ - */ - public function creatingAnLDAPConfigurationAt($apiUrl) { - $this->apiUrl = $apiUrl; - $this->sendingToWith('POST', $this->apiUrl, null); - $configElements = simplexml_load_string($this->response->getBody())->data[0]->configID; - $this->configID = $configElements[0]; - } - - /** - * @When /^deleting the LDAP configuration$/ - */ - public function deletingTheLDAPConfiguration() { - $this->sendingToWith('DELETE', $this->apiUrl . '/' . $this->configID, null); - } - - /** - * @Given /^the response should contain a tag "([^"]*)" with value "([^"]*)"$/ - */ - public function theResponseShouldContainATagWithValue($tagName, $expectedValue) { - $data = simplexml_load_string($this->response->getBody())->data[0]->$tagName; - Assert::assertEquals($expectedValue, $data[0]); - } - - /** - * @When /^getting the LDAP configuration with showPassword "([^"]*)"$/ - */ - public function gettingTheLDAPConfigurationWithShowPassword($showPassword) { - $this->sendingToWith( - 'GET', - $this->apiUrl . '/' . $this->configID . '?showPassword=' . $showPassword, - null - ); - } - - /** - * @Given /^setting the LDAP configuration to$/ - */ - public function settingTheLDAPConfigurationTo(TableNode $configData) { - $this->sendingToWith('PUT', $this->apiUrl . '/' . $this->configID, $configData); - } - - /** - * @Given /^having a valid LDAP configuration$/ - */ - public function havingAValidLDAPConfiguration() { - $this->asAn('admin'); - $this->creatingAnLDAPConfigurationAt('/apps/user_ldap/api/v1/config'); - $data = new TableNode([ - ['configData[ldapHost]', getenv('LDAP_HOST') ?: 'openldap'], - ['configData[ldapPort]', '389'], - ['configData[ldapBase]', 'dc=nextcloud,dc=ci'], - ['configData[ldapAgentName]', 'cn=admin,dc=nextcloud,dc=ci'], - ['configData[ldapAgentPassword]', 'admin'], - ['configData[ldapUserFilter]', '(&(objectclass=inetorgperson))'], - ['configData[ldapLoginFilter]', '(&(objectclass=inetorgperson)(uid=%uid))'], - ['configData[ldapUserDisplayName]', 'displayname'], - ['configData[ldapGroupDisplayName]', 'cn'], - ['configData[ldapEmailAttribute]', 'mail'], - ['configData[ldapConfigurationActive]', '1'], - ]); - $this->settingTheLDAPConfigurationTo($data); - $this->asAn(''); - } - - /** - * @Given /^looking up details for the first result matches expectations$/ - * @param TableNode $expectations - */ - public function lookingUpDetailsForTheFirstResult(TableNode $expectations) { - $userResultElements = simplexml_load_string($this->response->getBody())->data[0]->users[0]->element; - $userResults = json_decode(json_encode($userResultElements), 1); - $userId = array_shift($userResults); - - $this->sendingTo('GET', '/cloud/users/' . $userId); - $this->theRecordFieldsShouldMatch($expectations); - } - - /** - * @Given /^modify LDAP configuration$/ - */ - public function modifyLDAPConfiguration(TableNode $table) { - $originalAsAn = $this->currentUser; - $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)); - $this->asAn($originalAsAn); - } - - /** - * @Given /^the "([^"]*)" result should match$/ - */ - public function theGroupResultShouldMatch(string $type, TableNode $expectations) { - $listReturnedElements = simplexml_load_string($this->response->getBody())->data[0]->$type[0]->element; - $extractedIDsArray = json_decode(json_encode($listReturnedElements), 1); - foreach ($expectations->getRows() as $expectation) { - if ((int)$expectation[1] === 1) { - Assert::assertContains($expectation[0], $extractedIDsArray); - } else { - Assert::assertNotContains($expectation[0], $extractedIDsArray); - } - } - } - - /** - * @Given /^Expect ServerException on failed web login as "([^"]*)"$/ - */ - public function expectServerExceptionOnFailedWebLoginAs($login) { - try { - $this->loggingInUsingWebAs($login); - } catch (\GuzzleHttp\Exception\ServerException $e) { - Assert::assertEquals(500, $e->getResponse()->getStatusCode()); - return; - } - Assert::assertTrue(false, 'expected Exception not received'); - } - - /** - * @Given /^the "([^"]*)" result should contain "([^"]*)" of$/ - */ - public function theResultShouldContainOf($type, $expectedCount, TableNode $expectations) { - $listReturnedElements = simplexml_load_string($this->response->getBody())->data[0]->$type[0]->element; - $extractedIDsArray = json_decode(json_encode($listReturnedElements), 1); - $uidsFound = 0; - foreach ($expectations->getRows() as $expectation) { - if (in_array($expectation[0], $extractedIDsArray)) { - $uidsFound++; - } - } - Assert::assertSame((int)$expectedCount, $uidsFound); - } - - /** - * @Given /^the record's fields should match$/ - */ - public function theRecordFieldsShouldMatch(TableNode $expectations) { - foreach ($expectations->getRowsHash() as $k => $v) { - $value = (string)simplexml_load_string($this->response->getBody())->data[0]->$k; - Assert::assertEquals($v, $value, "got $value"); - } - - $backend = (string)simplexml_load_string($this->response->getBody())->data[0]->backend; - Assert::assertEquals('LDAP', $backend); - } - - public function disableLDAPConfiguration() { - $configKey = $this->configID . 'ldap_configuration_active'; - $this->invokingTheCommand('config:app:set user_ldap ' . $configKey . ' --value="0"'); - } - - protected function resetAppConfigs() { - // not implemented - } -} --- a/build/integration/features/bootstrap/Mail.php +++ /dev/null @@ -1,39 +0,0 @@ -fakeSmtpServerPid) { - return; - } - - exec('kill ' . $this->fakeSmtpServerPid); - - $this->runOcc(['config:system:delete', 'mail_smtpport']); - } - - /** - * @Given /^dummy mail server is listening$/ - */ - public function dummyMailServerIsListening() { - // Default smtpport (25) is restricted for regular users, so the - // FakeSMTP uses 2525 instead. - $this->runOcc(explode(' ', 'config:system:set mail_smtpport --value=2525 --type integer')); - - $this->fakeSmtpServerPid = exec('php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!'); - } -} --- a/build/integration/features/bootstrap/MetadataContext.php +++ /dev/null @@ -1,124 +0,0 @@ -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 = ' - - - - ' . $metadataValue . ' - - -'; - - $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 = ' - - - - - - -'; - - $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 = ' - - - - -'; - - $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 = ' - - - - -'; - - $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; - } -} --- a/build/integration/features/bootstrap/OccRunner.php +++ /dev/null @@ -1,57 +0,0 @@ - ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $this->ocPath); - if ($inputString !== '') { - fwrite($pipes[0], $inputString . "\n"); - fclose($pipes[0]); - } - $this->lastStdOut = stream_get_contents($pipes[1]); - $this->lastStdErr = stream_get_contents($pipes[2]); - $this->lastCode = proc_close($process); - - // Clean opcode cache - $client = new GuzzleHttp\Client(); - $client->request('GET', 'http://localhost:8080/apps/testing/clean_opcode_cache.php'); - - return $this->lastCode; - } -} --- a/build/integration/features/bootstrap/PrincipalPropertySearchContext.php +++ /dev/null @@ -1,140 +0,0 @@ -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' => ' - - - - - - ' . $match . ' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -', - '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 . '"'); - } - } -} --- a/build/integration/features/bootstrap/Provisioning.php +++ /dev/null @@ -1,1038 +0,0 @@ -asAn('admin'); - - foreach ($this->appsToEnableAfterScenario as $app) { - $this->sendingTo('POST', '/cloud/apps/' . $app); - } - - foreach ($this->appsToDisableAfterScenario as $app) { - $this->sendingTo('DELETE', '/cloud/apps/' . $app); - } - } - - /** - * @Given /^user "([^"]*)" exists$/ - * @param string $user - */ - public function assureUserExists($user) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->creatingTheUser($user); - $this->currentUser = $previous_user; - } - $this->userExists($user); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^user "([^"]*)" with displayname "((?:[^"]|\\")*)" exists$/ - * @param string $user - */ - public function assureUserWithDisplaynameExists($user, $displayname) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->creatingTheUser($user, $displayname); - $this->currentUser = $previous_user; - } - $this->userExists($user); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^user "([^"]*)" does not exist$/ - * @param string $user - */ - public function userDoesNotExist($user) { - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - return; - } - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->deletingTheUser($user); - $this->currentUser = $previous_user; - try { - $this->userExists($user); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - } - } - - public function creatingTheUser($user, $displayname = '') { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - - $options['form_params'] = [ - 'userid' => $user, - 'password' => '123456' - ]; - if ($displayname !== '') { - $options['form_params']['displayName'] = $displayname; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->post($fullUrl, $options); - if ($this->currentServer === 'LOCAL') { - $this->createdUsers[$user] = $user; - } elseif ($this->currentServer === 'REMOTE') { - $this->createdRemoteUsers[$user] = $user; - } - - //Quick hack to login once with the current user - $options2 = [ - 'auth' => [$user, '123456'], - ]; - $options2['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $url = $fullUrl . '/' . $user; - $client->get($url, $options2); - } - - /** - * @Then /^user "([^"]*)" has$/ - * - * @param string $user - * @param TableNode|null $settings - */ - public function userHasSetting($user, $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['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::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); - /* Sort both arrays as order is not important */ - sort($fieldsArray); - sort($expectedFields); - Assert::assertEquals($expectedFields, $fieldsArray); - } - - /** - * @Then /^search users by phone for region "([^"]*)" with$/ - * - * @param string $user - * @param TableNode|null $settings - */ - public function searchUserByPhone($region, TableNode $searchTable) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $search = []; - foreach ($searchTable->getRows() as $row) { - if (!isset($search[$row[0]])) { - $search[$row[0]] = []; - } - $search[$row[0]][] = $row[1]; - } - - $options['form_params'] = [ - 'location' => $region, - 'search' => $search, - ]; - - $this->response = $client->post($fullUrl, $options); - } - - public function createUser($user) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->creatingTheUser($user); - $this->userExists($user); - $this->currentUser = $previous_user; - } - - public function deleteUser($user) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->deletingTheUser($user); - $this->userDoesNotExist($user); - $this->currentUser = $previous_user; - } - - public function createGroup($group) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->creatingTheGroup($group); - $this->groupExists($group); - $this->currentUser = $previous_user; - } - - public function deleteGroup($group) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->deletingTheGroup($group); - $this->groupDoesNotExist($group); - $this->currentUser = $previous_user; - } - - public function userExists($user) { - $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true' - ]; - - $this->response = $client->get($fullUrl, $options); - } - - /** - * @Then /^check that user "([^"]*)" belongs to group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function checkThatUserBelongsToGroup($user, $group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - $respondedArray = $this->getArrayOfGroupsResponded($this->response); - sort($respondedArray); - Assert::assertContains($group, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - public function userBelongsToGroup($user, $group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - $respondedArray = $this->getArrayOfGroupsResponded($this->response); - - if (array_key_exists($group, $respondedArray)) { - return true; - } else { - return false; - } - } - - /** - * @Given /^user "([^"]*)" belongs to group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function assureUserBelongsToGroup($user, $group) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - - if (!$this->userBelongsToGroup($user, $group)) { - $this->addingUserToGroup($user, $group); - } - - $this->checkThatUserBelongsToGroup($user, $group); - $this->currentUser = $previous_user; - } - - /** - * @Given /^user "([^"]*)" does not belong to group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function userDoesNotBelongToGroup($user, $group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - $groups = [$group]; - $respondedArray = $this->getArrayOfGroupsResponded($this->response); - Assert::assertNotEqualsCanonicalizing($groups, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @When /^creating the group "([^"]*)"$/ - * @param string $group - */ - public function creatingTheGroup($group) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - - $options['form_params'] = [ - 'groupid' => $group, - ]; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->post($fullUrl, $options); - if ($this->currentServer === 'LOCAL') { - $this->createdGroups[$group] = $group; - } elseif ($this->currentServer === 'REMOTE') { - $this->createdRemoteGroups[$group] = $group; - } - } - - /** - * @When /^assure user "([^"]*)" is disabled$/ - */ - public function assureUserIsDisabled($user) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/disable"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - // TODO: fix hack - $options['form_params'] = [ - 'foo' => 'bar' - ]; - - $this->response = $client->put($fullUrl, $options); - } - - /** - * @When /^Deleting the user "([^"]*)"$/ - * @param string $user - */ - public function deletingTheUser($user) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->delete($fullUrl, $options); - } - - /** - * @When /^Deleting the group "([^"]*)"$/ - * @param string $group - */ - public function deletingTheGroup($group) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/$group"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->delete($fullUrl, $options); - - if ($this->currentServer === 'LOCAL') { - unset($this->createdGroups[$group]); - } elseif ($this->currentServer === 'REMOTE') { - unset($this->createdRemoteGroups[$group]); - } - } - - /** - * @Given /^Add user "([^"]*)" to the group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function addUserToGroup($user, $group) { - $this->userExists($user); - $this->groupExists($group); - $this->addingUserToGroup($user, $group); - } - - /** - * @When /^User "([^"]*)" is added to the group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function addingUserToGroup($user, $group) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/groups"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $options['form_params'] = [ - 'groupid' => $group, - ]; - - $this->response = $client->post($fullUrl, $options); - } - - public function groupExists($group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - } - - /** - * @Given /^group "([^"]*)" exists$/ - * @param string $group - */ - public function assureGroupExists($group) { - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->creatingTheGroup($group); - $this->currentUser = $previous_user; - } - $this->groupExists($group); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^group "([^"]*)" does not exist$/ - * @param string $group - */ - public function groupDoesNotExist($group) { - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - return; - } - $previous_user = $this->currentUser; - $this->currentUser = 'admin'; - $this->deletingTheGroup($group); - $this->currentUser = $previous_user; - try { - $this->groupExists($group); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - Assert::assertEquals(404, $ex->getResponse()->getStatusCode()); - } - } - - /** - * @Given /^user "([^"]*)" is subadmin of group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function userIsSubadminOfGroup($user, $group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - $respondedArray = $this->getArrayOfSubadminsResponded($this->response); - sort($respondedArray); - Assert::assertContains($user, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^Assure user "([^"]*)" is subadmin of group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function assureUserIsSubadminOfGroup($user, $group) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['form_params'] = [ - 'groupid' => $group - ]; - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $this->response = $client->post($fullUrl, $options); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^user "([^"]*)" is not a subadmin of group "([^"]*)"$/ - * @param string $user - * @param string $group - */ - public function userIsNotSubadminOfGroup($user, $group) { - $fullUrl = $this->baseUrl . "v2.php/cloud/groups/$group/subadmins"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - $respondedArray = $this->getArrayOfSubadminsResponded($this->response); - sort($respondedArray); - Assert::assertNotContains($user, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Then /^users returned are$/ - * @param TableNode|null $usersList - */ - public function theUsersShouldBe($usersList) { - if ($usersList instanceof TableNode) { - $users = $usersList->getRows(); - $usersSimplified = $this->simplifyArray($users); - $respondedArray = $this->getArrayOfUsersResponded($this->response); - Assert::assertEqualsCanonicalizing($usersSimplified, $respondedArray); - } - } - - /** - * @Then /^phone matches returned are$/ - * @param TableNode|null $usersList - */ - public function thePhoneUsersShouldBe($usersList) { - if ($usersList instanceof TableNode) { - $users = $usersList->getRowsHash(); - $listCheckedElements = simplexml_load_string($this->response->getBody())->data; - $respondedArray = json_decode(json_encode($listCheckedElements), true); - Assert::assertEquals($users, $respondedArray); - } - } - - /** - * @Then /^detailed users returned are$/ - * @param TableNode|null $usersList - */ - public function theDetailedUsersShouldBe($usersList) { - if ($usersList instanceof TableNode) { - $users = $usersList->getRows(); - $usersSimplified = $this->simplifyArray($users); - $respondedArray = $this->getArrayOfDetailedUsersResponded($this->response); - $respondedArray = array_keys($respondedArray); - Assert::assertEquals($usersSimplified, $respondedArray); - } - } - - /** - * @Then /^groups returned are$/ - * @param TableNode|null $groupsList - */ - public function theGroupsShouldBe($groupsList) { - if ($groupsList instanceof TableNode) { - $groups = $groupsList->getRows(); - $groupsSimplified = $this->simplifyArray($groups); - $respondedArray = $this->getArrayOfGroupsResponded($this->response); - Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray); - } - } - - /** - * @Then /^subadmin groups returned are$/ - * @param TableNode|null $groupsList - */ - public function theSubadminGroupsShouldBe($groupsList) { - if ($groupsList instanceof TableNode) { - $groups = $groupsList->getRows(); - $groupsSimplified = $this->simplifyArray($groups); - $respondedArray = $this->getArrayOfSubadminsResponded($this->response); - Assert::assertEqualsCanonicalizing($groupsSimplified, $respondedArray); - } - } - - /** - * @Then /^apps returned are$/ - * @param TableNode|null $appList - */ - public function theAppsShouldBe($appList) { - if ($appList instanceof TableNode) { - $apps = $appList->getRows(); - $appsSimplified = $this->simplifyArray($apps); - $respondedArray = $this->getArrayOfAppsResponded($this->response); - Assert::assertEqualsCanonicalizing($appsSimplified, $respondedArray); - } - } - - /** - * @Then /^subadmin users returned are$/ - * @param TableNode|null $groupsList - */ - public function theSubadminUsersShouldBe($groupsList) { - $this->theSubadminGroupsShouldBe($groupsList); - } - - /** - * Parses the xml answer to get the array of users returned. - * - * @param ResponseInterface $resp - * @return array - */ - public function getArrayOfUsersResponded($resp) { - $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->users[0]->element; - $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1); - return $extractedElementsArray; - } - - /** - * Parses the xml answer to get the array of detailed users returned. - * - * @param ResponseInterface $resp - * @return array - */ - public function getArrayOfDetailedUsersResponded($resp) { - $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->users; - $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1); - return $extractedElementsArray; - } - - /** - * Parses the xml answer to get the array of groups returned. - * - * @param ResponseInterface $resp - * @return array - */ - public function getArrayOfGroupsResponded($resp) { - $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->groups[0]->element; - $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1); - return $extractedElementsArray; - } - - /** - * Parses the xml answer to get the array of apps returned. - * - * @param ResponseInterface $resp - * @return array - */ - public function getArrayOfAppsResponded($resp) { - $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->apps[0]->element; - $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1); - return $extractedElementsArray; - } - - /** - * Parses the xml answer to get the array of subadmins returned. - * - * @param ResponseInterface $resp - * @return array - */ - public function getArrayOfSubadminsResponded($resp) { - $listCheckedElements = simplexml_load_string($resp->getBody())->data[0]->element; - $extractedElementsArray = json_decode(json_encode($listCheckedElements), 1); - return $extractedElementsArray; - } - - /** - * @Given /^app "([^"]*)" enabled state will be restored once the scenario finishes$/ - * @param string $app - */ - public function appEnabledStateWillBeRestoredOnceTheScenarioFinishes($app) { - $previousUser = $this->currentUser; - $this->currentUser = 'admin'; - - if (in_array($app, $this->getAppsWithFilter('enabled'))) { - $this->appsToEnableAfterScenario[] = $app; - } elseif (in_array($app, $this->getAppsWithFilter('disabled'))) { - $this->appsToDisableAfterScenario[] = $app; - } - - // Apps that were not installed before the scenario will not be - // disabled nor uninstalled after the scenario. - - $this->currentUser = $previousUser; - } - - private function getAppsWithFilter($filter) { - $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=' . $filter; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - return $this->getArrayOfAppsResponded($this->response); - } - - /** - * @Given /^app "([^"]*)" is disabled$/ - * @param string $app - */ - public function appIsDisabled($app) { - $respondedArray = $this->getAppsWithFilter('disabled'); - Assert::assertContains($app, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^app "([^"]*)" is enabled$/ - * @param string $app - */ - public function appIsEnabled($app) { - $respondedArray = $this->getAppsWithFilter('enabled'); - Assert::assertContains($app, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Given /^app "([^"]*)" is not enabled$/ - * - * Checks that the app is disabled or not installed. - * - * @param string $app - */ - public function appIsNotEnabled($app) { - $respondedArray = $this->getAppsWithFilter('enabled'); - Assert::assertNotContains($app, $respondedArray); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @Then /^user "([^"]*)" is disabled$/ - * @param string $user - */ - public function userIsDisabled($user) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - // false in xml is empty - Assert::assertTrue(empty(simplexml_load_string($this->response->getBody())->data[0]->enabled)); - } - - /** - * @Then /^user "([^"]*)" is enabled$/ - * @param string $user - */ - public function userIsEnabled($user) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - - $this->response = $client->get($fullUrl, $options); - // boolean to string is integer - Assert::assertEquals('1', simplexml_load_string($this->response->getBody())->data[0]->enabled); - } - - /** - * @Given user :user has a quota of :quota - * @param string $user - * @param string $quota - */ - public function userHasAQuotaOf($user, $quota) { - $body = new TableNode([ - 0 => ['key', 'quota'], - 1 => ['value', $quota], - ]); - - // method used from BasicStructure trait - $this->sendingToWith('PUT', '/cloud/users/' . $user, $body); - } - - /** - * @Given user :user has unlimited quota - * @param string $user - */ - public function userHasUnlimitedQuota($user) { - $this->userHasAQuotaOf($user, 'none'); - } - - /** - * Returns home path of the given user - * - * @param string $user - */ - public function getUserHome($user) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; - $client = new Client(); - $options = []; - $options['auth'] = $this->adminUser; - $this->response = $client->get($fullUrl, $options); - return simplexml_load_string($this->response->getBody())->data[0]->home; - } - - /** - * @BeforeScenario - * @AfterScenario - */ - public function cleanupUsers() { - $previousServer = $this->currentServer; - $this->usingServer('LOCAL'); - foreach ($this->createdUsers as $user) { - $this->deleteUser($user); - } - $this->usingServer('REMOTE'); - foreach ($this->createdRemoteUsers as $remoteUser) { - $this->deleteUser($remoteUser); - } - $this->usingServer($previousServer); - } - - /** - * @BeforeScenario - * @AfterScenario - */ - public function cleanupGroups() { - $previousServer = $this->currentServer; - $this->usingServer('LOCAL'); - foreach ($this->createdGroups as $group) { - $this->deleteGroup($group); - } - $this->usingServer('REMOTE'); - foreach ($this->createdRemoteGroups as $remoteGroup) { - $this->deleteGroup($remoteGroup); - } - $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]); - } - } - } -} --- a/build/integration/features/bootstrap/RateLimitingContext.php +++ /dev/null @@ -1,33 +0,0 @@ -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']); - } -} --- a/build/integration/features/bootstrap/RoutingContext.php +++ /dev/null @@ -1,42 +0,0 @@ -invokingTheCommand('config:system:delete memcache.local'); - } - - /** - * @Given /^route "([^"]*)" of app "([^"]*)" is defined in routes.php$/ - */ - public function routeOfAppIsDefinedInRoutesPhP(string $route, string $app): void { - $previousUser = $this->currentUser; - $this->currentUser = 'admin'; - - $this->sendingTo('GET', "/apps/testing/api/v1/routes/routesphp/{$app}"); - $this->theHTTPStatusCodeShouldBe('200'); - - Assert::assertStringContainsString($route, $this->response->getBody()->getContents()); - - $this->currentUser = $previousUser; - } -} --- a/build/integration/features/bootstrap/Search.php +++ /dev/null @@ -1,71 +0,0 @@ -searchForInApp($query, ''); - } - - /** - * @When /^searching for "([^"]*)" in app "([^"]*)"$/ - * @param string $query - * @param string $app - */ - public function searchingForInApp(string $query, string $app) { - $url = '/index.php/core/search'; - - $parameters[] = 'query=' . $query; - $parameters[] = 'inApps[]=' . $app; - - $url .= '?' . implode('&', $parameters); - - $this->sendingAToWithRequesttoken('GET', $url); - } - - /** - * @Then /^the list of search results has "(\d+)" results$/ - */ - public function theListOfSearchResultsHasResults(int $count) { - $this->theHTTPStatusCodeShouldBe(200); - - $searchResults = json_decode($this->response->getBody()); - - Assert::assertEquals($count, count($searchResults)); - } - - /** - * @Then /^search result "(\d+)" contains$/ - * - * @param int $number - * @param TableNode $body - */ - public function searchResultXContains(int $number, TableNode $body) { - if (!($body instanceof TableNode)) { - return; - } - - $searchResults = json_decode($this->response->getBody(), $asAssociativeArray = true); - $searchResult = $searchResults[$number]; - - foreach ($body->getRowsHash() as $expectedField => $expectedValue) { - if (!array_key_exists($expectedField, $searchResult)) { - Assert::fail("$expectedField was not found in response"); - } - - Assert::assertEquals($expectedValue, $searchResult[$expectedField], "Field '$expectedField' does not match ({$searchResult[$expectedField]})"); - } - } -} --- a/build/integration/features/bootstrap/SetupContext.php +++ /dev/null @@ -1,18 +0,0 @@ -deleteServerConfig('core', 'shareapi_allow_group_sharing'); - $this->deleteServerConfig('core', 'shareapi_allow_share_dialog_user_enumeration'); - $this->deleteServerConfig('core', 'shareapi_exclude_groups'); - $this->deleteServerConfig('core', 'shareapi_exclude_groups_list'); - $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members'); - $this->deleteServerConfig('core', 'shareapi_only_share_with_group_members_exclude_group_list'); - $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match'); - $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_full_match_displayname'); - $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_restrict_user_enumeration_full_match_user_id'); - $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_group'); - $this->deleteServerConfig('core', 'shareapi_restrict_user_enumeration_to_phone'); - } -} --- a/build/integration/features/bootstrap/Sharing.php +++ /dev/null @@ -1,914 +0,0 @@ -lastShareData = null; - $this->storedShareData = []; - $this->savedShareId = null; - } - /** @var ResponseInterface */ - private $response; - - /** - * @Given /^as "([^"]*)" creating a share with$/ - */ - public function asCreatingAShareWith(string $user, ?TableNode $body): void { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares"; - $client = new Client(); - $options = [ - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - ], - ]; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } - - if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - if (array_key_exists('expireDate', $fd)) { - $dateModification = $fd['expireDate']; - 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); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - } - - $this->lastShareData = simplexml_load_string($this->response->getBody()); - } - - /** - * @When /^save the last share data as "([^"]*)"$/ - */ - public function saveLastShareData(string $name): void { - $this->storedShareData[$name] = $this->lastShareData; - } - - /** - * @When /^restore the last share data from "([^"]*)"$/ - */ - public function restoreLastShareData(string $name): void { - $this->lastShareData = $this->storedShareData[$name]; - } - - /** - * @When /^creating a share with$/ - */ - public function creatingShare(?TableNode $body): void { - $this->asCreatingAShareWith($this->currentUser, $body); - } - - /** - * @When /^opening last share accept confirmation page$/ - */ - public function openingLastShareAcceptConfirmationPage(): void { - $share_id = $this->lastShareData->data[0]->id; - $url = "/index.php/apps/files_sharing/accept/ocinternal:$share_id"; - $baseUrl = substr($this->baseUrl, 0, -5); - $client = new Client(); - try { - $this->response = $client->get( - $baseUrl . $url, - [ - 'cookies' => $this->cookieJar, - ] - ); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } - } - - /** - * @When /^accepting last share via the accept endpoint with requesttoken$/ - */ - public function acceptingLastShareViaAcceptEndpointWithRequesttoken(): void { - $share_id = $this->lastShareData->data[0]->id; - $url = "/index.php/apps/files_sharing/accept/ocinternal:$share_id"; - $baseUrl = substr($this->baseUrl, 0, -5); - $client = new Client(); - try { - $this->response = $client->post( - $baseUrl . $url, - [ - 'cookies' => $this->cookieJar, - 'form_params' => [ - 'requesttoken' => $this->requestToken, - ], - 'headers' => [ - 'Origin' => $baseUrl, - ], - ] - ); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); - } - } - - /** - * @When /^accepting last share via the accept endpoint$/ - */ - public function acceptingLastShareViaAcceptEndpoint(): void { - $this->loggingInUsingWebAs($this->currentUser); - $this->openingLastShareAcceptConfirmationPage(); - - if ($this->response->getStatusCode() !== 200) { - return; - } - - $this->theCsrfTokenIsExtractedFromThePreviousResponse(); - $this->acceptingLastShareViaAcceptEndpointWithRequesttoken(); - } - - /** - * @When /^accepting last share$/ - */ - public function acceptingLastShare(): void { - $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->theHTTPStatusCodeShouldBe('200'); - } - - /** - * @When /^user "([^"]*)" accepts last share$/ - */ - public function userAcceptsLastShare(string $user): void { - // "As userXXX" and "user userXXX accepts last share" steps are not - // expected to be used in the same scenario, but restore the user just - // in case. - $previousUser = $this->currentUser; - - $this->currentUser = $user; - - $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->currentUser = $previousUser; - - $this->theHTTPStatusCodeShouldBe('200'); - } - - /** - * @Then /^last link share can be downloaded$/ - */ - public function lastLinkShareCanBeDownloaded(): void { - if (count($this->lastShareData->data->element) > 0) { - $url = $this->lastShareData->data[0]->url; - } else { - $url = $this->lastShareData->data->url; - } - $fullUrl = $url . '/download'; - $this->checkDownload($fullUrl, null, 'text/plain'); - } - - /** - * @Then /^last link share can not be downloaded with status "([^"]*)"$/ - */ - public function lastLinkShareCanNotBeDownloaded(string $status): void { - if (count($this->lastShareData->data->element) > 0) { - $url = $this->lastShareData->data[0]->url; - } else { - $url = $this->lastShareData->data->url; - } - $fullUrl = $url . '/download'; - - $client = new Client(); - try { - $response = $client->get($fullUrl, ['stream' => true]); - } catch (ClientException $ex) { - $response = $ex->getResponse(); - } - Assert::assertNotEquals($status, $response->getStatusCode()); - } - - /** - * @Then /^last share can be downloaded$/ - */ - public function lastShareCanBeDownloaded(): void { - if (count($this->lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data->token; - } - - $fullUrl = substr($this->baseUrl, 0, -4) . 'index.php/s/' . $token . '/download'; - $this->checkDownload($fullUrl, null, 'text/plain'); - } - - /** - * @Then /^last share with password "([^"]*)" can be downloaded$/ - */ - public function lastShareWithPasswordCanBeDownloaded($password) { - if (count($this->lastShareData->data->element) > 0) { - $token = $this->lastShareData->data[0]->token; - } else { - $token = $this->lastShareData->data->token; - } - - $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) { - if ($auth !== null) { - $options['auth'] = $auth; - } - $options['stream'] = true; - - $client = new Client(); - $this->response = $client->get($url, $options); - Assert::assertEquals(200, $this->response->getStatusCode()); - - $buf = ''; - $body = $this->response->getBody(); - while (!$body->eof()) { - // read everything - $buf .= $body->read(8192); - } - $body->close(); - - if ($mimeType !== null) { - $finfo = new finfo; - Assert::assertEquals($mimeType, $finfo->buffer($buf, FILEINFO_MIME_TYPE)); - } - } - - /** - * @When /^Adding expiration date to last share$/ - */ - public function addingExpirationDate() { - $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 = []; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - $date = date('Y-m-d', strtotime('+3 days')); - $options['form_params'] = ['expireDate' => $date]; - $this->response = $this->response = $client->request('PUT', $fullUrl, $options); - Assert::assertEquals(200, $this->response->getStatusCode()); - } - - /** - * @When /^Updating last share with$/ - * @param TableNode|null $body - */ - public function updatingLastShare($body) { - $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 = [ - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - ], - ]; - if ($this->currentUser === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$this->currentUser, $this->regularUser]; - } - - if ($body instanceof TableNode) { - $fd = $body->getRowsHash(); - if (array_key_exists('expireDate', $fd)) { - $dateModification = $fd['expireDate']; - $fd['expireDate'] = date('Y-m-d', strtotime($dateModification)); - } - $options['form_params'] = $fd; - } - - try { - $this->response = $client->request('PUT', $fullUrl, $options); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - } - } - - public function createShare($user, - $path = null, - $shareType = null, - $shareWith = null, - $publicUpload = null, - $password = null, - $permissions = null, - $viewOnly = false) { - $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares"; - $client = new Client(); - $options = [ - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - ], - ]; - - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } - $body = []; - if (!is_null($path)) { - $body['path'] = $path; - } - if (!is_null($shareType)) { - $body['shareType'] = $shareType; - } - if (!is_null($shareWith)) { - $body['shareWith'] = $shareWith; - } - if (!is_null($publicUpload)) { - $body['publicUpload'] = $publicUpload; - } - if (!is_null($password)) { - $body['password'] = $password; - } - if (!is_null($permissions)) { - $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->lastShareData = simplexml_load_string($this->response->getBody()); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $this->response = $ex->getResponse(); - throw new \Exception($this->response->getBody()); - } - } - - public function getFieldValueInResponse($field) { - $data = simplexml_load_string($this->response->getBody())->data[0]; - if (count($data->element) > 0) { - foreach ($data as $element) { - return (string)$element->$field; - } - - return false; - } - return $data->$field; - } - - public function isFieldInResponse($field, $contentExpected) { - $data = simplexml_load_string($this->response->getBody())->data[0]; - if ((string)$field === 'expiration') { - if (!empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 23:59:59'; - } - } - if (count($data->element) > 0) { - foreach ($data as $element) { - if ($contentExpected == 'A_TOKEN') { - $tokenLength = strlen((string)$element->$field); - return $tokenLength === 15 || $tokenLength === 32; - } elseif ($contentExpected == 'A_NUMBER') { - return is_numeric((string)$element->$field); - } elseif ($contentExpected == 'AN_URL') { - return $this->isExpectedUrl((string)$element->$field, 'index.php/s/'); - } elseif ((string)$element->$field == $contentExpected) { - return true; - } else { - print($element->$field); - } - } - - return false; - } else { - if ($contentExpected == 'A_TOKEN') { - $tokenLength = strlen((string)$data->$field); - return $tokenLength === 15 || $tokenLength === 32; - } elseif ($contentExpected == 'A_NUMBER') { - return is_numeric((string)$data->$field); - } elseif ($contentExpected == 'AN_URL') { - return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); - } elseif ($contentExpected == $data->$field) { - return true; - } else { - print($data->$field); - } - return false; - } - } - - /** - * @Then /^File "([^"]*)" should be included in the response$/ - * - * @param string $filename - */ - public function checkSharedFileInResponse($filename) { - Assert::assertEquals(true, $this->isFieldInResponse('file_target', "/$filename")); - } - - /** - * @Then /^File "([^"]*)" should not be included in the response$/ - * - * @param string $filename - */ - public function checkSharedFileNotInResponse($filename) { - Assert::assertEquals(false, $this->isFieldInResponse('file_target', "/$filename")); - } - - /** - * @Then /^User "([^"]*)" should be included in the response$/ - * - * @param string $user - */ - public function checkSharedUserInResponse($user) { - Assert::assertEquals(true, $this->isFieldInResponse('share_with', "$user")); - } - - /** - * @Then /^User "([^"]*)" should not be included in the response$/ - * - * @param string $user - */ - public function checkSharedUserNotInResponse($user) { - Assert::assertEquals(false, $this->isFieldInResponse('share_with', "$user")); - } - - public function isUserOrGroupInSharedData($userOrGroup, $permissions = null) { - $data = simplexml_load_string($this->response->getBody())->data[0]; - foreach ($data as $element) { - if ($element->share_with == $userOrGroup && ($permissions === null || $permissions == $element->permissions)) { - return true; - } - } - return false; - } - - /** - * @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, $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 = []; - if ($user1 === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user1, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $this->response = $client->get($fullUrl, $options); - if ($this->isUserOrGroupInSharedData($user2, $permissions)) { - return; - } else { - $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]*))?( view-only)?$/ - * - * @param string $filepath - * @param string $user - * @param string $group - */ - 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 = []; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } - $options['headers'] = [ - 'OCS-APIREQUEST' => 'true', - ]; - $this->response = $client->get($fullUrl, $options); - if ($this->isUserOrGroupInSharedData($group, $permissions)) { - return; - } else { - $this->createShare($user, $filepath, 1, $group, null, null, $permissions, $viewOnly !== null); - } - $this->response = $client->get($fullUrl, $options); - Assert::assertEquals(true, $this->isUserOrGroupInSharedData($group, $permissions)); - } - - /** - * @When /^Deleting last share$/ - */ - 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); - } - - /** - * @When /^Getting info of last share$/ - */ - 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); - } - - /** - * @Then /^last share_id is included in the answer$/ - */ - public function checkingLastShareIDIsIncluded() { - $share_id = $this->lastShareData->data[0]->id; - if (!$this->isFieldInResponse('id', $share_id)) { - Assert::fail("Share id $share_id not found in response"); - } - } - - /** - * @Then /^last share_id is not included in the answer$/ - */ - public function checkingLastShareIDIsNotIncluded() { - $share_id = $this->lastShareData->data[0]->id; - if ($this->isFieldInResponse('id', $share_id)) { - Assert::fail("Share id $share_id has been found in response"); - } - } - - /** - * @Then /^Share fields of last share match with$/ - * @param TableNode|null $body - */ - public function checkShareFields($body) { - if ($body instanceof TableNode) { - $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, 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"); - } - } - } - } - - /** - * @Then the list of returned shares has :count shares - */ - public function theListOfReturnedSharesHasShares(int $count): void { - $this->theHTTPStatusCodeShouldBe('200'); - $this->theOCSStatusCodeShouldBe('100'); - - $returnedShares = $this->getXmlResponse()->data[0]; - - Assert::assertEquals($count, count($returnedShares->element)); - } - - /** - * @Then share :count is returned with - * - * @param int $number - * @param TableNode $body - */ - public function shareXIsReturnedWith(int $number, TableNode $body) { - $this->theHTTPStatusCodeShouldBe('200'); - $this->theOCSStatusCodeShouldBe('100'); - - if (!($body instanceof TableNode)) { - return; - } - - $returnedShare = $this->getXmlResponse()->data[0]; - if ($returnedShare->element) { - $returnedShare = (array)$returnedShare; - $returnedShare = $returnedShare['element']; - if (is_array($returnedShare)) { - usort($returnedShare, fn ($share1, $share2) => (int)$share1->id <=> (int)$share2->id); - } - - $returnedShare = $returnedShare[$number]; - } - - $defaultExpectedFields = [ - 'id' => 'A_NUMBER', - 'permissions' => '19', - 'stime' => 'A_NUMBER', - 'parent' => '', - 'expiration' => '', - 'token' => '', - 'storage' => 'A_NUMBER', - 'item_source' => 'A_NUMBER', - 'file_source' => 'A_NUMBER', - 'file_parent' => 'A_NUMBER', - 'mail_send' => '0' - ]; - $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash()); - - 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)) { - $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 ($expectedFields['share_with'] === 'private_conversation') { - $expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/'; - } else { - $expectedFields['share_with'] = FeatureContext::getTokenForIdentifier($expectedFields['share_with']); - } - } - - foreach ($expectedFields as $field => $value) { - $this->assertFieldIsInReturnedShare($field, $value, $returnedShare); - } - } - - /** - * @return SimpleXMLElement - */ - private function getXmlResponse(): \SimpleXMLElement { - return simplexml_load_string($this->response->getBody()); - } - - /** - * @param string $field - * @param string $contentExpected - * @param \SimpleXMLElement $returnedShare - */ - private function assertFieldIsInReturnedShare(string $field, string $contentExpected, \SimpleXMLElement $returnedShare) { - if ($contentExpected === 'IGNORE') { - return; - } - - 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)) . ' 23:59:59'; - } - - if ($contentExpected === 'A_NUMBER') { - Assert::assertTrue(is_numeric((string)$returnedShare->$field), "Field '$field' is not a number: " . $returnedShare->$field); - } elseif ($contentExpected === 'A_TOKEN') { - // A token is either: - // - 15 characters from ISecureRandom::CHAR_HUMAN_READABLE (legacy), or - // - 32 characters from ISecureRandom::CHAR_ALPHANUMERIC (new OCM tokens) - Assert::assertMatchesRegularExpression('/^([a-zA-Z0-9]{15}|[a-zA-Z0-9]{32})$/', (string)$returnedShare->$field, "Field '$field' is not a token"); - } elseif (strpos($contentExpected, 'REGEXP ') === 0) { - Assert::assertMatchesRegularExpression(substr($contentExpected, strlen('REGEXP ')), (string)$returnedShare->$field, "Field '$field' does not match"); - } else { - Assert::assertEquals($contentExpected, (string)$returnedShare->$field, "Field '$field' does not match"); - } - } - - /** - * @Then As :user remove all shares from the file named :fileName - */ - public function asRemoveAllSharesFromTheFileNamed($user, $fileName) { - $url = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares?format=json"; - $client = new \GuzzleHttp\Client(); - $res = $client->get( - $url, - [ - 'auth' => [ - $user, - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - 'OCS-APIREQUEST' => 'true', - ], - ] - ); - $json = json_decode($res->getBody()->getContents(), true); - $deleted = false; - foreach ($json['ocs']['data'] as $data) { - if (stripslashes($data['path']) === $fileName) { - $id = $data['id']; - $client->delete( - $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/{$id}", - [ - 'auth' => [ - $user, - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - 'OCS-APIREQUEST' => 'true', - ], - ] - ); - $deleted = true; - } - } - - if ($deleted === false) { - throw new \Exception("Could not delete file $fileName"); - } - } - - /** - * @When save last share id - */ - public function saveLastShareId() { - $this->savedShareId = ($this->lastShareData['data']['id'] ?? null); - } - - /** - * @Then share ids should match - */ - public function shareIdsShouldMatch() { - if ($this->savedShareId !== ($this->lastShareData['data']['id'] ?? null)) { - throw new \Exception('Expected the same link share to be returned'); - } - } - - /** - * @When /^getting sharees for$/ - * @param TableNode $body - */ - public function whenGettingShareesFor($body) { - $url = '/apps/files_sharing/api/v1/sharees'; - if ($body instanceof TableNode) { - $parameters = []; - foreach ($body->getRowsHash() as $key => $value) { - if ($key === 'shareTypes') { - foreach (explode(' ', $value) as $shareType) { - $parameters[] = 'shareType[]=' . $shareType; - } - } else { - $parameters[] = $key . '=' . $value; - } - } - if (!empty($parameters)) { - $url .= '?' . implode('&', $parameters); - } - } - - $this->sendingTo('GET', $url); - } - - /** - * @Then /^"([^"]*)" sharees returned (are|is empty)$/ - * @param string $shareeType - * @param string $isEmpty - * @param TableNode|null $shareesList - */ - public function thenListOfSharees($shareeType, $isEmpty, $shareesList = null) { - if ($isEmpty !== 'is empty') { - $sharees = $shareesList->getRows(); - $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType); - Assert::assertEquals($sharees, $respondedArray); - } else { - $respondedArray = $this->getArrayOfShareesResponded($this->response, $shareeType); - Assert::assertEmpty($respondedArray); - } - } - - public function getArrayOfShareesResponded(ResponseInterface $response, $shareeType) { - $elements = simplexml_load_string($response->getBody())->data; - $elements = json_decode(json_encode($elements), 1); - if (strpos($shareeType, 'exact ') === 0) { - $elements = $elements['exact']; - $shareeType = substr($shareeType, 6); - } - - // "simplexml_load_string" creates a SimpleXMLElement object for each - // XML element with child elements. In turn, each child is indexed by - // its tag in the SimpleXMLElement object. However, when there are - // several child XML elements with the same tag, an array with all the - // children with the same tag is indexed instead. Therefore, when the - // XML contains - // - // - // - // ... - // - // - // the "$elements[$shareeType]" variable contains an "element" key which - // in turn contains "label" and "value" keys, but when the XML contains - // - // - // - // ... - // - // - // - // ... - // - // - // the "$elements[$shareeType]" variable contains an "element" key which - // in turn contains "0" and "1" keys, and in turn each one contains - // "label" and "value" keys. - if (array_key_exists('element', $elements[$shareeType]) && is_int(array_keys($elements[$shareeType]['element'])[0])) { - $elements[$shareeType] = $elements[$shareeType]['element']; - } - - $sharees = []; - foreach ($elements[$shareeType] as $element) { - $sharee = [$element['label'], $element['value']['shareType'], $element['value']['shareWith']]; - - if (array_key_exists('shareWithDisplayNameUnique', $element)) { - $sharee[] = $element['shareWithDisplayNameUnique']; - } - - $sharees[] = $sharee; - } - return $sharees; - } - - /** - * @Then /^Share mounts for "([^"]*)" match$/ - */ - public function checkShareMounts(string $user, ?TableNode $body) { - if ($body instanceof TableNode) { - $fd = $body->getRows(); - - $expected = []; - foreach ($fd as $row) { - $expected[] = $row[0]; - } - $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]); - $mounts = json_decode($this->lastStdOut, true)['cached']; - $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === MountProvider::class); - $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts)); - Assert::assertEquals($expected, $actual); - } - } - - /** - * @Then /^Share mounts for "([^"]*)" are empty$/ - */ - public function checkShareMountsEmpty(string $user) { - $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]); - $mounts = json_decode($this->lastStdOut, true)['cached']; - $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === MountProvider::class); - $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts)); - Assert::assertEquals([], $actual); - } -} --- a/build/integration/features/bootstrap/SharingContext.php +++ /dev/null @@ -1,40 +0,0 @@ -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('core', 'shareapi_allow_federation_on_public_shares'); - $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); - $this->deleteServerConfig('core', 'shareapi_allow_view_without_download'); - $this->deleteServerConfig('files_sharing', 'update_cutoff_time'); - - $this->runOcc(['config:system:delete', 'share_folder']); - } -} --- a/build/integration/features/bootstrap/TagsContext.php +++ /dev/null @@ -1,655 +0,0 @@ -baseUrl = substr($testServerUrl, 0, -5); - } - } - - /** @BeforeScenario */ - public function setUpScenario() { - $this->client = new Client(); - } - - /** @AfterScenario */ - public function tearDownScenario() { - $user = 'admin'; - $tags = $this->requestTagsForUser($user); - foreach ($tags as $tagId => $tag) { - $this->response = $this->client->delete( - $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } - try { - $this->client->delete( - $this->baseUrl . '/remote.php/webdav/myFileToTag.txt', - [ - 'auth' => [ - 'user0', - '123456', - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - } - } - - /** - * @param string $userName - * @return string - */ - private function getPasswordForUser($userName) { - if ($userName === 'admin') { - return 'admin'; - } - return '123456'; - } - - /** - * @param string $user - * @param string $type - * @param string $name - * @param string $groups - */ - private function createTag($user, $type, $name, $groups = null) { - $userVisible = true; - $userAssignable = true; - switch ($type) { - case 'normal': - break; - case 'not user-assignable': - $userAssignable = false; - break; - case 'not user-visible': - $userVisible = false; - break; - default: - throw new \Exception('Unsupported type'); - } - - $body = [ - 'name' => $name, - 'userVisible' => $userVisible, - 'userAssignable' => $userAssignable, - ]; - if ($groups !== null) { - $body['groups'] = $groups; - } - - try { - $this->response = $this->client->post( - $this->baseUrl . '/remote.php/dav/systemtags/', - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'body' => json_encode($body) - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When :user creates a :type tag with name :name - * @param string $user - * @param string $type - * @param string $name - * @throws \Exception - */ - public function createsATagWithName($user, $type, $name) { - $this->createTag($user, $type, $name); - } - - /** - * @When :user creates a :type tag with name :name and groups :groups - * @param string $user - * @param string $type - * @param string $name - * @param string $groups - * @throws \Exception - */ - public function createsATagWithNameAndGroups($user, $type, $name, $groups) { - $this->createTag($user, $type, $name, $groups); - } - - /** - * @Then The response should have a status code :statusCode - * @param int $statusCode - * @throws \Exception - */ - public function theResponseShouldHaveAStatusCode($statusCode) { - if ((int)$statusCode !== $this->response->getStatusCode()) { - throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode()); - } - } - - /** - * Returns all tags for a given user - * - * @param string $user - * @return array - */ - private function requestTagsForUser($user, $withGroups = false) { - try { - $body = ' - - - - - - - -'; - - if ($withGroups) { - $body .= ''; - } - - $body .= ' - -'; - $this->response = $this->client->request( - 'PROPFIND', - $this->baseUrl . '/remote.php/dav/systemtags/', - [ - 'body' => $body, - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - - $tags = []; - $service = new Sabre\Xml\Service(); - $parsed = $service->parse($this->response->getBody()->getContents()); - foreach ($parsed as $entry) { - $singleEntry = $entry['value'][1]['value'][0]['value']; - if (empty($singleEntry[0]['value'])) { - continue; - } - - // FIXME: use actual property names instead of guessing index position - $tags[$singleEntry[0]['value']] = [ - 'display-name' => $singleEntry[1]['value'], - 'user-visible' => $singleEntry[2]['value'], - 'user-assignable' => $singleEntry[3]['value'], - 'can-assign' => $singleEntry[4]['value'], - ]; - if (isset($singleEntry[5])) { - $tags[$singleEntry[0]['value']]['groups'] = $singleEntry[5]['value']; - } - } - - return $tags; - } - - /** - * @Then The following tags should exist for :user - * @param string $user - * @param TableNode $table - * @throws \Exception - */ - public function theFollowingTagsShouldExistFor($user, TableNode $table) { - $tags = $this->requestTagsForUser($user); - - if (count($table->getRows()) !== count($tags)) { - throw new \Exception( - sprintf( - 'Expected %s tags, got %s.', - count($table->getRows()), - count($tags) - ) - ); - } - - 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] - ) { - unset($tags[$key]); - } - } - } - if (count($tags) !== 0) { - throw new \Exception('Not expected response'); - } - } - - /** - * @Then the user :user :can assign The :type tag with name :tagName - */ - public function theUserCanAssignTheTag($user, $can, $type, $tagName) { - $foundTag = $this->findTag($type, $tagName, $user); - if ($foundTag === null) { - throw new \Exception('No matching tag found'); - } - - if ($can === 'can') { - $expected = 'true'; - } elseif ($can === 'cannot') { - $expected = 'false'; - } else { - throw new \Exception('Invalid condition, must be "can" or "cannot"'); - } - - if ($foundTag['can-assign'] !== $expected) { - throw new \Exception('Tag cannot be assigned by user'); - } - } - - /** - * @Then The :type tag with name :tagName has the groups :groups - */ - public function theTagHasGroup($type, $tagName, $groups) { - $foundTag = $this->findTag($type, $tagName, 'admin', true); - if ($foundTag === null) { - throw new \Exception('No matching tag found'); - } - - if ($foundTag['groups'] !== $groups) { - throw new \Exception('Tag has groups "' . $foundTag['group'] . '" instead of the expected "' . $groups . '"'); - } - } - - /** - * @Then :count tags should exist for :user - * @param int $count - * @param string $user - * @throws \Exception - */ - public function tagsShouldExistFor($count, $user) { - if ((int)$count !== count($this->requestTagsForUser($user))) { - throw new \Exception("Expected $count tags, got " . count($this->requestTagsForUser($user))); - } - } - - /** - * Find tag by type and name - * - * @param string $type tag type - * @param string $tagName tag name - * @param string $user retrieved from which user - * @param bool $withGroups whether to also query the tag's groups - * - * @return array tag values or null if not found - */ - private function findTag($type, $tagName, $user = 'admin', $withGroups = false) { - $tags = $this->requestTagsForUser($user, $withGroups); - $userAssignable = 'true'; - $userVisible = 'true'; - switch ($type) { - case 'normal': - break; - case 'not user-assignable': - $userAssignable = 'false'; - break; - case 'not user-visible': - $userVisible = 'false'; - break; - default: - throw new \Exception('Unsupported type'); - } - - $foundTag = null; - foreach ($tags as $tag) { - if ($tag['display-name'] === $tagName - && $tag['user-visible'] === $userVisible - && $tag['user-assignable'] === $userAssignable) { - $foundTag = $tag; - break; - } - } - return $foundTag; - } - - /** - * @param string $name - * @return int - */ - private function findTagIdByName($name) { - $tags = $this->requestTagsForUser('admin'); - $tagId = 0; - foreach ($tags as $id => $tag) { - if ($tag['display-name'] === $name) { - $tagId = $id; - break; - } - } - return (int)$tagId; - } - - /** - * @When :user edits the tag with name :oldNmae and sets its name to :newName - * @param string $user - * @param string $oldName - * @param string $newName - * @throws \Exception - */ - public function editsTheTagWithNameAndSetsItsNameTo($user, $oldName, $newName) { - $tagId = $this->findTagIdByName($oldName); - if ($tagId === 0) { - throw new \Exception('Could not find tag to rename'); - } - - try { - $this->response = $this->client->request( - 'PROPPATCH', - $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, - [ - 'body' => ' - - - - ' . $newName . ' - - -', - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When :user edits the tag with name :oldNmae and sets its groups to :groups - * @param string $user - * @param string $oldName - * @param string $groups - * @throws \Exception - */ - public function editsTheTagWithNameAndSetsItsGroupsTo($user, $oldName, $groups) { - $tagId = $this->findTagIdByName($oldName); - if ($tagId === 0) { - throw new \Exception('Could not find tag to rename'); - } - - try { - $this->response = $this->client->request( - 'PROPPATCH', - $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, - [ - 'body' => ' - - - - ' . $groups . ' - - -', - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When :user deletes the tag with name :name - * @param string $user - * @param string $name - */ - public function deletesTheTagWithName($user, $name) { - $tagId = $this->findTagIdByName($name); - try { - $this->response = $this->client->delete( - $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @param string $path - * @param string $user - * @return int - */ - private function getFileIdForPath($path, $user) { - $url = $this->baseUrl . '/remote.php/webdav/' . $path; - $credentials = base64_encode($user . ':' . $this->getPasswordForUser($user)); - $context = stream_context_create([ - 'http' => [ - 'method' => 'PROPFIND', - 'header' => "Authorization: Basic $credentials\r\nContent-Type: application/x-www-form-urlencoded", - 'content' => ' - - - - -' - ] - ]); - $response = file_get_contents($url, false, $context); - preg_match_all('/\(.*?)\<\/oc:fileid\>/', $response, $matches); - return (int)$matches[1][0]; - } - - /** - * @When /^"([^"]*)" adds the tag "([^"]*)" to "([^"]*)" (shared|owned) by "([^"]*)"$/ - * @param string $taggingUser - * @param string $tagName - * @param string $fileName - * @param string $sharingUser - */ - public function addsTheTagToSharedBy($taggingUser, $tagName, $fileName, $sharedOrOwnedBy, $sharingUser) { - $fileId = $this->getFileIdForPath($fileName, $sharingUser); - $tagId = $this->findTagIdByName($tagName); - - try { - $this->response = $this->client->put( - $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId, - [ - 'auth' => [ - $taggingUser, - $this->getPasswordForUser($taggingUser), - ] - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @Then /^"([^"]*)" (shared|owned) by "([^"]*)" has the following tags$/ - * @param string $fileName - * @param string $sharingUser - * @param TableNode $table - * @throws \Exception - */ - public function sharedByHasTheFollowingTags($fileName, $sharedOrOwnedBy, $sharingUser, TableNode $table) { - $loadedExpectedTags = $table->getTable(); - $expectedTags = []; - foreach ($loadedExpectedTags as $expected) { - $expectedTags[] = $expected[0]; - } - - // Get the real tags - $response = $this->client->request( - 'PROPFIND', - $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser), - [ - 'auth' => [ - $sharingUser, - $this->getPasswordForUser($sharingUser), - ], - 'body' => ' - - - - - - - -', - ] - )->getBody()->getContents(); - preg_match_all('/\(.*?)\<\/oc:display-name\>/', $response, $realTags); - - foreach ($expectedTags as $key => $row) { - foreach ($realTags as $tag) { - if ($tag[0] === $row) { - unset($expectedTags[$key]); - } - } - } - - if (count($expectedTags) !== 0) { - throw new \Exception('Not all tags found.'); - } - } - - /** - * @Then :fileName shared by :sharingUser has the following tags for :user - * @param string $fileName - * @param string $sharingUser - * @param string $user - * @param TableNode $table - * @throws \Exception - */ - public function sharedByHasTheFollowingTagsFor($fileName, $sharingUser, $user, TableNode $table) { - $loadedExpectedTags = $table->getTable(); - $expectedTags = []; - foreach ($loadedExpectedTags as $expected) { - $expectedTags[] = $expected[0]; - } - - // Get the real tags - try { - $this->response = $this->client->request( - 'PROPFIND', - $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser), - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - 'body' => ' - - - - - - - -', - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - preg_match_all('/\(.*?)\<\/oc:display-name\>/', $this->response->getBody()->getContents(), $realTags); - $realTags = array_filter($realTags); - $expectedTags = array_filter($expectedTags); - - foreach ($expectedTags as $key => $row) { - foreach ($realTags as $tag) { - foreach ($tag as $index => $foo) { - if ($tag[$index] === $row) { - unset($expectedTags[$key]); - } - } - } - } - - if (count($expectedTags) !== 0) { - throw new \Exception('Not all tags found.'); - } - } - - /** - * @When :user removes the tag :tagName from :fileName shared by :shareUser - * @param string $user - * @param string $tagName - * @param string $fileName - * @param string $shareUser - */ - public function removesTheTagFromSharedBy($user, $tagName, $fileName, $shareUser) { - $tagId = $this->findTagIdByName($tagName); - $fileId = $this->getFileIdForPath($fileName, $shareUser); - - try { - $this->response = $this->client->delete( - $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user), - ], - ] - ); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } -} --- a/build/integration/features/bootstrap/TalkContext.php +++ /dev/null @@ -1,54 +0,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); - - return $lastStdOut; - } -} --- a/build/integration/features/bootstrap/Theming.php +++ /dev/null @@ -1,49 +0,0 @@ -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'); - } -} --- a/build/integration/features/bootstrap/Trashbin.php +++ /dev/null @@ -1,155 +0,0 @@ -getSabreClient($user); - $response = $client->request('DELETE', $this->makeSabrePath($user, 'trash', 'trashbin')); - Assert::assertEquals(204, $response['statusCode']); - } - - private function findFullTrashname($user, $name) { - $rootListing = $this->listTrashbinFolder($user, '/'); - - foreach ($rootListing as $href => $rootItem) { - if ($rootItem['{http://nextcloud.org/ns}trashbin-filename'] === $name) { - return basename($href); - } - } - - return null; - } - - /** - * Get the full /startofpath.dxxxx/rest/of/path from /startofpath/rest/of/path - */ - private function getFullTrashPath($user, $path) { - if ($path !== '' && $path !== '/') { - $parts = explode('/', $path); - $fullName = $this->findFullTrashname($user, $parts[1]); - if ($fullName === null) { - Assert::fail("cant find $path in trash"); - return '/dummy_full_path_not_found'; - } - $parts[1] = $fullName; - - $path = implode('/', $parts); - } - return $path; - } - - /** - * List trashbin folder - * - * @param string $user user - * @param string $path path - * @return array response - */ - public function listTrashbinFolder($user, $path) { - $path = $this->getFullTrashPath($user, $path); - $client = $this->getSabreClient($user); - - $results = $client->propfind($this->makeSabrePath($user, 'trash' . $path, 'trashbin'), [ - '{http://nextcloud.org/ns}trashbin-filename', - '{http://nextcloud.org/ns}trashbin-original-location', - '{http://nextcloud.org/ns}trashbin-deletion-time' - ], 1); - $results = array_filter($results, function (array $item) { - return isset($item['{http://nextcloud.org/ns}trashbin-filename']); - }); - if ($path !== '' && $path !== '/') { - array_shift($results); - } - return $results; - } - - /** - * @Then /^user "([^"]*)" in trash folder "([^"]*)" should have the following elements$/ - * @param string $user - * @param string $folder - * @param \Behat\Gherkin\Node\TableNode|null $expectedElements - */ - public function checkTrashContents($user, $folder, $expectedElements) { - $elementList = $this->listTrashbinFolder($user, $folder); - $trashContent = array_filter(array_map(function (array $item) { - return $item['{http://nextcloud.org/ns}trashbin-filename']; - }, $elementList)); - if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) { - $elementRows = $expectedElements->getRows(); - $elementsSimplified = $this->simplifyArray($elementRows); - foreach ($elementsSimplified as $expectedElement) { - $expectedElement = ltrim($expectedElement, '/'); - if (array_search($expectedElement, $trashContent, true) === false) { - Assert::fail("$expectedElement" . ' is not in trash listing'); - } - } - } - } - - /** - * @Then /^as "([^"]*)" the (file|folder) "([^"]*)" exists in trash$/ - * @param string $user - * @param string $type - * @param string $file - */ - public function checkTrashContains($user, $type, $file) { - $parent = dirname($file); - if ($parent === '.') { - $parent = '/'; - } - $name = basename($file); - $elementList = $this->listTrashbinFolder($user, $parent); - $trashContent = array_filter(array_map(function (array $item) { - return $item['{http://nextcloud.org/ns}trashbin-filename']; - }, $elementList)); - - if (array_search($name, array_values($trashContent), true) === false) { - Assert::fail("$name" . ' is not in trash listing'); - } - } - - /** - * @Then /^user "([^"]*)" in trash folder "([^"]*)" should have (\d+) elements?$/ - * @param string $user - * @param string $folder - * @param \Behat\Gherkin\Node\TableNode|null $expectedElements - */ - public function checkTrashSize($user, $folder, $expectedCount) { - $elementList = $this->listTrashbinFolder($user, $folder); - Assert::assertEquals($expectedCount, count($elementList)); - } - - /** - * @When /^user "([^"]*)" in restores "([^"]*)" from trash$/ - * @param string $user - * @param string $file - */ - public function restoreFromTrash($user, $file) { - $file = $this->getFullTrashPath($user, $file); - $url = $this->makeSabrePath($user, 'trash' . $file, 'trashbin'); - $client = $this->getSabreClient($user); - $response = $client->request('MOVE', $url, null, [ - 'Destination' => $this->makeSabrePath($user, 'restore/' . basename($file), 'trashbin'), - ]); - Assert::assertEquals(201, $response['statusCode']); - return; - } -} --- a/build/integration/features/bootstrap/WebDav.php +++ /dev/null @@ -1,1342 +0,0 @@ -davPath = $davPath; - } - - /** - * @Given /^using old dav path$/ - */ - public function usingOldDavPath(): void { - $this->davPath = 'remote.php/webdav'; - $this->usingOldDavPath = true; - } - - /** - * @Given /^using new dav path$/ - */ - public function usingNewDavPath(): void { - $this->davPath = 'remote.php/dav'; - $this->usingOldDavPath = false; - } - - /** - * @Given /^using new public dav path$/ - */ - public function usingNewPublicDavPath(): void { - $this->davPath = 'public.php/dav'; - $this->usingOldDavPath = false; - } - - public function getDavFilesPath($user) { - if ($this->usingOldDavPath === true) { - return $this->davPath; - } else { - return $this->davPath . '/files/' . $user; - } - } - - 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') { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path"; - } else { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; - } - $client = new GClient(); - $options = [ - 'headers' => $headers, - 'body' => $body - ]; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } elseif ($user !== '') { - $options['auth'] = [$user, $this->regularUser]; - } - return $client->request($method, $fullUrl, $options); - } - - /** - * @Given /^User "([^"]*)" moved (file|folder|entry) "([^"]*)" to "([^"]*)"$/ - * @param string $user - * @param string $fileSource - * @param string $fileDestination - */ - 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); - Assert::assertEquals(201, $this->response->getStatusCode()); - } - - /** - * @When /^User "([^"]*)" moves (file|folder|entry) "([^"]*)" to "([^"]*)"$/ - * @param string $user - * @param string $fileSource - * @param string $fileDestination - */ - public function userMovesFile($user, $entry, $fileSource, $fileDestination) { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); - $headers['Destination'] = $fullUrl . $fileDestination; - try { - $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When /^User "([^"]*)" copies file "([^"]*)" to "([^"]*)"$/ - * @param string $user - * @param string $fileSource - * @param string $fileDestination - */ - public function userCopiesFileTo($user, $fileSource, $fileDestination) { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); - $headers['Destination'] = $fullUrl . $fileDestination; - try { - $this->response = $this->makeDavRequest($user, 'COPY', $fileSource, $headers); - } catch (\GuzzleHttp\Exception\ClientException $e) { - // 4xx and 5xx responses cause an exception - $this->response = $e->getResponse(); - } - } - - /** - * @When /^Downloading file "([^"]*)" with range "([^"]*)"$/ - * @param string $fileSource - * @param string $range - */ - public function downloadFileWithRange($fileSource, $range) { - $headers['Range'] = $range; - $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileSource, $headers); - } - - /** - * @When /^Downloading last public shared file with range "([^"]*)"$/ - * @param string $range - */ - public function downloadPublicFileWithRange($range) { - $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token"; - - $client = new GClient(); - $options = []; - $options['headers'] = [ - 'Range' => $range - ]; - - $this->response = $client->request('GET', $fullUrl, $options); - } - - /** - * @When /^Downloading last public shared file inside a folder "([^"]*)" with range "([^"]*)"$/ - * @param string $range - */ - public function downloadPublicFileInsideAFolderWithRange($path, $range) { - $token = $this->lastShareData->data->token; - $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$path"; - - $client = new GClient(); - $options = [ - 'headers' => [ - 'Range' => $range - ] - ]; - - $this->response = $client->request('GET', $fullUrl, $options); - } - - /** - * @Then /^Downloaded content should be "([^"]*)"$/ - * @param string $content - */ - public function downloadedContentShouldBe($content) { - Assert::assertEquals($content, (string)$this->response->getBody()); - } - - /** - * @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, - '', - null, - ' - - - - yes - ' - ); - Assert::assertEquals(207, $this->response->getStatusCode()); - } - - /** - * @Then /^Downloaded content when downloading file "([^"]*)" with range "([^"]*)" should be "([^"]*)"$/ - * @param string $fileSource - * @param string $range - * @param string $content - */ - public function downloadedContentWhenDownloadindShouldBe($fileSource, $range, $content) { - $this->downloadFileWithRange($fileSource, $range); - $this->downloadedContentShouldBe($content); - } - - /** - * @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 - */ - public function downloadingFile($fileName) { - try { - $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileName, []); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @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 - */ - public function downloadedContentShouldStartWith($start) { - if (strpos($this->response->getBody()->getContents(), $start) !== 0) { - throw new \Exception( - sprintf( - "Expected '%s', got '%s'", - $start, - $this->response->getBody()->getContents() - ) - ); - } - } - - /** - * @When Uploading public file :filename with content :content - */ - public function uploadingPublicFile(string $filename, string $content) { - $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('PUT', $fullUrl, [ - 'body' => $content - ]); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @Then /^as "([^"]*)" gets properties of (file|folder|entry) "([^"]*)" with$/ - * @param string $user - * @param string $elementType - * @param string $path - * @param \Behat\Gherkin\Node\TableNode|null $propertiesTable - */ - public function asGetsPropertiesOfFolderWith($user, $elementType, $path, $propertiesTable) { - $properties = null; - if ($propertiesTable instanceof \Behat\Gherkin\Node\TableNode) { - foreach ($propertiesTable->getRows() as $row) { - $properties[] = $row[0]; - } - } - $this->response = $this->listFolder($user, $path, 0, $properties); - } - - /** - * @Then /^as "([^"]*)" the (file|folder|entry) "([^"]*)" does not exist$/ - * @param string $user - * @param string $entry - * @param string $path - * @param \Behat\Gherkin\Node\TableNode|null $propertiesTable - */ - public function asTheFileOrFolderDoesNotExist($user, $entry, $path) { - $client = $this->getSabreClient($user); - $response = $client->request('HEAD', $this->makeSabrePath($user, $path)); - if ($response['statusCode'] !== 404) { - throw new \Exception($entry . ' "' . $path . '" expected to not exist (status code ' . $response['statusCode'] . ', expected 404)'); - } - - return $response; - } - - /** - * @Then /^as "([^"]*)" the (file|folder|entry) "([^"]*)" exists$/ - * @param string $user - * @param string $entry - * @param string $path - */ - public function asTheFileOrFolderExists($user, $entry, $path) { - $this->response = $this->listFolder($user, $path, 0); - } - - /** - * @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) { - $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 = $response[$key]; - if ($value instanceof ResourceType) { - $value = $value->getValue(); - if (empty($value)) { - $value = ''; - } else { - $value = $value[0]; - } - } - if ($value != $expectedValue) { - throw new \Exception("Property \"$key\" found with value \"$value\", expected \"$expectedValue\""); - } - } - - /** - * @Then the response should contain a share-types property with - */ - 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"'); - } - - $foundTypes = []; - $data = $keys['{http://owncloud.org/ns}share-types']; - foreach ($data as $item) { - if ($item['name'] !== '{http://owncloud.org/ns}share-type') { - throw new \Exception('Invalid property found: "' . $item['name'] . '"'); - } - - $foundTypes[] = $item['value']; - } - - foreach ($table->getRows() as $row) { - $key = array_search($row[0], $foundTypes, true); - if ($key === false) { - throw new \Exception('Expected type ' . $row[0] . ' not found'); - } - - unset($foundTypes[$key]); - } - - if ($foundTypes !== []) { - throw new \Exception('Found more share types then specified: ' . $foundTypes); - } - } - - /** - * @Then the response should contain an empty property :property - * @param string $property - * @throws \Exception - */ - public function theResponseShouldContainAnEmptyProperty($property) { - $properties = $this->response; - if (!array_key_exists($property, $properties)) { - throw new \Exception("Cannot find property \"$property\""); - } - - if ($properties[$property] !== null) { - throw new \Exception("Property \"$property\" is not empty"); - } - } - - /*Returns the elements of a propfind, $folderDepth requires 1 to see elements without children*/ - public function listFolder($user, $path, $folderDepth, $properties = null) { - $client = $this->getSabreClient($user); - if (!$properties) { - $properties = [ - '{DAV:}getetag' - ]; - } - - $response = $client->propfind($this->makeSabrePath($user, $path), $properties, $folderDepth); - - 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 = ' - - - ' . $properties . ' - - '; - - $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 = ' '; - } - - if ($condition === null) { - $condition = ' - - - - - - image/png - - - - - - - image/jpeg - - - - - - - image/heic - - - - - - - video/mp4 - - - - - - - video/quicktime - - - - - - - ' . $user . ' - -'; - } - - if ($scope === null) { - $scope = '/files/' . $user . 'infinity'; - } - - $body = ' - - - - ' . $properties . ' - - ' . $scope . ' - ' . $condition . ' - - - - - - - - 35 - 0 - - -'; - - 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 - * @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 reportFolder($user, $path, $properties, $filterRules) { - $client = $this->getSabreClient($user); - - $body = ' - - - ' . $properties . ' - - - ' . $filterRules . ' - - '; - - $response = $client->request('REPORT', $this->makeSabrePath($user, $path), $body); - $parsedResponse = $client->parseMultistatus($response['body']); - return $parsedResponse; - } - - public function makeSabrePath($user, $path, $type = 'files') { - if ($type === 'files') { - return $this->encodePath($this->getDavFilesPath($user) . $path); - } else { - return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path); - } - } - - public function getSabreClient($user) { - $fullUrl = substr($this->baseUrl, 0, -4); - - $settings = [ - 'baseUri' => $fullUrl, - 'userName' => $user, - ]; - - if ($user === 'admin') { - $settings['password'] = $this->adminUser[1]; - } else { - $settings['password'] = $this->regularUser; - } - $settings['authType'] = SClient::AUTH_BASIC; - - return new SClient($settings); - } - - /** - * @Then /^user "([^"]*)" should see following elements$/ - * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $expectedElements - */ - public function checkElementList($user, $expectedElements) { - $elementList = $this->listFolder($user, '/', 3); - if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) { - $elementRows = $expectedElements->getRows(); - $elementsSimplified = $this->simplifyArray($elementRows); - foreach ($elementsSimplified as $expectedElement) { - $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; - if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . ' is not in propfind answer'); - } - } - } - } - - /** - * @When User :user uploads file :source to :destination - * @param string $user - * @param string $source - * @param string $destination - */ - public function userUploadsAFileTo($user, $source, $destination) { - $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); - try { - $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); - } 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(); - } - } - - /** - * @When User :user adds a file of :bytes bytes to :destination - * @param string $user - * @param string $bytes - * @param string $destination - */ - public function userAddsAFileTo($user, $bytes, $destination) { - $filename = 'filespecificSize.txt'; - $this->createFileSpecificSize($filename, $bytes); - Assert::assertEquals(1, file_exists("work/$filename")); - $this->userUploadsAFileTo($user, "work/$filename", $destination); - $this->removeFile('work/', $filename); - $expectedElements = new \Behat\Gherkin\Node\TableNode([["$destination"]]); - $this->checkElementList($user, $expectedElements); - } - - /** - * @When User :user uploads file with content :content to :destination - */ - public function userUploadsAFileWithContentTo($user, $content, $destination) { - $file = \GuzzleHttp\Psr7\Utils::streamFor($content); - try { - $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); - } 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(); - } - } - - /** - * @When /^user "([^"]*)" uploads file with content "([^"]*)" and mtime "([^"]*)" to "([^"]*)"$/ - * @param string $user - * @param string $content - * @param string $mtime - * @param string $destination - */ - public function userUploadsAFileWithContentAndMtimeTo($user, $content, $mtime, $destination) { - $file = \GuzzleHttp\Psr7\Utils::streamFor($content); - try { - $this->response = $this->makeDavRequest($user, 'PUT', $destination, ['X-OC-Mtime' => $mtime], $file); - } catch (\GuzzleHttp\Exception\ServerException $e) { - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * Downloads a specific version (identified by its revision/timestamp) of a - * file through the versions DAV endpoint: - * GET remote.php/dav/versions//versions// - * - * @When /^user "([^"]*)" downloads version "([^"]*)" of file "([^"]*)"$/ - * @param string $user - * @param string $revision - * @param string $path - */ - public function userDownloadsVersionOfFile($user, $revision, $path) { - $fileId = $this->getFileIdForPath($user, $path); - $versionPath = '/' . $user . '/versions/' . $fileId . '/' . $revision; - try { - $this->response = $this->makeDavRequest($user, 'GET', $versionPath, [], null, 'versions'); - } catch (\GuzzleHttp\Exception\ServerException $e) { - $this->response = $e->getResponse(); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When /^User "([^"]*)" deletes (file|folder) "([^"]*)"$/ - * @param string $user - * @param string $type - * @param string $file - */ - public function userDeletesFile($user, $type, $file) { - try { - $this->response = $this->makeDavRequest($user, 'DELETE', $file, []); - } 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(); - } - } - - /** - * @Given User :user created a folder :destination - * @param string $user - * @param string $destination - */ - public function userCreatedAFolder($user, $destination) { - try { - $destination = '/' . ltrim($destination, '/'); - $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []); - } 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(); - } - } - - /** - * @Given user :user uploads bulked files :name1 with :content1 and :name2 with :content2 and :name3 with :content3 - * @param string $user - * @param string $name1 - * @param string $content1 - * @param string $name2 - * @param string $content2 - * @param string $name3 - * @param string $content3 - */ - 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'); - } - - /** - * @When user :user creates a new chunking upload with id :id in the uploads folder for :uidOrToken - */ - public function userCreatesANewChunkingUploadWithIdInFolderOf($user, $id, $uidOrToken): void { - $destination = '/uploads/' . $uidOrToken . '/' . $id; - try { - $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [], null, 'uploads'); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @Given user :user uploads new chunk file :num with :data to id :id - */ - public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id) { - $data = \GuzzleHttp\Psr7\Utils::streamFor($data); - $destination = '/uploads/' . $user . '/' . $id . '/' . $num; - $this->makeDavRequest($user, 'PUT', $destination, [], $data, 'uploads'); - } - - /** - * @Given user :user moves new chunk file with id :id to :dest - */ - public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest) { - $source = '/uploads/' . $user . '/' . $id . '/.file'; - $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; - $this->makeDavRequest($user, 'MOVE', $source, [ - 'Destination' => $destination - ], null, 'uploads'); - } - - /** - * @Then user :user moves new chunk file with id :id to :dest with size :size - */ - public function userMovesNewChunkFileWithIdToMychunkedfileWithSize($user, $id, $dest, $size) { - $source = '/uploads/' . $user . '/' . $id . '/.file'; - $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; - - try { - $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ - 'Destination' => $destination, - 'OC-Total-Length' => $size - ], null, 'uploads'); - } catch (\GuzzleHttp\Exception\BadResponseException $ex) { - $this->response = $ex->getResponse(); - } - } - - /** - * @Given creating a new public chunking upload with id :id - */ - public function creatingANewPublicChunkingUploadWithId(string $id): void { - $this->makePublicUploadsDavRequest('MKCOL', '/' . $id); - } - - /** - * @Given uploading new public chunk file :num with :data to id :id - */ - public function uploadingNewPublicChunkFileWithToId(string $num, string $data, string $id): void { - $this->makePublicUploadsDavRequest('PUT', '/' . $id . '/' . $num, [], \GuzzleHttp\Psr7\Utils::streamFor($data)); - } - - /** - * @When moving new public chunk file with id :id to :dest - */ - public function movingNewPublicChunkFileWithIdTo(string $id, string $dest): void { - $this->makePublicUploadsDavRequest('MOVE', '/' . $id . '/.file', [ - 'Destination' => $this->getPublicDavFilesUrl() . $dest, - ]); - } - - /** - * @When copying new public chunk file with id :id to :dest - */ - public function copyingNewPublicChunkFileWithIdTo(string $id, string $dest): void { - $this->makePublicUploadsDavRequest('COPY', '/' . $id . '/.file', [ - 'Destination' => $this->getPublicDavFilesUrl() . $dest, - ]); - } - - private function getLastShareToken(): string { - if (count($this->lastShareData->data->element) > 0) { - return (string)$this->lastShareData->data[0]->token; - } - return (string)$this->lastShareData->data->token; - } - - private function getPublicDavFilesUrl(): string { - return substr($this->baseUrl, 0, -4) . 'public.php/dav/files/' . $this->getLastShareToken(); - } - - /** - * Performs a request on the public chunked upload endpoint of the last created share - */ - private function makePublicUploadsDavRequest(string $method, string $path, array $headers = [], $body = null): void { - $fullUrl = substr($this->baseUrl, 0, -4) . 'public.php/dav/uploads/' . $this->getLastShareToken() . $path; - // Non GET requests on the public DAV endpoint require the AJAX header - $headers['X-Requested-With'] = 'XMLHttpRequest'; - - $client = new GClient(); - try { - $this->response = $client->request($method, $fullUrl, [ - 'headers' => $headers, - 'body' => $body, - ]); - } catch (\GuzzleHttp\Exception\BadResponseException $e) { - // 4xx and 5xx responses cause an exception - $this->response = $e->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); - try { - $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [ - '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(); - } - } - - /** - * @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 $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(); - } - } - - /** - * URL encodes the given path but keeps the slashes - * - * @param string $path to encode - * @return string encoded path - */ - private function encodePath($path) { - // slashes need to stay - return str_replace('%2F', '/', rawurlencode($path)); - } - - /** - * @When user :user favorites element :path - */ - public function userFavoritesElement($user, $path) { - $this->response = $this->changeFavStateOfAnElement($user, $path, 1, 0, null); - } - - /** - * @When user :user unfavorites element :path - */ - public function userUnfavoritesElement($user, $path) { - $this->response = $this->changeFavStateOfAnElement($user, $path, 0, 0, null); - } - - /*Set the elements of a proppatch, $folderDepth requires 1 to see elements without children*/ - public function changeFavStateOfAnElement($user, $path, $favOrUnfav, $folderDepth, $properties = null) { - $fullUrl = substr($this->baseUrl, 0, -4); - $settings = [ - 'baseUri' => $fullUrl, - 'userName' => $user, - ]; - if ($user === 'admin') { - $settings['password'] = $this->adminUser[1]; - } else { - $settings['password'] = $this->regularUser; - } - $settings['authType'] = SClient::AUTH_BASIC; - - $client = new SClient($settings); - if (!$properties) { - $properties = [ - '{http://owncloud.org/ns}favorite' => $favOrUnfav - ]; - } - - $response = $client->proppatch($this->getDavFilesPath($user) . $path, $properties, $folderDepth); - return $response; - } - - /** - * @Given user :user stores etag of element :path - */ - public function userStoresEtagOfElement($user, $path) { - $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); - $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable); - $pathETAG[$path] = $this->response['{DAV:}getetag']; - $this->storedETAG[$user] = $pathETAG; - } - - /** - * @Then etag of element :path of user :user has not changed - */ - public function checkIfETAGHasNotChanged($path, $user) { - $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); - $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable); - Assert::assertEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]); - } - - /** - * @Then etag of element :path of user :user has changed - */ - public function checkIfETAGHasChanged($path, $user) { - $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); - $this->asGetsPropertiesOfFolderWith($user, 'entry', $path, $propertiesTable); - Assert::assertNotEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]); - } - - /** - * @When Connecting to dav endpoint - */ - public function connectingToDavEndpoint() { - try { - $this->response = $this->makeDavRequest($this->currentUser, 'PROPFIND', '', []); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->response = $e->getResponse(); - } - } - - /** - * @When Requesting share note on dav endpoint - */ - public function requestingShareNote() { - $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() { - $headers = $this->response->getHeaders(); - foreach ($headers as $headerName => $headerValues) { - // if a header has multiple values, they must be different - if (count($headerValues) > 1 && count(array_unique($headerValues)) < count($headerValues)) { - throw new \Exception('Duplicate header found: ' . $headerName); - } - } - } - - /** - * @Then /^user "([^"]*)" in folder "([^"]*)" should have favorited the following elements$/ - * @param string $user - * @param string $folder - * @param \Behat\Gherkin\Node\TableNode|null $expectedElements - */ - public function checkFavoritedElements($user, $folder, $expectedElements) { - $elementList = $this->reportFolder($user, - $folder, - '', - '1'); - if ($expectedElements instanceof \Behat\Gherkin\Node\TableNode) { - $elementRows = $expectedElements->getRows(); - $elementsSimplified = $this->simplifyArray($elementRows); - foreach ($elementsSimplified as $expectedElement) { - $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; - if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . ' is not in report answer'); - } - } - } - } - - /** - * @When /^User "([^"]*)" deletes everything from folder "([^"]*)"$/ - * @param string $user - * @param string $folder - */ - public function userDeletesEverythingInFolder($user, $folder) { - $elementList = $this->listFolder($user, $folder, 1); - $elementListKeys = array_keys($elementList); - array_shift($elementListKeys); - $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); - } - } - - /** - * @param string $user - * @param string $path - * @return int - */ - private function getFileIdForPath($user, $path) { - $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']; - } - - /** - * @Given /^User "([^"]*)" stores id of file "([^"]*)"$/ - * @param string $user - * @param string $path - */ - public function userStoresFileIdForPath($user, $path) { - $this->storedFileID = $this->getFileIdForPath($user, $path); - } - - /** - * @Given /^User "([^"]*)" checks id of file "([^"]*)"$/ - * @param string $user - * @param string $path - */ - public function userChecksFileIdForPath($user, $path) { - $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; - } -} --- a/build/integration/features/bootstrap/autoload.php +++ /dev/null @@ -1,11 +0,0 @@ -/dev/null 2>&1; do sleep 1; done"; then - echo "Could not start database server after 600 seconds" >&2 - - exit 1 - fi -} - -# Creates a Docker container to run the integration tests. -# -# This function starts a Docker container with a copy of the Nextcloud code from -# the grandparent directory, although ignoring any configuration or data that it -# may provide (for example, if that directory was used directly to deploy a -# Nextcloud instance in a web server). As the Nextcloud code is copied to the -# container instead of referenced the original code can be modified while the -# integration tests are running without interfering in them. -function prepareDocker() { - NEXTCLOUD_LOCAL_CONTAINER=nextcloud-local-test-integration - - NEXTCLOUD_LOCAL_CONTAINER_NETWORK_OPTIONS="" - if [ -n "$DATABASE_CONTAINER" ]; then - # The network stack is shared between the database and the Nextcloud - # container, so the Nextcloud server can access the database directly on - # 127.0.0.1. - NEXTCLOUD_LOCAL_CONTAINER_NETWORK_OPTIONS="--network=container:$DATABASE_CONTAINER" - fi - - 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 \ - --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)" - - # Setting the user and group of files in the tar would be superfluous, as - # "docker cp" does not take them into account (the extracted files are set - # to root). - echo "Copying local Git working directory of Nextcloud to the container" - tar --create --file="$NEXTCLOUD_LOCAL_TAR" \ - --exclude=".git" \ - --exclude="./config/config.php" \ - --exclude="./config/*.config.php" \ - --exclude="./data" \ - --exclude="./data-autotest" \ - --exclude="./tests" \ - --exclude="node_modules" \ - --directory=../../ \ - . - - docker exec $NEXTCLOUD_LOCAL_CONTAINER mkdir /nextcloud - docker cp - $NEXTCLOUD_LOCAL_CONTAINER:/nextcloud/ < "$NEXTCLOUD_LOCAL_TAR" - docker exec $NEXTCLOUD_LOCAL_CONTAINER chown -R www-data:www-data /nextcloud - - # Install Nextcloud in the container - docker exec -w /nextcloud $NEXTCLOUD_LOCAL_CONTAINER composer install - - # Database options are needed only when a database other than SQLite is - # used. - NEXTCLOUD_LOCAL_CONTAINER_INSTALL_DATABASE_OPTIONS="" - if [ -n "$DATABASE_CONTAINER" ]; then - NEXTCLOUD_LOCAL_CONTAINER_INSTALL_DATABASE_OPTIONS="--database=$DATABASE --database-name=$DATABASE_NAME --database-user=$DATABASE_USER --database-pass=$DATABASE_PASSWORD --database-host=127.0.0.1" - fi - - echo "Installing Nextcloud in the container" - docker exec --user www-data --workdir /nextcloud $NEXTCLOUD_LOCAL_CONTAINER php occ maintenance:install --admin-pass=admin $NEXTCLOUD_LOCAL_CONTAINER_INSTALL_DATABASE_OPTIONS -} - -# Removes/stops temporal elements created/started by this script. -function cleanUp() { - # Disable (yes, "+" disables) exiting immediately on errors to ensure that - # all the cleanup commands are executed (well, no errors should occur during - # the cleanup anyway, but just in case). - set +o errexit - - echo "Cleaning up" - - if [ -f "$NEXTCLOUD_LOCAL_TAR" ]; then - echo "Removing $NEXTCLOUD_LOCAL_TAR" - rm $NEXTCLOUD_LOCAL_TAR - fi - - # The name filter must be specified as "^/XXX$" to get an exact match; using - # just "XXX" would match every name that contained "XXX". - if [ -n "$(docker ps --all --quiet --filter name="^/$NEXTCLOUD_LOCAL_CONTAINER$")" ]; then - echo "Removing Docker container $NEXTCLOUD_LOCAL_CONTAINER" - docker rm --volumes --force $NEXTCLOUD_LOCAL_CONTAINER - fi - - if [ -n "$DATABASE_CONTAINER" -a -n "$(docker ps --all --quiet --filter name="^/$DATABASE_CONTAINER$")" ]; then - echo "Removing Docker container $DATABASE_CONTAINER" - docker rm --volumes --force $DATABASE_CONTAINER - fi -} - -# Exit immediately on errors. -set -o errexit - -# Execute cleanUp when the script exits, either normally or due to an error. -trap cleanUp EXIT - -# Ensure working directory is script directory, as some actions (like copying -# the Git working directory to the container) expect that. -cd "$(dirname $0)" - -# "--image XXX" option can be provided to set the Docker image to use to run -# the integration tests (one of the "nextcloud/continuous-integration-phpX.Y:latest" images). -NEXTCLOUD_LOCAL_IMAGE="ghcr.io/nextcloud/continuous-integration-php8.3:latest" -if [ "$1" = "--image" ]; then - NEXTCLOUD_LOCAL_IMAGE=$2 - - shift 2 -fi - -# "--database XXX" option can be provided to set the database to use to run the -# integration tests (one of "sqlite", "mysql" or "pgsql"; "sqlite" is used -# by default). -DATABASE="sqlite" -if [ "$1" = "--database" ]; then - DATABASE=$2 - - shift 2 -fi - -if [ "$DATABASE" != "sqlite" ] && [ "$DATABASE" != "mysql" ] && [ "$DATABASE" != "pgsql" ]; then - echo "--database must be followed by one of: sqlite, mysql or pgsql" - - exit 1 -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:8.4" -elif [ "$DATABASE" = "pgsql" ]; then - DATABASE_IMAGE="postgres:15" -fi -if [ "$1" = "--database-image" ]; then - DATABASE_IMAGE=$2 - - shift 2 -fi - -# If no parameter is provided to this script all the integration tests are run. -SCENARIO_TO_RUN=$1 - -setOperatingSystemAbstractionVariables - -prepareDatabase -prepareDocker - -echo "Running tests" -# --tty is needed to get colourful output. -docker exec --tty --user www-data -w "/nextcloud/build/integration" $NEXTCLOUD_LOCAL_CONTAINER bash -c "./run.sh $SCENARIO_TO_RUN" --- a/build/integration/run.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/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 - -BEHAT_EXECUTABLE=../../lib/composer/bin/behat -if [ ! -f "$BEHAT_EXECUTABLE" ]; then - echo "Behat executable not found. Please run 'composer install' in the root directory first." >&2 - exit 1 -fi - -OC_PATH=../../ -OCC=${OC_PATH}occ -TAGS="" -if [ "$1" = "--tags" ]; then - TAGS="--tags=$2" - - shift 2 -fi -SCENARIO_TO_RUN=$1 -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 - exit 1 - fi -fi -NC_DATADIR=$($OCC config:system:get datadirectory) - -# avoid port collision on jenkins - use $EXECUTOR_NUMBER -if [ -z "$EXECUTOR_NUMBER" ]; then - EXECUTOR_NUMBER=0 -fi -PORT=$((8080 + $EXECUTOR_NUMBER)) -echo $PORT -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 -export PORT_FED - -export TEST_SERVER_URL="http://localhost:$PORT/ocs/" -export TEST_SERVER_FED_URL="http://localhost:$PORT_FED/ocs/" - -if [ "$INSTALLED" == "true" ]; then - - #Enable external storage app - $OCC app:enable files_external user_ldap - - mkdir -p work/local_storage - OUTPUT_CREATE_STORAGE=`$OCC files_external:create local_storage local null::null -c datadir=$PWD/work/local_storage` - - ID_STORAGE=`echo $OUTPUT_CREATE_STORAGE | tr ' ' '\n' | tail -n1` - - $OCC files_external:option $ID_STORAGE enable_sharing true - -fi - -$BEHAT_EXECUTABLE \ - --strict \ - --colors \ - -f junit \ - -f pretty \ - $TAGS \ - $SCENARIO_TO_RUN - -RESULT=$? - -if [ "$INSTALLED" == "true" ]; then - - $OCC files_external:delete -y $ID_STORAGE - - #Disable external storage app - $OCC app:disable files_external user_ldap -fi - -if [ -z $HIDE_OC_LOGS ]; then - tail "${NC_DATADIR}/nextcloud.log" -fi - -echo "runsh: Exit code: $RESULT" -exit $RESULT --- a/build/integration/setup_features/setup.feature +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: setup - - Scenario: setup page is shown properly - When requesting "/index.php" with "GET" - Then the HTTP status code should be "200" --- a/build/integration/sharees_features/sharees.feature +++ /dev/null @@ -1,714 +0,0 @@ -# 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" - And user "test" exists - And user "Sharee1" exists - And group "ShareeGroup" exists - And user "test" belongs to group "ShareeGroup" - And user "Sharee2" exists - And As an "admin" - And sending "PUT" to "/cloud/users/Sharee2" with - | key | email | - | value | sharee2@system.com | - And sending "PUT" to "/cloud/users/Sharee2" with - | key | additional_mail | - | value | sharee2@secondary.com | - - Scenario: Search without exact match - Given As an "test" - When getting sharees for - | search | Sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search without exact match not-exact casing - Given As an "test" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search without exact match does not return disabled users - Given As an "admin" - And assure user "Sharee1" is disabled - And As an "test" - When getting sharees for - | search | Sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - denied - Given As an "test" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - allowed - Given As an "test" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And user "Sharee1" belongs to group "ShareeGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - allowed with exact match - Given As an "test" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And user "Sharee1" belongs to group "ShareeGroup" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - no group as non-member - Given As an "Sharee1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search when belonging to a group excluded from sharing - Given As an "test" - And parameter "shareapi_exclude_groups" of app "core" is set to "yes" - And parameter "shareapi_exclude_groups_list" of app "core" is set to "ShareeGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search when belonging to both a group excluded from sharing and another group - Given As an "test" - And group "AnotherGroup" exists - And user "test" belongs to group "AnotherGroup" - And parameter "shareapi_exclude_groups" of app "core" is set to "yes" - And parameter "shareapi_exclude_groups_list" of app "core" is set to "ShareeGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search when belonging to a group not excluded from sharing - Given As an "test" - And group "AnotherGroup" exists - And user "test" belongs to group "AnotherGroup" - And parameter "shareapi_exclude_groups" of app "core" is set to "yes" - And parameter "shareapi_exclude_groups_list" of app "core" is set to "ExcludedGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search when not belonging to a group allowed to share - Given As an "test" - And group "AnotherGroup" exists - And parameter "shareapi_exclude_groups" of app "core" is set to "allow" - And parameter "shareapi_exclude_groups_list" of app "core" is set to "AnotherGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search when belonging to both a group allowed to share and another group - Given As an "test" - And group "AnotherGroup" exists - And user "test" belongs to group "AnotherGroup" - And parameter "shareapi_exclude_groups" of app "core" is set to "allow" - And parameter "shareapi_exclude_groups_list" of app "core" is set to "AnotherGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search without exact match no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | Sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match group no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | ShareeGroup | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match - Given As an "test" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with exact match does not return disabled users - Given As an "admin" - And assure user "Sharee1" is disabled - And As an "test" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match not-exact casing - Given As an "test" - When getting sharees for - | search | sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with exact match not-exact casing group - Given As an "test" - When getting sharees for - | search | shareegroup | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with "self" - Given As an "Sharee1" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Remote sharee for files - Given As an "test" - When getting sharees for - | search | test@localhost | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned are - | test (localhost) | 6 | test@localhost | - Then "remotes" sharees returned is empty - - Scenario: Remote sharee for calendars - Given As an "test" - When getting sharees for - | search | test@localhost | - | itemType | calendar | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned are - | test (localhost) | 6 | test@localhost | - Then "remotes" sharees returned is empty - - Scenario: Group sharees not returned when group sharing is disabled - Given As an "test" - And parameter "shareapi_allow_group_sharing" of app "core" is set to "no" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search user by system e-mail address - Given As an "test" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - # MailPlugin does not add a result if there is already one for that user. - And "exact users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by system e-mail address does not return disabled users - Given As an "admin" - And assure user "Sharee2" is disabled - And As an "test" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by system e-mail address without exact match - Given As an "test" - When getting sharees for - | search | sharee2@system.c | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - # MailPlugin does not add a result if there is already one for that user. - And "users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by secondary e-mail address - Given As an "test" - When getting sharees for - | search | sharee2@secondary.com | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - # UserPlugin only searches in the system e-mail address, but not in - # secondary addresses. - And "exact users" sharees returned are - | Sharee2 (sharee2@secondary.com) | 0 | Sharee2 | sharee2@secondary.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by secondary e-mail address without exact match - Given As an "test" - When getting sharees for - | search | sharee2@secondary.c | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - # UserPlugin only searches in the system e-mail address, but not in - # secondary addresses. - # MailPlugin adds a result for every e-mail address of the contact unless - # there is an exact match. - And "users" sharees returned are - | Sharee2 (sharee2@system.com) | 0 | Sharee2 | sharee2@system.com | - | Sharee2 (sharee2@secondary.com) | 0 | Sharee2 | sharee2@secondary.com | - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by system e-mail address with e-mail full match disabled - Given As an "test" - And parameter "shareapi_restrict_user_enumeration_full_match_email" of app "core" is set to "no" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - # With enumeration allowed the user is still found as non-exact match, as - # enumeration also searches in the e-mail address - And "users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user by system e-mail address with e-mail full match and user enumeration disabled - Given As an "test" - And parameter "shareapi_restrict_user_enumeration_full_match_email" of app "core" is set to "no" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareType | 0 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - # The user id full match must not resolve e-mail addresses through the user - # backends (login via e-mail address) - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search e-mail - Given As an "test" - When getting sharees for - | search | sharee2@unknown.com | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned are - | sharee2@unknown.com | 4 | sharee2@unknown.com | - And "emails" sharees returned is empty - - Scenario: Search e-mail when sharebymail app is disabled - Given app "sharebymail" enabled state will be restored once the scenario finishes - And sending "DELETE" to "/cloud/apps/sharebymail" - And app "sharebymail" is disabled - And As an "test" - When getting sharees for - | search | sharee2@unknown.com | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search e-mail matching system e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search e-mail partially matching system e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@system.c | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned are - | sharee2@system.c | 4 | sharee2@system.c | - And "emails" sharees returned is empty - - Scenario: Search e-mail matching secondary e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@secondary.com | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search e-mail partially matching secondary e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@secondary.c | - | itemType | file | - | shareType | 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned are - | sharee2@secondary.c | 4 | sharee2@secondary.c | - And "emails" sharees returned is empty - - Scenario: Search user and e-mail matching system e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareTypes | 0 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user and e-mail matching system e-mail address of user when sharebymail app is disabled - Given app "sharebymail" enabled state will be restored once the scenario finishes - And sending "DELETE" to "/cloud/apps/sharebymail" - And app "sharebymail" is disabled - And As an "test" - When getting sharees for - | search | sharee2@system.com | - | itemType | file | - | shareTypes | 0 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee2 | 0 | Sharee2 | sharee2@system.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user and e-mail matching secondary e-mail address of user - Given As an "test" - When getting sharees for - | search | sharee2@secondary.com | - | itemType | file | - | shareTypes | 0 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee2 (sharee2@secondary.com) | 0 | Sharee2 | sharee2@secondary.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty - - Scenario: Search user and e-mail matching secondary e-mail address of user when sharebymail app is disabled - Given app "sharebymail" enabled state will be restored once the scenario finishes - And sending "DELETE" to "/cloud/apps/sharebymail" - And app "sharebymail" is disabled - And As an "test" - When getting sharees for - | search | sharee2@secondary.com | - | itemType | file | - | shareTypes | 0 4 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee2 (sharee2@secondary.com) | 0 | Sharee2 | sharee2@secondary.com | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - And "exact emails" sharees returned is empty - And "emails" sharees returned is empty --- a/build/integration/sharees_features/sharees_provisioningapiv2.feature +++ /dev/null @@ -1,243 +0,0 @@ -# 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" - And user "test" exists - And user "Sharee1" exists - And group "ShareeGroup" exists - And user "test" belongs to group "ShareeGroup" - - Scenario: Search without exact match - Given As an "test" - When getting sharees for - | search | Sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search without exact match not-exact casing - Given As an "test" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - denied - Given As an "test" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - allowed - Given As an "test" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And user "Sharee1" belongs to group "ShareeGroup" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "exact groups" sharees returned is empty - And "groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search only with group members - no group as non-member - Given As an "Sharee1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search without exact match no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | Sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "users" sharees returned is empty - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match group no iteration allowed - Given As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | ShareeGroup | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - And "exact groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty - - Scenario: Search with exact match - Given As an "test" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with exact match not-exact casing - Given As an "test" - When getting sharees for - | search | sharee1 | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with exact match not-exact casing group - Given As an "test" - When getting sharees for - | search | shareegroup | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned are - | ShareeGroup | 1 | ShareeGroup | - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Search with "self" - Given As an "Sharee1" - When getting sharees for - | search | Sharee1 | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned is empty - Then "remotes" sharees returned is empty - - Scenario: Remote sharee for files - Given As an "test" - When getting sharees for - | search | test@localhost | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned are - | test (localhost) | 6 | test@localhost | - Then "remotes" sharees returned is empty - - Scenario: Remote sharee for calendars - Given As an "test" - When getting sharees for - | search | test@localhost | - | itemType | calendar | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - Then "exact users" sharees returned is empty - Then "users" sharees returned is empty - Then "exact groups" sharees returned is empty - Then "groups" sharees returned is empty - Then "exact remotes" sharees returned are - | test (localhost) | 6 | test@localhost | - Then "remotes" sharees returned is empty - - Scenario: Group sharees not returned when group sharing is disabled - Given As an "test" - And parameter "shareapi_allow_group_sharing" of app "core" is set to "no" - When getting sharees for - | search | sharee | - | itemType | file | - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Sharee1 | 0 | Sharee1 | Sharee1 | - And "exact groups" sharees returned is empty - And "groups" sharees returned is empty - And "exact remotes" sharees returned is empty - And "remotes" sharees returned is empty --- a/build/integration/sharees_features/sharees_user.feature +++ /dev/null @@ -1,531 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: sharees_user - - Background: - Given using api version "1" - - Scenario: Search for userid returns exact user - Given user "test" with displayname "Test" exists - And user "user1" exists - And As an "user1" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test | test | - And "users" sharees returned is empty - - Scenario: Search for userid returns exact user without sharee enumeration - Given user "test" with displayname "Test" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test | test | - And "users" sharees returned is empty - - Scenario: Search for userid without shared group returns nothing with sharing in group only - Given user "test" with displayname "Test" exists - And group "test-group" exists - And user "test" belongs to group "test-group" - And user "user1" exists - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for userid without shared group returns nothing with sharing in group only and without sharee enumeration - Given user "test" with displayname "Test" exists - And group "test-group" exists - And user "test" belongs to group "test-group" - And user "user1" exists - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for userid with shared group returns exact user with sharing in group only - Given user "test" with displayname "Test" exists - And group "test-group" exists - And user "test" belongs to group "test-group" - And user "user1" exists - And user "user1" belongs to group "test-group" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test | test | - And "users" sharees returned is empty - - Scenario: Search for userid with shared group returns exact user with sharing in group only and without sharee enumeration - Given user "test" with displayname "Test" exists - And group "test-group" exists - And user "test" belongs to group "test-group" - And user "user1" exists - And user "user1" belongs to group "test-group" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test | test | - And "users" sharees returned is empty - - Scenario: Search for part of userid returns wide user - Given user "test1" with displayname "Test One" exists - And user "user1" exists - And As an "user1" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - - Scenario: Search for part of userid returns nothing without sharee enumeration - Given user "test1" with displayname "Test One" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for part of userid returns wide users - Given user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And user "user1" exists - And As an "user1" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - | Test Two | 0 | test2 | test2 | - - Scenario: Search for part of userid returns nothing without sharee enumeration - Given user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for part of displayname returns exact user and wide users - Given user "test0" with displayname "Test" exists - And user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And user "user1" exists - And As an "user1" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test0 | test0 | - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - | Test Two | 0 | test2 | test2 | - - Scenario: Search for part of displayname returns exact user without sharee enumeration - Given user "test0" with displayname "Test" exists - And user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test | 0 | test0 | test0 | - And "users" sharees returned is empty - - Scenario: Search for part of userid with shared group returns wide user with sharing in group only - Given user "test1" with displayname "Test One" exists - And group "abc" exists - And user "test1" belongs to group "abc" - And group "xyz" exists - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - - Scenario: Search for part of userid with shared group returns nothing with sharing in group only and without sharee enumeration - Given user "test1" with displayname "Test One" exists - And group "abc" exists - And user "test1" belongs to group "abc" - And group "xyz" exists - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for part of userid with shared groups returns wide users with sharing in group only - Given user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "abc" exists - And user "test1" belongs to group "abc" - And user "test2" belongs to group "abc" - And group "xyz" exists - And user "test1" belongs to group "xyz" - And user "test2" belongs to group "xyz" - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - | Test Two | 0 | test2 | test2 | - - Scenario: Search for part of userid with shared groups returns nothing with sharing in group only and without sharee enumeration - Given user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "abc" exists - And user "test1" belongs to group "abc" - And user "test2" belongs to group "abc" - And group "xyz" exists - And user "test1" belongs to group "xyz" - And user "test2" belongs to group "xyz" - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for part of userid with shared groups returns exact user and wide user with sharing in group only - Given user "test" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "abc" exists - And user "test" belongs to group "abc" - And group "xyz" exists - And user "test2" belongs to group "xyz" - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test | test | - And "users" sharees returned are - | Test Two | 0 | test2 | test2 | - - Scenario: Search for part of userid with shared groups returns exact user with sharing in group only and without sharee enumeration - Given user "test" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "abc" exists - And user "test" belongs to group "abc" - And group "xyz" exists - And user "test2" belongs to group "xyz" - And user "user1" exists - And user "user1" belongs to group "abc" - And user "user1" belongs to group "xyz" - And As an "user1" - And parameter "shareapi_only_share_with_group_members" of app "core" is set to "yes" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test | test | - And "users" sharees returned is empty - - Scenario: Search for part of userid with shared group returns wide user with sharee enumeration limited to group - Given user "test" with displayname "foo" exists - And user "test1" exists - And user "test2" exists - And group "groupA" exists - And group "groupB" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupB" - And As an "test" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | test1 | 0 | test1 | test1 | - - Scenario: Search for exact userid with shared group returns nothing without sharee enumeration and without full match userid enumeration - Given user "test" with displayname "foo" exists - And user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "groupA" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupA" - And As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - And parameter "shareapi_restrict_user_enumeration_full_match_user_id" of app "core" is set to "no" - When getting sharees for - | search | test1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for displayname returns exact user without sharee enumeration and without full match userid enumeration - Given user "test" with displayname "foo" exists - And user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "groupA" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupA" - And As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - And parameter "shareapi_restrict_user_enumeration_full_match_user_id" of app "core" is set to "no" - When getting sharees for - | search | Test One | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test1 | test1 | - And "users" sharees returned is empty - - Scenario: Search for part of displayname returns exact user without sharee enumeration and with ignoring full match of second displayname - Given user "test" with displayname "foo" exists - And user "test1" with displayname "Test One (Second displayname for user 1)" exists - And user "test2" with displayname "Test Two (Second displayname for user 2)" exists - And group "groupA" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupA" - And As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - And parameter "shareapi_restrict_user_enumeration_full_match_ignore_second_dn" of app "core" is set to "yes" - When getting sharees for - | search | Test One | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One (Second displayname for user 1) | 0 | test1 | test1 | - And "users" sharees returned is empty - - Scenario: Search for displayname returns nothing without sharee enumeration and without full match displayname enumeration - Given user "test" with displayname "foo" exists - And user "test1" with displayname "Test One" exists - And user "test2" with displayname "Test Two" exists - And group "groupA" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupA" - And As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - And parameter "shareapi_restrict_user_enumeration_full_match_displayname" of app "core" is set to "no" - When getting sharees for - | search | Test One | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for exact userid returns exact user without sharee enumeration and without full match displayname enumeration - Given user "test" with displayname "foo" exists - And user "test1" with displayname "Test One" exists - And As an "test" - And parameter "shareapi_allow_share_dialog_user_enumeration" of app "core" is set to "no" - And parameter "shareapi_restrict_user_enumeration_full_match_displayname" of app "core" is set to "no" - When getting sharees for - | search | test1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test1 | test1 | - And "users" sharees returned is empty - - Scenario: Search for exact userid with shared group returns exact user with sharee enumeration limited to group - Given user "test" with displayname "foo" exists - And user "test1" exists - And user "test2" exists - And group "groupA" exists - And group "groupB" exists - And user "test" belongs to group "groupA" - And user "test1" belongs to group "groupA" - And user "test2" belongs to group "groupB" - And As an "test" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | test1 | 0 | test1 | test1 | - And "users" sharees returned is empty - - Scenario: Search for part of userid with shared group returns wide user with sharee enumeration limited to group - Given user "test1" with displayname "Test One" exists - And group "test-group" exists - And user "test1" belongs to group "test-group" - And user "user1" exists - And user "user1" belongs to group "test-group" - And As an "user1" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned are - | Test One | 0 | test1 | test1 | - - Scenario: Search for part of userid without shared group returns nothing with sharee enumeration limited to group - Given user "test1" with displayname "Test One" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned is empty - And "users" sharees returned is empty - - Scenario: Search for exact userid without shared group returns exact user with sharee enumeration limited to group - Given user "test1" with displayname "Test One" exists - And user "user1" exists - And As an "user1" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test1 | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test1 | test1 | - And "users" sharees returned is empty - - Scenario: Search for exact email without shared group returns exact user with sharee enumeration limited to group - Given user "test1" with displayname "Test One" exists - And As an "admin" - And sending "PUT" to "/cloud/users/test1" with - | key | email | - | value | test@example.com | - And user "user1" exists - And As an "user1" - And parameter "shareapi_restrict_user_enumeration_to_group" of app "core" is set to "yes" - When getting sharees for - | search | test@example.com | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One | 0 | test1 | test@example.com | - And "users" sharees returned is empty - - Scenario: Search for exact additional email returns exact user - Given user "test1" with displayname "Test One" exists - And As an "admin" - And sending "PUT" to "/cloud/users/test1" with - | key | email | - | value | test@example.com | - And sending "PUT" to "/cloud/users/test1" with - | key | additional_mail | - | value | test@example.org | - And user "user1" exists - And As an "user1" - When getting sharees for - | search | test@example.org | - | itemType | file | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And "exact users" sharees returned are - | Test One (test@example.org) | 0 | test1 | test@example.org | - And "users" sharees returned is empty --- a/build/integration/sharing_features/sharing-accept.feature +++ /dev/null @@ -1,67 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -Feature: sharing-accept - Background: - Given using api version "1" - Given using old dav path - - Scenario: Accepting a non-existent share - Given user "user0" exists - And As an "user0" - When sending "GET" with exact url to "/index.php/apps/files_sharing/accept/ocinternal:999999" - Then the HTTP status code should be "404" - - Scenario: Accepting a share with an invalid share ID - Given user "user0" exists - And As an "user0" - When sending "GET" with exact url to "/index.php/apps/files_sharing/accept/invalid:format" - Then the HTTP status code should be "404" - - Scenario: Accepting a valid share - Given user "user0" exists - And user "user1" exists - 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 As an "user1" - And Logging in using web as "user1" - When opening last share accept confirmation page - Then the HTTP status code should be "200" - When the CSRF token is extracted from the previous response - And accepting last share via the accept endpoint with requesttoken - Then the HTTP status code should be "200" - - Scenario: Accepting a share as a different user - Given user "user0" exists - And user "user1" exists - And user "user2" exists - 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 As an "user2" - When accepting last share via the accept endpoint - Then the HTTP status code should be "404" - - Scenario: Accepting a share that is not a user share - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And As an "user0" - And User user0 created a folder drop - When creating a share with - | path | drop | - | shareType | 3 | - | publicUpload | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And As an "user2" - When accepting last share via the accept endpoint - Then the HTTP status code should be "404" --- a/build/integration/sharing_features/sharing-activity.feature +++ /dev/null @@ -1,46 +0,0 @@ -# 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 | --- a/build/integration/sharing_features/sharing-v1-part2.feature +++ /dev/null @@ -1,1409 +0,0 @@ -# SPDX-FileCopyrightText: 2019 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 - -# See sharing-v1.feature - - Scenario: getting all shares of a file with reshares - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0 (2).txt" of user "user1" is shared with user "user2" - And As an "user0" - When sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=textfile0.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user1" should be included in the response - And User "user2" should be included in the response - And User "user3" should not be included in the response - -Scenario: getting all shares of a file with reshares with link share with less permissions - Given user "user0" exists - And user "user1" exists - When as "user0" creating a share with - | path | textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - When as "user0" creating a share with - | path | textfile0.txt | - | shareType | 3 | - | permissions | 19 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last link share can be downloaded - When As an "user1" - And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=textfile0 (2).txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user1" should not be included in the response - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 3 | - | uid_owner | user0 | - | token | | - | url | | - | permissions | 19 | - - Scenario: getting all shares of a file with a received share after revoking the resharing rights - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And user "user0" accepts last share - And Updating last share with - | permissions | 1 | - And file "textfile0.txt" of user "user1" is shared with user "user2" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/textfile0 (2).txt | - | file_target | /textfile0 (2).txt | - | share_with | user2 | - | share_with_displayname | user2 | - -Scenario: getting all shares of a file with a received share after revoking the resharing rights with delayed share check - Given user "user0" exists - And parameter "update_cutoff_time" of app "files_sharing" is set to "0" - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And user "user0" accepts last share - And Updating last share with - | permissions | 1 | - And file "textfile0.txt" of user "user1" is shared with user "user2" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/textfile0 (2).txt | - | file_target | /textfile0.txt | - | share_with | user2 | - | share_with_displayname | user2 | - # After user2 does an FS setup the share is renamed - When As an "user2" - And Downloading file "/textfile0 (2).txt" with range "bytes=10-18" - Then Downloaded content should be "test text" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/textfile0 (2).txt | - | file_target | /textfile0 (2).txt | - | share_with | user2 | - | share_with_displayname | user2 | - - Scenario: getting all shares of a file with a received share also reshared after revoking the resharing rights - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And user "user0" accepts last share - And save the last share data as "textfile0.txt from user1" - And file "textfile0 (2).txt" of user "user0" is shared with user "user3" - And restore the last share data from "textfile0.txt from user1" - And Updating last share with - | permissions | 1 | - And file "textfile0.txt" of user "user1" is shared with user "user2" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt" - Then the list of returned shares has 2 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | uid_file_owner | user1 | - | displayname_file_owner | user1 | - | path | /textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/textfile0 (2).txt | - | file_target | /textfile0 (2).txt | - | share_with | user3 | - | share_with_displayname | user3 | - And share 1 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/textfile0 (2).txt | - | file_target | /textfile0 (2).txt | - | share_with | user2 | - | share_with_displayname | user2 | - - Scenario: Reshared files can be still accessed if a user in the middle removes it. - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0 (2).txt" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And file "textfile0 (2).txt" of user "user2" is shared with user "user3" - And user "user3" accepts last share - And As an "user1" - When User "user1" deletes file "/textfile0 (2).txt" - And As an "user3" - And Downloading file "/textfile0 (2).txt" with range "bytes=1-8" - Then Downloaded content should be "extcloud" - - Scenario: getting share info of a share - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user0" - When 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 - | id | A_NUMBER | - | item_type | file | - | item_source | A_NUMBER | - | share_type | 0 | - | share_with | user1 | - | file_source | A_NUMBER | - | file_target | /textfile0 (2).txt | - | path | /textfile0.txt | - | permissions | 19 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | share_with_displayname | user1 | - | displayname_owner | user0 | - | mimetype | text/plain | - - Scenario: getting share info of a group share - Given user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And file "textfile0.txt" of user "user0" is shared with group "group1" - And As an "user0" - When 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 - | id | A_NUMBER | - | item_type | file | - | item_source | A_NUMBER | - | share_type | 1 | - | share_with | group1 | - | file_source | A_NUMBER | - | file_target | /textfile0.txt | - | path | /textfile0.txt | - | permissions | 19 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | share_with_displayname | group1 | - | displayname_owner | user0 | - | mimetype | text/plain | - And As an "user1" - And accepting last share - And Getting info of last share - And 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 | 1 | - | share_with | group1 | - | file_source | A_NUMBER | - | file_target | /textfile0 (2).txt | - | path | /textfile0 (2).txt | - | permissions | 19 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | shared::/textfile0 (2).txt | - | file_parent | A_NUMBER | - | share_with_displayname | group1 | - | displayname_owner | user0 | - | mimetype | text/plain | - - Scenario: getting all shares including subfiles in a directory - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "PARENT/CHILD" of user "user0" is shared with user "user1" - And file "PARENT/parent.txt" of user "user0" is shared with user "user2" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=PARENT" - Then the list of returned shares has 2 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | path | /PARENT/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | home::user0 | - | file_target | /CHILD | - | share_with | user1 | - | share_with_displayname | user1 | - | permissions | 31 | - And share 1 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | path | /PARENT/parent.txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | home::user0 | - | file_target | /parent.txt | - | share_with | user2 | - | share_with_displayname | user2 | - - Scenario: getting all shares including subfiles in a directory with received shares - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0.txt" of user "user1" is shared with user "user0" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=/" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | path | /textfile0.txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | home::user0 | - | file_target | /textfile0 (2).txt | - | share_with | user1 | - | share_with_displayname | user1 | - - Scenario: getting all shares including subfiles in a directory with shares in subdirectories - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "PARENT/CHILD" of user "user0" is shared with user "user1" - And file "PARENT/CHILD/child.txt" of user "user0" is shared with user "user2" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=PARENT" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | path | /PARENT/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | home::user0 | - | file_target | /CHILD | - | share_with | user1 | - | share_with_displayname | user1 | - | permissions | 31 | - - Scenario: getting all shares including subfiles in a shared directory with reshares - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "PARENT" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "PARENT (2)/CHILD" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And file "CHILD" of user "user2" is shared with user "user3" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=PARENT" - Then the list of returned shares has 2 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | uid_file_owner | user0 | - | displayname_file_owner | user0 | - | path | /PARENT/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | home::user0 | - | file_target | /CHILD | - | share_with | user2 | - | share_with_displayname | user2 | - | permissions | 31 | - And share 1 is returned with - | share_type | 0 | - | uid_owner | user2 | - | displayname_owner | user2 | - | uid_file_owner | user0 | - | displayname_file_owner | user0 | - | path | /PARENT/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | home::user0 | - | file_target | /CHILD | - | share_with | user3 | - | share_with_displayname | user3 | - | permissions | 31 | - - Scenario: getting all shares including subfiles in a directory by a resharer - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "PARENT" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "PARENT (2)/CHILD" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And file "CHILD" of user "user2" is shared with user "user3" - When As an "user1" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=PARENT (2)" - Then the list of returned shares has 2 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | uid_file_owner | user0 | - | displayname_file_owner | user0 | - | path | /PARENT (2)/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | shared::/PARENT (2) | - | file_target | /CHILD | - | share_with | user2 | - | share_with_displayname | user2 | - | permissions | 31 | - And share 1 is returned with - | share_type | 0 | - | uid_owner | user2 | - | displayname_owner | user2 | - | uid_file_owner | user0 | - | displayname_file_owner | user0 | - | path | /PARENT (2)/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | shared::/PARENT (2) | - | file_target | /CHILD | - | share_with | user3 | - | share_with_displayname | user3 | - | permissions | 31 | - - Scenario: getting all shares including subfiles in a directory by a resharer after revoking the resharing rights - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "PARENT" of user "user0" is shared with user "user1" - And save the last share data as "parent folder" - And user "user1" accepts last share - And file "PARENT (2)/CHILD" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And file "CHILD" of user "user2" is shared with user "user3" - And As an "user0" - And restore the last share data from "parent folder" - And Updating last share with - | permissions | 1 | - When As an "user1" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=PARENT (2)" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | uid_file_owner | user0 | - | displayname_file_owner | user0 | - | path | /PARENT (2)/CHILD | - | item_type | folder | - | mimetype | httpd/unix-directory | - | storage_id | shared::/PARENT (2) | - | file_target | /CHILD | - | share_with | user2 | - | share_with_displayname | user2 | - | permissions | 31 | - - Scenario: getting all shares including subfiles in a directory after moving a received share not reshareable also shared with another user - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And user "user0" accepts last share - And Updating last share with - | permissions | 1 | - And file "textfile0.txt" of user "user1" is shared with user "user2" - And User "user0" moved file "/textfile0 (2).txt" to "/FOLDER/textfile0.txt" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=/FOLDER" - Then the list of returned shares has 1 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /FOLDER/textfile0.txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/FOLDER/textfile0.txt | - | file_target | /textfile0 (2).txt | - | share_with | user2 | - | share_with_displayname | user2 | - - Scenario: getting all shares including subfiles in a directory after moving a share and a received share not reshareable also shared with another user - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0.txt" of user "user1" is shared with user "user0" - And user "user0" accepts last share - And Updating last share with - | permissions | 1 | - And file "textfile0.txt" of user "user1" is shared with user "user2" - And User "user0" moved file "/textfile0.txt" to "/FOLDER/textfile0.txt" - And User "user0" moved file "/textfile0 (2).txt" to "/FOLDER/textfile0 (2).txt" - When As an "user0" - And sending "GET" to "/apps/files_sharing/api/v1/shares?subfiles=true&path=/FOLDER" - Then the list of returned shares has 2 shares - And share 0 is returned with - | share_type | 0 | - | uid_owner | user0 | - | displayname_owner | user0 | - | path | /FOLDER/textfile0.txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | home::user0 | - | file_target | /textfile0 (2).txt | - | share_with | user1 | - | share_with_displayname | user1 | - And share 1 is returned with - | share_type | 0 | - | uid_owner | user1 | - | displayname_owner | user1 | - | path | /FOLDER/textfile0 (2).txt | - | item_type | file | - | mimetype | text/plain | - | storage_id | shared::/FOLDER/textfile0 (2).txt | - | file_target | /textfile0 (2).txt | - | share_with | user2 | - | share_with_displayname | user2 | - - Scenario: keep group permissions in sync - Given As an "admin" - Given user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And file "textfile0.txt" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And User "user1" moved file "/textfile0 (2).txt" to "/FOLDER/textfile0.txt" - And As an "user0" - When Updating last share with - | permissions | 1 | - 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 - | id | A_NUMBER | - | item_type | file | - | item_source | A_NUMBER | - | share_type | 1 | - | file_source | A_NUMBER | - | file_target | /textfile0.txt | - | permissions | 1 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | mimetype | text/plain | - And As an "user1" - And Getting info of last share - And 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 | 1 | - | file_source | A_NUMBER | - | file_target | /FOLDER/textfile0.txt | - | permissions | 1 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | shared::/FOLDER/textfile0.txt | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | mimetype | text/plain | - - Scenario: Sharee can see the share - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user1" - 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 - - Scenario: Sharee can see the filtered share - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And file "textfile1.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user1" - When sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true&path=textfile1 (2).txt" - 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 - - Scenario: Sharee can't see the share that is filtered out - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile1.txt" of user "user0" is shared with user "user1" - And As an "user1" - When sending "GET" to "/apps/files_sharing/api/v1/shares?shared_with_me=true&path=textfile0 (2).txt" - Then 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: Sharee can see the group share - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group0" exists - And user "user1" belongs to group "group0" - And file "textfile0.txt" of user "user0" is shared with group "group0" - And As an "user1" - 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 - - 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 - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 8 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile0 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: User is allowed to reshare file with more permissions if shares of same file to same user have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 1 | - | shareWith | group1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - And As an "user1" - And accepting last share - 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" - - Scenario: User is not allowed to reshare file with more permissions - As an "admin" - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 16 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile0 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: User is not allowed to reshare file with more permissions even if shares of same file to other users have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user3 | - | permissions | 15 | - And As an "user3" - And accepting last share - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile0 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 19 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: User is not allowed to reshare file with more permissions even if shares of other files from same user have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And As an "user0" - And creating a share with - | path | /textfile1.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile1 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 19 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: User is not allowed to reshare file with more permissions even if shares of other files from other users have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And As an "user3" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And As an "user0" - And creating a share with - | path | /textfile1.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile1 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 19 | - Then the OCS status code should be "403" - 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 - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /PARENT | - | shareType | 0 | - | shareWith | user1 | - | permissions | 16 | - And As an "user1" - And accepting last share - When creating a share with - | path | /PARENT (2) | - | shareType | 0 | - | shareWith | user2 | - | permissions | 25 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: User is not allowed to reshare file with additional delete permissions for files - As an "admin" - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /textfile0.txt | - | shareType | 0 | - | shareWith | user1 | - | permissions | 16 | - And As an "user1" - And accepting last share - When creating a share with - | path | /textfile0 (2).txt | - | shareType | 0 | - | shareWith | user2 | - | permissions | 25 | - 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 - | id | A_NUMBER | - | item_type | file | - | item_source | A_NUMBER | - | share_type | 0 | - | share_with | user2 | - | file_source | A_NUMBER | - | file_target | /textfile0 (2).txt | - | path | /textfile0 (2).txt | - | permissions | 17 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user1 | - | storage_id | shared::/textfile0 (2).txt | - | file_parent | A_NUMBER | - | share_with_displayname | user2 | - | displayname_owner | user1 | - | mimetype | text/plain | - - Scenario: Get a share with a user which didn't received the share - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user2" - When Getting info of last share - Then the OCS status code should be "404" - And the HTTP status code should be "200" - - Scenario: Get a share with a user with resharing rights - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0.txt" of user "user0" is shared with user "user2" - And As an "user1" - When 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 - | id | A_NUMBER | - | item_type | file | - | item_source | A_NUMBER | - | share_type | 0 | - | share_with | user2 | - | file_source | A_NUMBER | - | file_target | /textfile0 (2).txt | - | path | /textfile0 (2).txt | - | permissions | 19 | - | stime | A_NUMBER | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | shared::/textfile0 (2).txt | - | file_parent | A_NUMBER | - | share_with_displayname | user2 | - | displayname_owner | user0 | - | mimetype | text/plain | - - Scenario: Share of folder and sub-folder to same user - core#20645 - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group0" exists - And user "user1" belongs to group "group0" - And file "/PARENT" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When file "/PARENT/CHILD" of user "user0" is shared with group "group0" - And user "user1" accepts last share - Then user "user1" should see following elements - | /FOLDER/ | - | /PARENT/ | - | /PARENT/CHILD/ | - | /PARENT/parent.txt | - | /PARENT/CHILD/child.txt | - | /PARENT%20(2)/ | - | /PARENT%20(2)/CHILD/ | - | /PARENT%20(2)/parent.txt | - | /PARENT%20(2)/CHILD/child.txt | - | /CHILD/ | - | /CHILD/child.txt | - And the HTTP status code should be "200" - - Scenario: Share a file by multiple channels - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user2" exists - And group "group0" exists - And user "user1" belongs to group "group0" - And user "user2" belongs to group "group0" - And user "user0" created a folder "/common" - And user "user0" created a folder "/common/sub" - And file "common" of user "user0" is shared with group "group0" - And user "user1" accepts last share - And user "user2" accepts last share - And file "textfile0.txt" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And User "user1" moved file "/textfile0.txt" to "/common/textfile0.txt" - And User "user1" moved file "/common/textfile0.txt" to "/common/sub/textfile0.txt" - And As an "user2" - When Downloading file "/common/sub/textfile0.txt" with range "bytes=10-18" - Then Downloaded content should be "test text" - And Downloaded content when downloading file "/textfile0.txt" with range "bytes=10-18" should be "test text" - And user "user2" should see following elements - | /common/sub/textfile0.txt | - - Scenario: Share a file by multiple channels - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user2" exists - And group "group0" exists - And user "user1" belongs to group "group0" - And user "user2" belongs to group "group0" - And user "user0" created a folder "/common" - And user "user0" created a folder "/common/sub" - And file "common" of user "user0" is shared with group "group0" - And user "user1" accepts last share - And user "user2" accepts last share - And file "textfile0.txt" of user "user1" is shared with user "user2" - And user "user2" accepts last share - And User "user1" moved file "/textfile0.txt" to "/common/textfile0.txt" - And User "user1" moved file "/common/textfile0.txt" to "/common/sub/textfile0.txt" - And As an "user2" - When Downloading file "/textfile0 (2).txt" with range "bytes=10-18" - Then Downloaded content should be "test text" - And user "user2" should see following elements - | /common/sub/textfile0.txt | - - Scenario: Delete all group shares - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And file "textfile0.txt" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And User "user1" moved file "/textfile0 (2).txt" to "/FOLDER/textfile0.txt" - And As an "user0" - And Deleting last share - And As an "user1" - 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 not included in the answer - - Scenario: delete a share - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user0" - When Deleting last share - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: delete a share with a user that didn't receive the share - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user2" - When Deleting last share - Then the OCS status code should be "404" - And the HTTP status code should be "200" - - Scenario: delete a share with a user with resharing rights that didn't receive the share - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0.txt" of user "user0" is shared with user "user2" - And As an "user1" - When Deleting last share - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Keep usergroup shares (#22143) - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user2" exists - And group "group" exists - And user "user1" belongs to group "group" - And user "user2" belongs to group "group" - And user "user0" created a folder "/TMP" - And file "TMP" of user "user0" is shared with group "group" - And user "user1" accepts last share - And user "user2" accepts last share - And user "user1" created a folder "/myFOLDER" - And User "user1" moves file "/TMP" to "/myFOLDER/myTMP" - And user "user2" does not exist - And user "user1" should see following elements - | /myFOLDER/myTMP/ | - - Scenario: Check quota of owners parent directory of a shared file - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user1" exists - And user "user1" has a quota of "0" - And User "user0" moved file "/welcome.txt" to "/myfile.txt" - And file "myfile.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When User "user1" uploads file "data/textfile.txt" to "/myfile.txt" - Then the HTTP status code should be "204" - - Scenario: Don't allow sharing of the root - Given user "user0" exists - And As an "user0" - When creating a share with - | path | / | - | shareType | 3 | - Then the OCS status code should be "403" - - Scenario: Allow modification of reshare - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user0" created a folder "/TMP" - And file "TMP" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "TMP" of user "user1" is shared with user "user2" - And As an "user1" - When Updating last share with - | permissions | 1 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Allow reshare to exceed permissions if shares of same file to same user have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 1 | - | shareWith | group1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 17 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Do not allow reshare to exceed permissions - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 21 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 21 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Do not allow reshare to exceed permissions even if shares of same file to other users have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user3 | - | permissions | 15 | - And As an "user3" - And accepting last share - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 21 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 21 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Do not allow reshare to exceed permissions even if shares of other files from same user have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And As an "user0" - And creating a share with - | path | /FOLDER | - | shareType | 0 | - | shareWith | user1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 21 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 21 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Do not allow reshare to exceed permissions even if shares of other files from other users have them - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And As an "user3" - And creating a share with - | path | /FOLDER | - | shareType | 0 | - | shareWith | user1 | - | permissions | 15 | - And As an "user1" - And accepting last share - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 21 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 21 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Do not allow sub reshare to exceed permissions - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user0" created a folder "/TMP" - And user "user0" created a folder "/TMP/SUB" - And As an "user0" - And creating a share with - | path | /TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 21 | - And As an "user1" - And accepting last share - And creating a share with - | path | /TMP/SUB | - | shareType | 0 | - | shareWith | user2 | - | permissions | 21 | - When Updating last share with - | permissions | 31 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Only allow 1 link share per file/folder - Given user "user0" exists - And As an "user0" - And creating a share with - | path | welcome.txt | - | shareType | 3 | - When save last share id - And creating a share with - | path | welcome.txt | - | shareType | 3 | - Then share ids should match - - Scenario: Correct webdav share-permissions for owned file - Given user "user0" exists - And User "user0" uploads file with content "foo" to "/tmp.txt" - When as "user0" gets properties of folder "/tmp.txt" with - |{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 --- a/build/integration/sharing_features/sharing-v1-part3.feature +++ /dev/null @@ -1,612 +0,0 @@ -# SPDX-FileCopyrightText: 20198 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-part2.feature - - Scenario: Correct webdav share-permissions for received file with edit and reshare permissions - Given user "user0" exists - And user "user1" exists - And User "user0" uploads file with content "foo" to "/tmp.txt" - And file "/tmp.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When as "user1" gets properties of folder "/tmp.txt" with - |{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: Correct webdav share-permissions for received file with edit permissions but no reshare permissions - Given user "user0" exists - And user "user1" exists - And User "user0" uploads file with content "foo" to "/tmp.txt" - And file "tmp.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 3 | - When as "user1" gets properties of folder "/tmp.txt" with - |{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 "3" - - Scenario: Correct webdav share-permissions for received file with reshare permissions but no edit permissions - Given user "user0" exists - And user "user1" exists - And User "user0" uploads file with content "foo" to "/tmp.txt" - And file "tmp.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 17 | - When as "user1" gets properties of folder "/tmp.txt" with - |{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 "17" - - Scenario: Correct webdav share-permissions for owned folder - Given user "user0" exists - And user "user0" created a folder "/tmp" - When as "user0" gets properties of folder "/tmp" with - |{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 "31" - - Scenario: Correct webdav share-permissions for received folder with all permissions - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/tmp" - And file "/tmp" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When as "user1" gets properties of folder "/tmp" with - |{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 "31" - - Scenario: Correct webdav share-permissions for received folder with all permissions but edit - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/tmp" - And file "/tmp" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 29 | - When as "user1" gets properties of folder "/tmp" with - |{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 "29" - - Scenario: Correct webdav share-permissions for received folder with all permissions but create - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/tmp" - And file "/tmp" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 27 | - When as "user1" gets properties of folder "/tmp" with - |{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 "27" - - Scenario: Correct webdav share-permissions for received folder with all permissions but delete - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/tmp" - And file "/tmp" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 23 | - When as "user1" gets properties of folder "/tmp" with - |{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 "23" - - Scenario: Correct webdav share-permissions for received folder with all permissions but share - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/tmp" - And file "/tmp" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And As an "user0" - And Updating last share with - | permissions | 15 | - When as "user1" gets properties of folder "/tmp" with - |{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 "15" - - Scenario: unique target names for incoming shares - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user0" created a folder "/foo" - And user "user1" created a folder "/foo" - When file "/foo" of user "user0" is shared with user "user2" - And user "user2" accepts last share - And file "/foo" of user "user1" is shared with user "user2" - And user "user2" accepts last share - Then user "user2" should see following elements - | /foo/ | - | /foo%20(2)/ | - - Scenario: Creating a new share with a disabled user - Given As an "admin" - And user "user0" exists - And user "user1" exists - And assure user "user0" is disabled - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareWith | user1 | - | shareType | 0 | - Then the OCS status code should be "997" - And the HTTP status code should be "401" - - Scenario: Deleting a group share as its owner - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user0" belongs to group "group1" - And user "user1" belongs to group "group1" - And As an "user0" - And creating a share with - | path | welcome.txt | - | shareType | 1 | - | shareWith | group1 | - When As an "user0" - And Deleting last share - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And Getting info of last share - And the OCS status code should be "404" - And the HTTP status code should be "200" - And As an "user1" - And Getting info of last share - And the OCS status code should be "404" - And the HTTP status code should be "200" - - Scenario: Deleting a group share as user - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And As an "user0" - And creating a share with - | path | welcome.txt | - | shareType | 1 | - | shareWith | group1 | - When As an "user1" - And Deleting last share - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Merging shares for recipient when shared from outside with group and member - Given using old dav path - And As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And user "user0" created a folder "/merge-test-outside" - When folder "/merge-test-outside" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And folder "/merge-test-outside" of user "user0" is shared with user "user1" - And user "user1" accepts last share - Then as "user1" the folder "/merge-test-outside" exists - And as "user1" the folder "/merge-test-outside (2)" does not exist - - Scenario: Merging shares for recipient when shared from outside with group and member with different permissions - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And user "user0" created a folder "/merge-test-outside-perms" - When folder "/merge-test-outside-perms" of user "user0" is shared with group "group1" with permissions 1 - And user "user1" accepts last share - And folder "/merge-test-outside-perms" of user "user0" is shared with user "user1" with permissions 31 - And user "user1" accepts last share - Then as "user1" gets properties of folder "/merge-test-outside-perms" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "SRGDNVCK" - And as "user1" the folder "/merge-test-outside-perms (2)" does not exist - - Scenario: Merging shares for recipient when shared from outside with two groups - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And group "group2" exists - And user "user1" belongs to group "group1" - And user "user1" belongs to group "group2" - And user "user0" created a folder "/merge-test-outside-twogroups" - When folder "/merge-test-outside-twogroups" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And folder "/merge-test-outside-twogroups" of user "user0" is shared with group "group2" - And user "user1" accepts last share - Then as "user1" the folder "/merge-test-outside-twogroups" exists - And as "user1" the folder "/merge-test-outside-twogroups (2)" does not exist - - Scenario: Merging shares for recipient when shared from outside with two groups with different permissions - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And group "group2" exists - And user "user1" belongs to group "group1" - And user "user1" belongs to group "group2" - And user "user0" created a folder "/merge-test-outside-twogroups-perms" - When folder "/merge-test-outside-twogroups-perms" of user "user0" is shared with group "group1" with permissions 1 - And user "user1" accepts last share - And folder "/merge-test-outside-twogroups-perms" of user "user0" is shared with group "group2" with permissions 31 - And user "user1" accepts last share - Then as "user1" gets properties of folder "/merge-test-outside-twogroups-perms" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "SRGDNVCK" - And as "user1" the folder "/merge-test-outside-twogroups-perms (2)" does not exist - - Scenario: Merging shares for recipient when shared from outside with two groups and member - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And group "group2" exists - And user "user1" belongs to group "group1" - And user "user1" belongs to group "group2" - And user "user0" created a folder "/merge-test-outside-twogroups-member-perms" - When folder "/merge-test-outside-twogroups-member-perms" of user "user0" is shared with group "group1" with permissions 1 - And user "user1" accepts last share - And folder "/merge-test-outside-twogroups-member-perms" of user "user0" is shared with group "group2" with permissions 31 - And user "user1" accepts last share - And folder "/merge-test-outside-twogroups-member-perms" of user "user0" is shared with user "user1" with permissions 1 - And user "user1" accepts last share - Then as "user1" gets properties of folder "/merge-test-outside-twogroups-member-perms" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "SRGDNVCK" - And as "user1" the folder "/merge-test-outside-twogroups-member-perms (2)" does not exist - - Scenario: Merging shares for recipient when shared from inside with group - Given As an "admin" - And user "user0" exists - And group "group1" exists - And user "user0" belongs to group "group1" - And user "user0" created a folder "/merge-test-inside-group" - When folder "/merge-test-inside-group" of user "user0" is shared with group "group1" - Then as "user0" the folder "/merge-test-inside-group" exists - And as "user0" the folder "/merge-test-inside-group (2)" does not exist - - Scenario: Merging shares for recipient when shared from inside with two groups - Given As an "admin" - And user "user0" exists - And group "group1" exists - And group "group2" exists - And user "user0" belongs to group "group1" - And user "user0" belongs to group "group2" - And user "user0" created a folder "/merge-test-inside-twogroups" - When folder "/merge-test-inside-twogroups" of user "user0" is shared with group "group1" - And folder "/merge-test-inside-twogroups" of user "user0" is shared with group "group2" - Then as "user0" the folder "/merge-test-inside-twogroups" exists - And as "user0" the folder "/merge-test-inside-twogroups (2)" does not exist - And as "user0" the folder "/merge-test-inside-twogroups (3)" does not exist - - Scenario: Merging shares for recipient when shared from inside with group with less permissions - Given As an "admin" - And user "user0" exists - And group "group1" exists - And group "group2" exists - And user "user0" belongs to group "group1" - And user "user0" belongs to group "group2" - And user "user0" created a folder "/merge-test-inside-twogroups-perms" - When folder "/merge-test-inside-twogroups-perms" of user "user0" is shared with group "group1" - And folder "/merge-test-inside-twogroups-perms" of user "user0" is shared with group "group2" - Then as "user0" gets properties of folder "/merge-test-inside-twogroups-perms" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "RGDNVCK" - And as "user0" the folder "/merge-test-inside-twogroups-perms (2)" does not exist - And as "user0" the folder "/merge-test-inside-twogroups-perms (3)" does not exist - - Scenario: Merging shares for recipient when shared from outside with group then user and recipient renames in between - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And user "user0" created a folder "/merge-test-outside-groups-renamebeforesecondshare" - When folder "/merge-test-outside-groups-renamebeforesecondshare" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And User "user1" moved folder "/merge-test-outside-groups-renamebeforesecondshare" to "/merge-test-outside-groups-renamebeforesecondshare-renamed" - And Sleep for "1" seconds - And folder "/merge-test-outside-groups-renamebeforesecondshare" of user "user0" is shared with user "user1" - And user "user1" accepts last share - Then as "user1" gets properties of folder "/merge-test-outside-groups-renamebeforesecondshare-renamed" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "SRGDNVCK" - And as "user1" the folder "/merge-test-outside-groups-renamebeforesecondshare" does not exist - - Scenario: Merging shares for recipient when shared from outside with user then group and recipient renames in between - Given using old dav path - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And user "user0" created a folder "/merge-test-outside-groups-renamebeforesecondshare" - When folder "/merge-test-outside-groups-renamebeforesecondshare" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And User "user1" moved folder "/merge-test-outside-groups-renamebeforesecondshare" to "/merge-test-outside-groups-renamebeforesecondshare-renamed" - And Sleep for "1" seconds - And folder "/merge-test-outside-groups-renamebeforesecondshare" of user "user0" is shared with group "group1" - And user "user1" accepts last share - Then as "user1" gets properties of folder "/merge-test-outside-groups-renamebeforesecondshare-renamed" with - |{http://owncloud.org/ns}permissions| - And the single response should contain a property "{http://owncloud.org/ns}permissions" with value "SRGDNVCK" - And as "user1" the folder "/merge-test-outside-groups-renamebeforesecondshare" does not exist - - Scenario: Empting trashbin - Given As an "admin" - And user "user0" exists - And User "user0" deletes file "/textfile0.txt" - When User "user0" empties trashbin - Then the HTTP status code should be "204" - - Scenario: orphaned shares - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/common" - And user "user0" created a folder "/common/sub" - And file "/common/sub" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And User "user0" deletes folder "/common" - When User "user0" empties trashbin - Then as "user1" the folder "/sub" does not exist - - Scenario: sharing again an own file while belonging to a group - Given As an "admin" - Given user "user0" exists - And group "sharing-group" exists - And user "user0" belongs to group "sharing-group" - And file "welcome.txt" of user "user0" is shared with group "sharing-group" - And Deleting last share - When creating a share with - | path | welcome.txt | - | shareWith | sharing-group | - | shareType | 1 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: unshare from self - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "sharing-group" exists - And user "user0" belongs to group "sharing-group" - And user "user1" belongs to group "sharing-group" - And file "/PARENT/parent.txt" of user "user0" is shared with group "sharing-group" - And user "user1" accepts last share - And user "user0" stores etag of element "/PARENT" - And user "user1" stores etag of element "/" - And As an "user1" - When Deleting last share - Then etag of element "/" of user "user1" has changed - And etag of element "/PARENT" of user "user0" has not changed - - Scenario: do not allow to increase permissions on received share - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - When As an "user1" - And Updating last share with - | permissions | 19 | - Then the OCS status code should be "403" - 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" - And user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 31 | - And user "user1" accepts last share - And creating a share with - | path | TMP | - | shareType | 0 | - | shareWith | user2 | - | permissions | 17 | - When As an "user1" - And Updating last share with - | permissions | 19 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: do not allow to increase link share permissions on reshare - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/TMP" - And As an "user0" - And creating a share with - | path | TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - When As an "user1" - And accepting last share - And creating a share with - | path | TMP | - | shareType | 3 | - And Updating last share with - | publicUpload | true | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: do not allow to increase link share permissions on sub reshare - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/TMP" - And user "user0" created a folder "/TMP/SUB" - And As an "user0" - And creating a share with - | path | TMP | - | shareType | 0 | - | shareWith | user1 | - | permissions | 17 | - When As an "user1" - And accepting last share - And creating a share with - | path | TMP/SUB | - | shareType | 3 | - And Updating last share with - | publicUpload | true | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: deleting file out of a share as recipient creates a backup for the owner - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/shared" - And User "user0" moved file "/textfile0.txt" to "/shared/shared_file.txt" - And folder "/shared" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When User "user1" deletes file "/shared/shared_file.txt" - Then as "user1" the file "/shared/shared_file.txt" does not exist - And as "user0" the file "/shared/shared_file.txt" does not exist - And as "user0" the file "/shared_file.txt" exists in trash - And as "user1" the file "/shared_file.txt" exists in trash - - Scenario: deleting folder out of a share as recipient creates a backup for the owner - Given As an "admin" - And user "user0" exists - And user "user1" exists - And user "user0" created a folder "/shared" - And user "user0" created a folder "/shared/sub" - And User "user0" moved file "/textfile0.txt" to "/shared/sub/shared_file.txt" - And folder "/shared" of user "user0" is shared with user "user1" - And user "user1" accepts last share - When User "user1" deletes folder "/shared/sub" - Then as "user1" the folder "/shared/sub" does not exist - And as "user0" the folder "/shared/sub" does not exist - And as "user0" the folder "/sub" exists in trash - And as "user0" the file "/sub/shared_file.txt" exists in trash - And as "user1" the folder "/sub" exists in trash - And as "user1" the file "/sub/shared_file.txt" exists in trash - - Scenario: moving a file into a share as recipient - Given As an "admin" - And user "user0" exists - And user "user1" 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 - When User "user1" moved file "/textfile0.txt" to "/shared/shared_file.txt" - 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 - And user "user1" exists - And group "group1" exists - And user "user1" belongs to group "group1" - And As an "user0" - And user "user0" created a folder "/share" - And folder "/share" of user "user0" is shared with group "group1" - And user "user1" accepts last share - And user "user0" created a folder "/share/subfolder" - And As an "user1" - And save the last share data as "original" - And as "user1" creating a share with - | path | /share/subfolder | - | shareType | 3 | - | permissions | 31 | - And save the last share data as "link" - And As an "user0" - And restore the last share data from "original" - When Updating last share with - | permissions | 23 | - | expireDate | +3 days | - And restore the last share data from "link" - And Getting info of last share - And Share fields of last share match with - | id | A_NUMBER | - | item_source | A_NUMBER | - | share_type | 3 | - | permissions | 31 | - | file_target | /subfolder | - | expireDate | | --- a/build/integration/sharing_features/sharing-v1-part4.feature +++ /dev/null @@ -1,579 +0,0 @@ -# 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 | - -Scenario: Cannot copy files from share without share permission into other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" copies file "/share/test.txt" to "/re-share/copytest.txt" - Then the HTTP status code should be "403" - -Scenario: Cannot move files from share without share permission into other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" moves file "/share/test.txt" to "/re-share/movetest.txt" - Then the HTTP status code should be "403" - - Scenario: Can move files from share without share permission but with delete permissions into other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 15 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" moves file "/share/test.txt" to "/re-share/movetest.txt" - Then the HTTP status code should be "201" - -Scenario: Cannot move folder containing share without share permission into other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/contains-share" - When User "user1" moves file "/share" to "/contains-share/share" - Then the HTTP status code should be "201" - And user "user1" created a folder "/re-share" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" moves file "/contains-share" to "/re-share/movetest" - Then the HTTP status code should be "403" - -Scenario: Can copy file between shares if share permissions - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" copies file "/share/test.txt" to "/re-share/movetest.txt" - Then the HTTP status code should be "201" - -Scenario: Cannot copy files from share without share permission into subfolder of other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - And user "user1" created a folder "/re-share/subfolder" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" copies file "/share/test.txt" to "/re-share/subfolder/copytest.txt" - Then the HTTP status code should be "403" - -Scenario: Cannot move files from share without share permission into subfolder of other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - And user "user1" created a folder "/re-share/subfolder" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" moves file "/share/test.txt" to "/re-share/subfolder/movetest.txt" - Then the HTTP status code should be "403" - -Scenario: Cannot move folder containing share without share permission into subfolder of other share - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 7 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/contains-share" - When User "user1" moves file "/share" to "/contains-share/share" - Then the HTTP status code should be "201" - And user "user1" created a folder "/re-share" - And user "user1" created a folder "/re-share/subfolder" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" moves file "/contains-share" to "/re-share/subfolder/movetest" - Then the HTTP status code should be "403" - -Scenario: Can copy file between shares into subfolder if share permissions - Given user "user0" exists - Given user "user1" exists - Given user "user2" exists - And As an "user0" - And user "user0" created a folder "/share" - When creating a share with - | path | share | - | shareType | 0 | - | shareWith | user1 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - And User "user0" uploads file with content "test" to "/share/test.txt" - And As an "user1" - And user "user1" created a folder "/re-share" - And user "user1" created a folder "/re-share/subfolder" - When creating a share with - | path | re-share | - | shareType | 0 | - | shareWith | user2 | - | permissions | 31 | - Then the HTTP status code should be "200" - And the OCS status code should be "100" - When User "user1" copies file "/share/test.txt" to "/re-share/subfolder/copytest.txt" - Then the HTTP status code should be "201" - -Scenario: Group deletes removes mount without marking - 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" - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - And group "group0" does not exist - Then Share mounts for "user0" are empty - -Scenario: Group deletes removes mount with marking - Given As an "admin" - And parameter "update_cutoff_time" of app "files_sharing" is set to "0" - 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" - Then Share mounts for "user0" are empty - When Connecting to dav endpoint - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - And group "group0" does not exist - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When Connecting to dav endpoint - Then Share mounts for "user0" are empty - -Scenario: User share mount without marking - Given As an "admin" - And user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And As an "user0" - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When Deleting last share - Then Share mounts for "user0" are empty - -Scenario: User share mount with marking - Given As an "admin" - And parameter "update_cutoff_time" of app "files_sharing" is set to "0" - And user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And As an "user0" - Then Share mounts for "user0" are empty - When Connecting to dav endpoint - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When Deleting last share - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When Connecting to dav endpoint - Then Share mounts for "user0" are empty - -Scenario: User added/removed to group share without marking - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "group0" exists - And file "textfile0.txt" of user "user1" is shared with group "group0" - And As an "user0" - Then Share mounts for "user0" are empty - When user "user0" belongs to group "group0" - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When As an "admin" - Then sending "DELETE" to "/cloud/users/user0/groups" with - | groupid | group0 | - Then As an "user0" - And Share mounts for "user0" are empty - -Scenario: User added/removed to group share with marking - Given As an "admin" - And parameter "update_cutoff_time" of app "files_sharing" is set to "0" - And user "user0" exists - And user "user1" exists - And group "group0" exists - And file "textfile0.txt" of user "user1" is shared with group "group0" - And As an "user0" - When user "user0" belongs to group "group0" - Then Share mounts for "user0" are empty - When Connecting to dav endpoint - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When As an "admin" - Then sending "DELETE" to "/cloud/users/user0/groups" with - | groupid | group0 | - Then As an "user0" - And Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When Connecting to dav endpoint - Then Share mounts for "user0" are empty - - Scenario: Share moved without marking - Given As an "admin" - And user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user1" is shared with user "user0" - And As an "user0" - Then Share mounts for "user0" match - | /user0/files/textfile0 (2).txt/ | - When User "user0" moves file "/textfile0 (2).txt" to "/target.txt" - Then Share mounts for "user0" match - | /user0/files/target.txt/ | - - # The initiator reaches the folder through a read-only path as well, which is found first - # by id. The share permission of the other path still allows creating the share. - Scenario: Creating a link share of a folder that is also reachable without share permission - Given user "user0" exists - And user "user1" exists - And user "user0" created a folder "/parent" - And user "user0" created a folder "/parent/z-child" - And folder "/parent" of user "user0" is shared with user "user1" with permissions 31 - And user "user1" accepts last share - And folder "/parent/z-child" of user "user0" is shared with user "user1" with permissions 1 - And user "user1" accepts last share - When as "user1" creating a share with - | path | parent/z-child | - | shareType | 3 | - | permissions | 1 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - # Renaming the parent folder moves the share mount through Updater::renameChildren, - # which passes a mount point instead of a path. Mount points carry a trailing slash, - # which used to end up in the share target. - Scenario: Renaming a folder that contains a received share keeps the share target normalized - Given user "user0" exists - And user "user1" exists - And User "user0" uploads file with content "content" to "/moved-share.txt" - And file "/moved-share.txt" of user "user0" is shared with user "user1" with permissions 19 - And user "user1" accepts last share - And user "user1" created a folder "/received" - And User "user1" moves file "/moved-share.txt" to "/received/moved-share.txt" - Then the HTTP status code should be "201" - When User "user1" moves file "/received" to "/archive" - Then the HTTP status code should be "201" - 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 - | file_target | /archive/moved-share.txt | - And user "user1" should see following elements - | /archive/moved-share.txt | --- a/build/integration/sharing_features/sharing-v1.feature +++ /dev/null @@ -1,695 +0,0 @@ -# SPDX-FileCopyrightText: 2019 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 - - Scenario: Creating a new share with user - Given user "user0" exists - And user "user1" exists - 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 The following headers should be set - | Content-Security-Policy | default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' | - - Scenario: Creating a share with a group - Given user "user0" exists - And user "user1" exists - And group "sharing-group" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareWith | sharing-group | - | shareType | 1 | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - - Scenario: Creating a new share with user who already received a share through their group - Given As an "admin" - And user "user0" exists - And user "user1" exists - And group "sharing-group" exists - And user "user1" belongs to group "sharing-group" - And file "welcome.txt" of user "user0" is shared with group "sharing-group" - And user "user1" accepts last share - And As an "user0" - Then 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" - - Scenario: Creating a new room share when Talk is not enabled - Given As an "admin" - And app "spreed" is not enabled - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareWith | a-room-token | - | shareType | 10 | - Then the OCS status code should be "403" - And the HTTP status code should be "200" - - Scenario: Creating a new mail share - Given dummy mail server is listening - And user "user0" exists - 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 - - Scenario: Creating a new mail share with password - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dumy@test.com | - | password | publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - 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 "shareapi_enforce_links_password" of app "core" is set to "yes" - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dumy@test.com | - | password | publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - Scenario: Creating a new mail share and setting a password - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dumy@test.com | - And Updating last share with - | password | publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - Scenario: Creating a new mail share and setting a password twice - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dumy@test.com | - And Updating last share with - | password | publicpw | - And Updating last share with - | password | another publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another publicpw" can be downloaded - - Scenario: Creating a new mail share and setting the same password twice - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dumy@test.com | - And Updating last share with - | password | publicpw | - And Updating last share with - | password | publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - Scenario: Creating a new public share - 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 - - Scenario: Creating a new public share with password - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | publicpw | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - Scenario: Creating a new public share of a folder - Given user "user0" exists - And As an "user0" - When creating a share with - | path | FOLDER | - | shareType | 3 | - | password | publicpw | - | expireDate | +3 days | - | publicUpload | true | - | permissions | 7 | - 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 | - | permissions | 31 | - | expiration | +3 days | - | url | AN_URL | - | token | A_TOKEN | - | mimetype | httpd/unix-directory | - - Scenario: Creating a new public share with password and adding an expiration date - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | publicpw | - And Updating last share with - | expireDate | +3 days | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "publicpw" can be downloaded - - Scenario: Creating a new public share, updating its expiration date and getting its info - Given user "user0" exists - And As an "user0" - When creating a share with - | path | FOLDER | - | shareType | 3 | - And Updating last share with - | expireDate | +3 days | - 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 - | id | A_NUMBER | - | item_type | folder | - | item_source | A_NUMBER | - | share_type | 3 | - | file_source | A_NUMBER | - | file_target | /FOLDER | - | permissions | 17 | - | stime | A_NUMBER | - | expiration | +3 days | - | token | A_TOKEN | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | 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" - When creating a share with - | path | FOLDER | - | shareType | 3 | - And Updating last share with - | password | publicpw | - 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 - | id | A_NUMBER | - | item_type | folder | - | item_source | A_NUMBER | - | share_type | 3 | - | file_source | A_NUMBER | - | file_target | /FOLDER | - | permissions | 17 | - | stime | A_NUMBER | - | token | A_TOKEN | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | url | AN_URL | - | mimetype | httpd/unix-directory | - - Scenario: Creating a new public share, updating its permissions and getting its info - Given user "user0" exists - And As an "user0" - When creating a share with - | path | FOLDER | - | 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 - 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 | folder | - | item_source | A_NUMBER | - | share_type | 3 | - | file_source | A_NUMBER | - | file_target | /FOLDER | - | permissions | 31 | - | stime | A_NUMBER | - | token | A_TOKEN | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | url | AN_URL | - | mimetype | httpd/unix-directory | - - Scenario: Creating a new public share, updating its permissions for "hide file list" - Given user "user0" exists - And As an "user0" - When creating a share with - | path | FOLDER | - | shareType | 3 | - And Updating last share with - | permissions | 4 | - 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 - | id | A_NUMBER | - | item_type | folder | - | item_source | A_NUMBER | - | share_type | 3 | - | file_source | A_NUMBER | - | file_target | /FOLDER | - | permissions | 4 | - | stime | A_NUMBER | - | token | A_TOKEN | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | url | AN_URL | - | mimetype | httpd/unix-directory | - - Scenario: Creating a new public share, updating publicUpload option and getting its info - Given user "user0" exists - And As an "user0" - When creating a share with - | path | FOLDER | - | shareType | 3 | - And Updating last share with - | publicUpload | true | - 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 - | id | A_NUMBER | - | item_type | folder | - | item_source | A_NUMBER | - | share_type | 3 | - | file_source | A_NUMBER | - | file_target | /FOLDER | - | permissions | 31 | - | stime | A_NUMBER | - | token | A_TOKEN | - | storage | A_NUMBER | - | mail_send | 0 | - | uid_owner | user0 | - | storage_id | home::user0 | - | file_parent | A_NUMBER | - | displayname_owner | user0 | - | url | AN_URL | - | mimetype | httpd/unix-directory | - - Scenario: Creating a new share of a file with default 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 - | permissions | 3 | - - Scenario: Creating a new share of a folder with default 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 | FOLDER | - | 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 - | permissions | 7 | - - Scenario: Creating a new internal share with default expiration date - Given user "user0" exists - And user "user1" exists - And As an "user0" - 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" - 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 - | expiration | +3 days | - - Scenario: Creating a new internal share with relaxed default expiration date - Given user "user0" exists - And user "user1" exists - And As an "user0" - 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 parameter "internal_defaultExpDays" of app "core" is set to "1" - 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 - | expiration | +1 days | - - Scenario: Creating a new internal share with relaxed default expiration date too large - Given user "user0" exists - And user "user1" exists - And As an "user0" - 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 parameter "internal_defaultExpDays" of app "core" is set to "10" - 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 - | expiration | +3 days | - - Scenario: Creating a new link share with default expiration date - Given user "user0" exists - And As an "user0" - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" - And parameter "shareapi_expire_after_n_days" of app "core" is set to "3" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - 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 - | expiration | +3 days | - - Scenario: Creating a new link share with relaxed default expiration date - Given user "user0" exists - And As an "user0" - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" - And parameter "shareapi_expire_after_n_days" of app "core" is set to "3" - And parameter "link_defaultExpDays" of app "core" is set to "1" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - 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 - | expiration | +1 days | - - Scenario: Creating a new link share with relaxed default expiration date too large - Given user "user0" exists - And As an "user0" - And parameter "shareapi_default_expire_date" of app "core" is set to "yes" - And parameter "shareapi_expire_after_n_days" of app "core" is set to "3" - And parameter "link_defaultExpDays" of app "core" is set to "10" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - 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 - | expiration | +3 days | - -Scenario: Writing to a read-only link share of an external storage - Given user "user0" exists - Then As an "user0" - When creating a share with - | path | local_storage | - | shareType | 3 | - And the OCS status code should be "100" - And the HTTP status code should be "200" - Then Uploading public file "foo.txt" with content "bar" - And the HTTP status code should be "403" - -Scenario: Writing to a read-write link share of an external storage - Given user "user0" exists - Then As an "user0" - When creating a share with - | path | local_storage | - | shareType | 3 | - | permissions | 7 | - Then Uploading public file "foo.txt" with content "bar" - And the HTTP status code should be "201" - -Scenario: getting all shares of a user using that user - Given user "user0" exists - And user "user1" exists - When User "user1" deletes file "/textfile0.txt" - And the HTTP status code should be "204" - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "user0" - When sending "GET" to "/apps/files_sharing/api/v1/shares" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And File "textfile0.txt" should be included in the response - - Scenario: getting all shares of a user using another user - Given user "user0" exists - And user "user1" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And As an "admin" - When sending "GET" to "/apps/files_sharing/api/v1/shares" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And File "textfile0.txt" should not be included in the response - - Scenario: getting all shares of a file - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And file "textfile0.txt" of user "user0" is shared with user "user2" - And As an "user0" - When sending "GET" to "/apps/files_sharing/api/v1/shares?path=textfile0.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user1" should be included in the response - And User "user2" should be included in the response - And User "user3" should not be included in the response - - Scenario: getting all shares of a file with a user with resharing rights but not yourself - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - And file "textfile0.txt" of user "user0" is shared with user "user1" - And user "user1" accepts last share - And file "textfile0.txt" of user "user0" is shared with user "user2" - And As an "user1" - When sending "GET" to "/apps/files_sharing/api/v1/shares?path=textfile0 (2).txt&reshares=true" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user1" should not be included in the response - And User "user2" should be included in the response - And User "user3" should not be included in the response - - Scenario: getting inherited shares of a file - Given user "user0" exists - And user "user1" exists - And user "user2" exists - And user "user3" exists - # will be shared with user1 - And User "user0" created a folder "/first" - # will be shared with user1, user2 - And User "user0" created a folder "/first/second" - # will be shared with user1, user3 - And User "user0" uploads file "data/textfile.txt" to "/first/test1.txt" - # will be shared with user1, user2, user3 - And User "user0" uploads file "data/textfile.txt" to "/first/second/test2.txt" - And As an "user0" - And creating a share with - | path | /first | - | shareType | 0 | - | shareWith | user1 | - | permissions | 16 | - And As an "user1" - And accepting last share - And folder "first/second" of user "user0" is shared with user "user2" - And file "first/test1.txt" of user "user0" is shared with user "user3" - And file "first/second/test2.txt" of user "user0" is shared with user "user3" - # get inherited shares from the owner PoV - And As an "user0" - When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/second/test2.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user0" should not be included in the response - And User "user1" should be included in the response - And User "user2" should be included in the response - And User "user3" should not be included in the response - When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user0" should not be included in the response - And User "user1" should be included in the response - And User "user2" should not be included in the response - And User "user3" should not be included in the response - # get inherited shares from the a user with no shares rights - And As an "user2" - When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt" - Then the OCS status code should be "404" - And the HTTP status code should be "200" - # get inherited shares from the PoV of a user with resharing rights (user1) - And As an "user1" - When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/second/test2.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user0" should not be included in the response - And User "user1" should not be included in the response - And User "user2" should be included in the response - And User "user3" should not be included in the response - When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt" - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And User "user0" should not be included in the response - And User "user1" should not be included in the response - And User "user2" should not be included in the response - And User "user3" should not be included in the response - -# See sharing-v1-part2.feature --- a/build/integration/theming_features/theming.feature +++ /dev/null @@ -1,131 +0,0 @@ -# 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" --- a/build/integration/videoverification_features/sharing-v1-video-verification.feature +++ /dev/null @@ -1,507 +0,0 @@ -@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 --force spreed" - Given the command was successful - - Scenario: Creating a link share with send password by Talk - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with different password in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Enabling send password by Talk with different password set after creation in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Enabling send password by Talk with same password in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with same password set after creation in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk without updating password in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk without updating password set after creation in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with no password in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share can be downloaded - - Scenario: Enabling send password by Talk with no password removed after creation in a link share - Given user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - And Updating last share with - | password | | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share can be downloaded - - Scenario: Disabling send password by Talk without setting new password in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk without setting new password set after creation in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting same password in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting same password set after creation in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting new password in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Disabling send password by Talk setting new password set after creation in a link share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 3 | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - - - - - Scenario: Creating a mail share with send password by Talk - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with different password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Enabling send password by Talk with different password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Enabling send password by Talk with same password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with same password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk without updating password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk without updating password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Enabling send password by Talk with no password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share can be downloaded - - Scenario: Enabling send password by Talk with no password removed after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - And Updating last share with - | password | | - And Updating last share with - | sendPasswordByTalk | true | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share can be downloaded - - Scenario: Disabling send password by Talk without setting new password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | sendPasswordByTalk | false | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk without setting new password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | sendPasswordByTalk | false | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting same password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting same password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "400" - And the HTTP status code should be "200" - And last share with password "secret" can be downloaded - - Scenario: Disabling send password by Talk setting new password in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded - - Scenario: Disabling send password by Talk setting new password set after creation in a mail share - Given dummy mail server is listening - And user "user0" exists - And As an "user0" - When creating a share with - | path | welcome.txt | - | shareType | 4 | - | shareWith | dummy@test.com | - And Updating last share with - | password | secret | - | sendPasswordByTalk | true | - And Updating last share with - | password | another secret | - | sendPasswordByTalk | false | - Then the OCS status code should be "100" - And the HTTP status code should be "200" - And last share with password "another secret" can be downloaded --- a/build/merge-font-noto-fix-merging-v20201206-phase3-76b29f8f8f9b.patch +++ /dev/null @@ -1,230 +0,0 @@ -diff --git a/nototools/merge_noto.py b/nototools/merge_noto.py -index 17c07ed..029845a 100755 ---- a/nototools/merge_noto.py -+++ b/nototools/merge_noto.py -@@ -34,7 +34,7 @@ def make_puncless_font_name(script): - return make_font_name(script).replace(" ", "").replace("-", "") - - --def make_font_file_name(script, weight, directory="individual/unhinted"): -+def make_font_file_name(script, weight, directory="individual/hinted"): - filename = "%s/%s-%s.ttf" % (directory, make_puncless_font_name(script), weight) - return filename - -@@ -85,6 +85,11 @@ SCRIPT_TO_OPENTYPE_SCRIPT_TAG = { - "Cuneiform": "xsux", - "Cypriot": "cprt", - "Yi": "yi ", -+ "AnatolianHieroglyphs":"hluw", -+ "Bamum": "bamu", -+ "NewTaiLue": "talu", -+ "Tagbanwa": "tagb", -+ "Thaana": "thaa", - } - - -@@ -135,96 +140,129 @@ def add_gsub_to_font(fontfile): - - def main(): - merge_table = { -- "Historic": [ -+ # Use a single file with all the fonts copied from merge_fonts.py. -+ "": [ # LGC, -+ "Adlam", -+ "AdlamUnjoined", -+ "AnatolianHieroglyphs", -+ "Arabic", -+ "ArabicUI", -+ "Armenian", - "Avestan", -- "Carian", -- "Egyptian Hieroglyphs", -- "Imperial Aramaic", -- "Pahlavi", # Should be 'Inscriptional Pahlavi', -- "Parthian", # Should be 'Inscriptional Parthian', -- "Linear B", -- "Lycian", -- "Lydian", -- "Mandaic", -- "Old Persian", -- "Old South Arabian", -- "Old Turkic", -- "Osmanya", -- "Phags-Pa", -- "Phoenician", -- "Samaritan", -- "Sumero-Akkadian Cuneiform", -- "Ugaritic", -- ], -- "South Asian": [ -- "Devanagari", -+ "Balinese", -+ "Bamum", -+ "Batak", - "Bengali", -- "Gurmukhi", -- "Gujarati", -- "Oriya", -- "Tamil", -- "Telugu", -- "Kannada", -- "Malayalam", -- "Sinhala", -- "Thaana", -+ "BengaliUI", - "Brahmi", -- "Kaithi", -- "Kharoshthi", # Move to Historic? -- "Lepcha", -- "Limbu", -- "Meetei Mayek", -- "Ol Chiki", -- "Saurashtra", -- "Syloti Nagri", -- ], -- "Southeast Asian": [ -- "Thai", -- "Lao", -- "Khmer", -- "Batak", - "Buginese", - "Buhid", -+ "CJKjp-Regular.otf", -+ "CJKkr-Regular.otf", -+ "CJKsc-Regular.otf", -+ "CJKtc-Regular.otf", -+ "CanadianAboriginal", -+ "Carian", -+ "Chakma", - "Cham", -- "Hanunoo", -- "Javanese", -- "Kayah Li", -- "New Tai Lue", -- "Rejang", -- "Sundanese", -- "Tagalog", -- "Tagbanwa", -- "Tai Le", -- "Tai Tham", -- "Tai Viet", -- ], -- "": [ # LGC, -- "Armenian", -- "Bamum", -- "Canadian Aboriginal", - "Cherokee", - "Coptic", -- "Cypriot Syllabary", -+ "Cuneiform", -+ "Cypriot", - "Deseret", -+ "Devanagari", -+ "DevanagariUI", -+ "Display", -+ "EgyptianHieroglyphs", - "Ethiopic", - "Georgian", - "Glagolitic", - "Gothic", -+ "Gujarati", -+ "GujaratiUI", -+ "Gurmukhi", -+ "GurmukhiUI", -+ "Hanunoo", - "Hebrew", -+ "ImperialAramaic", -+ "InscriptionalPahlavi", -+ "InscriptionalParthian", -+ "Javanese", -+ "Kaithi", -+ "Kannada", -+ "KannadaUI", -+ "KayahLi", -+ "Kharoshthi", -+ "Khmer", -+ "KhmerUI", -+ "Lao", -+ "LaoUI", -+ "Lepcha", -+ "Limbu", -+ "LinearB", - "Lisu", -+ "Lycian", -+ "Lydian", -+ "Malayalam", -+ "MalayalamUI", -+ "Mandaic", -+ "MeeteiMayek", -+ #"NotoSansMongolian", -+ "Mono", -+ "MonoCJKjp-Regular.otf", -+ "MonoCJKkr-Regular.otf", -+ "MonoCJKsc-Regular.otf", -+ "MonoCJKtc-Regular.otf", -+ "Myanmar", -+ "MyanmarUI", - "NKo", -+ "NewTaiLue", - "Ogham", -- "Old Italic", -+ "OlChiki", -+ "OldItalic", -+ "OldPersian", -+ "OldSouthArabian", -+ "OldTurkic", -+ "Oriya", -+ "OriyaUI", -+ "Osage", -+ "Osmanya", -+ "PhagsPa", -+ "Phoenician", -+ "Rejang", - "Runic", -+ "Samaritan", -+ "Saurashtra", - "Shavian", -+ "Sinhala", -+ "SinhalaUI", -+ "Sundanese", -+ "SylotiNagri", -+ "Symbols", -+ "Symbols2", -+ "SyriacEastern", -+ "SyriacEstrangela", -+ "SyriacWestern", -+ "Tagalog", -+ "Tagbanwa", -+ "TaiLe", -+ "TaiTham", -+ "TaiViet", -+ "Tamil", -+ "TamilUI", -+ "Telugu", -+ "TeluguUI", -+ "Thaana", -+ "Thai", -+ "ThaiUI", -+ "Tibetan", - "Tifinagh", -+ "Ugaritic", - "Vai", -+ "Yi", - ], - } - -- add_ui_alternative(merge_table, "South Asian") -- add_ui_alternative(merge_table, "Southeast Asian") -- - for merge_target in sorted(merge_table): - for weight in ["Regular", "Bold"]: - merger = merge.Merger() -@@ -261,7 +299,7 @@ def main(): - name_record.string = name.encode("UTF-16BE") - - font.save( -- make_font_file_name(merge_target, weight, directory="combined/unhinted") -+ make_font_file_name(merge_target, weight, directory="combined/hinted") - ) - - --- a/build/merge-font-noto-fix-merging-v20201206-phase3-76b29f8f8f9b.patch.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/build/merge-font-noto.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -# Helper script to merge several Noto fonts in a single TTF file. -# -# The "Noto Sans" font (https://www.google.com/get/noto) only includes a subset -# of all the available glyphs in the Noto fonts. This scripts uses -# "merge_noto.py" from the Noto Tools package to add other scripts, like Arabic, -# Devanagari or Hebrew. -# -# "merge_noto.py" originally merges the fonts by region. However it was adjusted -# to merge "all" the fonts in a single file, like done by "merge_fonts.py". The -# reason to use "merge_noto.py" instead of "merge_fonts.py" is that -# "merge_noto.py" merges regular and bold fonts, which are both needed in -# Nextcloud. "merge_fonts.py" only merges regular fonts, and adjusting it to -# handle bold fonts too would have been more work than adjusting -# "merge_noto.py". -# -# Please note that, due to technical limitations of the TTF format (a single -# file can not have more than 65535 glyphs) the merged file does not include any -# Chinese, Japanese or Korean glyph (the Noto CJK files already use all the -# slots). In fact, it seems that it can not include either all the glyphs from -# all the non CJK Noto fonts, so it merges only those predefined in the -# "merge_fonts.py" script (as it is a larger set than the original one in -# "merge_noto.py"). -# -# Also please note that merging the fonts is a slow process and it can take a -# while (from minutes to hours, depending on the system). -# -# To perform its job, the script requires the "docker" command to be available. -# -# The Docker Command Line Interface (the "docker" command) requires special -# permissions to talk to the Docker daemon, and those permissions are typically -# available only to the root user. Please see the Docker documentation to find -# out how to give access to a regular user to the Docker daemon: -# https://docs.docker.com/engine/installation/linux/linux-postinstall/ -# -# Note, however, that being able to communicate with the Docker daemon is the -# same as being able to get root privileges for the system. Therefore, you must -# give access to the Docker daemon (and thus run this script as) ONLY to trusted -# and secure users: -# https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface - -# Stops the container started by this script. -function cleanUp() { - # Disable (yes, "+" disables) exiting immediately on errors to ensure that - # all the cleanup commands are executed (well, no errors should occur during - # the cleanup anyway, but just in case). - set +o errexit - - echo "Cleaning up" - docker rm --volumes --force $DOCKER_CONTAINER_ID -} - -# Exit immediately on errors. -set -o errexit - -# Execute cleanUp when the script exits, either normally or due to an error. -trap cleanUp EXIT - -# Ensure working directory is script directory, as some actions (like copying -# the patches to the container) expect that. -cd "$(dirname $0)" - -# python:3.9 can not be used, as one of the requeriments of Noto Tools -# (pyclipper) fails to build. -# -# The container exits immediately if no command is given, so a Bash session -# is created to prevent that. -DOCKER_CONTAINER_ID=`docker run --rm --detach --interactive --tty python:3.8-slim bash` - -# Install required dependencies. -docker exec $DOCKER_CONTAINER_ID apt-get update -docker exec $DOCKER_CONTAINER_ID apt-get install -y git gcc g++ libjpeg-dev zlib1g-dev wget - -# Install Noto Tools in the container. -docker exec --workdir /tmp $DOCKER_CONTAINER_ID git clone https://github.com/googlefonts/nototools -docker exec --workdir /tmp/nototools $DOCKER_CONTAINER_ID git checkout 76b29f8f8f9b -docker exec --workdir /tmp/nototools $DOCKER_CONTAINER_ID pip install --requirement requirements.txt -docker exec --workdir /tmp/nototools $DOCKER_CONTAINER_ID pip install --editable . - -# As Noto Tools were installed as "editable" the scripts can be patched after -# installation. -docker cp merge-font-noto-fix-merging-v20201206-phase3-76b29f8f8f9b.patch $DOCKER_CONTAINER_ID:/tmp/nototools/merge-font-noto-fix-merging-v20201206-phase3-76b29f8f8f9b.patch -docker exec --workdir /tmp/nototools --interactive $DOCKER_CONTAINER_ID patch --strip 1 < merge-font-noto-fix-merging-v20201206-phase3-76b29f8f8f9b.patch - -# Get Noto fonts. -# -# Phase 2 Noto fonts use 2048 units per em, while phase 3 Noto fonts use 1000*. -# Currently the fonts in the released package** (apparently from 2017-10-25) are -# a mix of both, but fonts with different units per em can not be merged***. -# However, the fonts in the Git repository, although not released yet, are all -# using 1000 units per em already, so those are the ones merged. -# -# *https://github.com/googlefonts/noto-fonts/issues/908#issuecomment-298687906. -# **https://noto-website-2.storage.googleapis.com/pkgs/Noto-unhinted.zip -# ***https://fonttools.readthedocs.io/en/latest/merge.html -docker exec --workdir /tmp $DOCKER_CONTAINER_ID wget https://github.com/googlefonts/noto-fonts/archive/v20201206-phase3.tar.gz -docker exec --workdir /tmp $DOCKER_CONTAINER_ID tar -xzf v20201206-phase3.tar.gz - -# noto-fonts in Git and snapshots of Git (like the package used) have a -# subdirectory for each font, but "merge_noto.py" expects to find all the fonts -# in a single directory, so the structure needs to be "flattened". -# -# Hinted fonts* adapt better to being rendered in different sizes. The full -# package in https://www.google.com/get/noto/ includes only unhinted fonts -# (according to its name**, I have not actually verified the fonts themselves), -# while the individual fonts listed below in the page are a mix of hinted and -# unhinted fonts. However, the Git directory has hinted versions of all fonts, -# so those are the ones merged (maybe there is a good reason not to merge hinted -# fonts, but seems to work :-P). -# -# *https://en.wikipedia.org/wiki/Font_hinting -# **https://noto-website-2.storage.googleapis.com/pkgs/Noto-unhinted.zip -docker exec --workdir /tmp $DOCKER_CONTAINER_ID mkdir --parent individual/hinted -docker exec --workdir /tmp $DOCKER_CONTAINER_ID find noto-fonts-20201206-phase3/hinted/ttf -iname "NotoSans*Regular.ttf" -exec mv {} individual/hinted/ \; -docker exec --workdir /tmp $DOCKER_CONTAINER_ID find noto-fonts-20201206-phase3/hinted/ttf -iname "NotoSans*Bold.ttf" -exec mv {} individual/hinted/ \; - -# Merge the fonts. -docker exec --workdir /tmp $DOCKER_CONTAINER_ID mkdir --parent combined/hinted -docker exec --workdir /tmp $DOCKER_CONTAINER_ID merge_noto.py - -# Copy resulting files. -# -# Noto fonts, as well as the merged files, are licensed under the SIL Open Font -# License: https://scripts.sil.org/OFL -docker cp $DOCKER_CONTAINER_ID:/tmp/combined/hinted/NotoSans-Regular.ttf ../core/fonts/NotoSans-Regular.ttf -docker cp $DOCKER_CONTAINER_ID:/tmp/combined/hinted/NotoSans-Bold.ttf ../core/fonts/NotoSans-Bold.ttf --- a/build/npm-post-build.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -set -e - -# Build CSS files from SCSS -npm run sass -# Build icons -npm run sass:icons - -# Add licenses for source maps -if [ -d "dist" ]; then - for f in dist/*.js dist/*.mjs dist/*.css; do - # If license file and source map exists copy license for the source map - if [ -f "$f.license" ] && [ -f "$f.map" ]; then - # Remove existing link - [ -L "$f.map.license" ] && rm "$f.map.license" - # Create a new link - [ ! -e "$f.map.license" ] && ln -s "$(basename "$f.license")" "$f.map.license" - fi - done - echo "Copying licenses for sourcemaps done" -else - echo "This script needs to be executed from the root of the repository" - exit 1 -fi - --- a/build/openapi-checker.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -specs=() -for path in core apps/*; do - if [ ! -f "$path/.noopenapi" ] && [[ "$(git check-ignore "$path")" != "$path" ]]; then - composer exec generate-spec "$path" "$path/openapi.json" || exit 1 - if [[ "$(basename "$path")" != "core" ]]; then - if [ -f "$path/openapi-full.json" ]; then - specs+=("$path/openapi-full.json") - else - specs+=("$path/openapi.json") - fi; - fi; - fi -done - -composer exec merge-specs -- \ - --core core/openapi-full.json \ - --merged openapi.json \ - "${specs[@]}" - -files="$(git ls-files --exclude-standard --modified --others)" -changed=false -for file in $files; do - if [[ $file == *"openapi"*".json" ]]; then - changed=true - break - fi -done - -if [ $changed = true ] -then - git --no-pager diff - echo "The OpenAPI specifications are not up to date" - echo "Please run: bash build/openapi-checker.sh" - echo "And commit the result" - exit 1 -else - echo "OpenAPI specifications up to date. Carry on" - exit 0 -fi --- a/build/phpDocumentor.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -wget https://phpdoc.org/phpDocumentor.phar - -mkdir -p api/ - -php phpDocumentor.phar --target=./api --directory=./lib/public --title="Nextcloud PHP API ($BRANCH)" --- a/build/psalm-baseline-ocp.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - --- a/build/psalm-baseline-security.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - --- a/build/psalm-baseline.xml +++ /dev/null @@ -1,4068 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - &$uid] - )]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getPath())]]> - - - - - - - - - - getPath())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RRULE]]> - - - - - stream, strlen($this->boundary))]]> - - - - - - - - strtolower($matches[0]), - urlencode($raw), - )]]> - - - - - UID]]> - vobject->getBaseComponent()->{'LAST-MODIFIED'}]]> - vobject->getBaseComponent()->{'LAST-MODIFIED'}]]> - - - - - - - - - - - - - - - PRODID]]> - VERSION]]> - DTEND]]> - DTSTART]]> - - - UID]]> - VEVENT]]> - VEVENT]]> - DTEND]]> - DTSTART]]> - {'SUMMARY'}]]> - - - - - objectData['calendardata']]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CLASS]]> - DTEND]]> - DTEND]]> - DTSTART]]> - DUE]]> - DUE]]> - DURATION]]> - DURATION]]> - RDATE]]> - RDATE]]> - RRULE]]> - UID]]> - - - - - - - - - - - - - - - - getCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri)]]> - - - - - - - - - - - - - - - - - - baseEvent->DTSTART]]> - - - - - - - - - - PRODID]]> - VERSION]]> - - - UID]]> - PRODID]]> - VERSION]]> - VTIMEZONE]]> - VTIMEZONE]]> - VTIMEZONE]]> - - - - - source, $end - $start)]]> - - - - - source, $end - $start)]]> - - - - - - - - - - - - - - - - - - - - - - - - - DTEND]]> - - - DTSTART]]> - DTSTART]]> - STATUS]]> - - - - - - - - ]]> - - - DTSTART]]> - ORGANIZER]]> - - - - - - - - DTSTART]]> - UID]]> - - - - - - - - - - - - - - - - - - - - - - - - - - DTSTART]]> - VALARM]]> - VALARM]]> - ACTION]]> - ACTION]]> - ACTION]]> - DURATION]]> - DURATION]]> - REPEAT]]> - TRIGGER]]> - TRIGGER]]> - DTEND]]> - DTSTART]]> - DTSTART]]> - DURATION]]> - EXDATE]]> - RDATE]]> - RRULE]]> - STATUS]]> - {'RECURRENCE-ID'}]]> - {'RECURRENCE-ID'}]]> - {'RECURRENCE-ID'}]]> - UID]]> - UID]]> - VTIMEZONE]]> - - - - - - parent->UID]]> - - - - - - - - - - - - - - - - - - - - - - - - - recipientName]]> - - - - - - - - - DTSTART]]> - UID]]> - message->VEVENT]]> - message->VEVENT]]> - VEVENT]]> - {'UID'}]]> - - - - - - - - - - - - - DTEND]]> - - - - - - METHOD]]> - - - message->VEVENT]]> - DTSTART]]> - DTSTART]]> - DTSTART]]> - UID]]> - - - - - - - - - - - - - - - - $property, - 'parameter' => $parameter, - ]]]> - - - - - - - - - - - - - - - - - - VTIMEZONE]]> - - - - - ATTENDEE]]> - ORGANIZER]]> - SEQUENCE]]> - METHOD]]> - - - - - {'X-PUBLISHED-TTL'}]]> - - - - - UID]]> - - - - - getKey()]]> - getKey()]]> - - - - - - - - - UID]]> - - - - - - - - - - - - - - - - - - - - - - UID]]> - - - - - - - - UID]]> - VERSION]]> - - - - - PHOTO]]> - - - - - - - - - - PHOTO]]> - - - - - - - - - - UID]]> - - - - - - - - - - - - - - - - - - principalUri]]> - principalUri]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dirContent]]> - - - - - - node]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - node]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getPrincipalPropertiesByPath($path)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getShareType()]]> - - - - - - - - - - - - - - - - - - - {'VEVENT'}]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getTimestamp() !== false]]> - - - arguments[0]->name]]> - arguments[0]->name]]> - - - - - backend->getArbiterPath()]]> - - - - - - - - VEVENT]]> - ATTENDEE]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {'RECURRENCE-ID'}]]> - - - - - getCursor()]]> - - - FN]]> - PHOTO]]> - TEL]]> - UID]]> - - - - - getCursor()]]> - - - DTSTART]]> - DTSTART]]> - DTSTART]]> - DTSTART]]> - DTSTART]]> - - - - - - - - getCursor()]]> - - - COMPLETED]]> - DUE]]> - - - - - - - - - - - - - - - Note]]> - REV]]> - UID]]> - - - - - DTEND]]> - DTSTART]]> - UID]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - currentStream]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getPath())]]> - - - getPath())]]> - - - - - $addressBookNode->getName(), - 'displayName' => $addressBookInfo['{DAV:}displayname'], - 'description' => $addressBookInfo['{' . CardDAVPlugin::NS_CARDDAV . '}addressbook-description'], - 'vCards' => $vCards, - ]]]> - - - - - - PRODID]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - &$user1] - )]]> - &$user2] - )]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - &$shareWith] - )]]> - - - - - - - - - - - ]]> - - - - - fileEncrypted[$fileId]]]> - - - - fileEncrypted[$fileId]]]> - - - - - - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - fileIsEncrypted]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getUID())]]> - getUID())]]> - getUID())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getCode()]]> - - - - - - storageClass]]> - - - ]]> - - - - - - - - - - - - - - getCode()]]> - getCode()]]> - getCode()]]> - getCode()]]> - getCode()]]> - getCode()]]> - getCode()]]> - - - - - lastModified]]> - - - - - - - - - - - - - - - - - - - - - - - - - - $mountPoint, - Filesystem::signal_param_mount_type => $mountType, - Filesystem::signal_param_users => $applicable, - ] - )]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sourceRootInfo]]> - - - - - - - - - - sourceRootInfo]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - data->getId()]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Filesystem::normalizePath($file_path), - 'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))])]]> - $targetPath, 'trashPath' => $sourcePath])]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getUID() . '/files')]]> - getUID())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getUID() . '/files')]]> - getUID())]]> - - - - - - - - - - - - - getUID())]]> - getUID())]]> - - - - - - - getUID())]]> - getUID())]]> - - - - - - - - - - - - - - - - - - - - - getUID())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - session->get(self::WEBAUTHN_REGISTRATION))]]> - - - - - - - - - getEMailAddress() => $user->getDisplayName()]]]> - - - - - - - - - - - - - - - - container->get($taskTypeClass); - } catch (NotFoundExceptionInterface $e) { - continue; - } catch (ContainerExceptionInterface $e) { - continue; - }]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getUID())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getRgb())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - getCode()]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - config)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - timeFactory->getTime()]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - &$user] - )]]> - - - - getCode()]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getUserId())]]> - - - - - - userId, $identifier)]]> - - - - - - - - - - - userId, $identifier)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - session->get(self::WEBAUTHN_LOGIN))]]> - &$uid] - )]]> - - - - - request->server]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - filterClasses]]> - providerClasses]]> - settingsClasses]]> - - - settings]]> - - - - - - - - - - - enabledAppsCache]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - inputStream, 'rb')]]> - - - getOverwriteHost()]]> - - - - - - - - - - - - - - - - - - - - - request->server]]> - request->server]]> - - - - - request->server]]> - - - - - - - - - - - - - - - - - - - - - - - - - - getCode()]]> - - - appName]]> - - - - - tar->extractInString($path)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - providers]]> - - - providers]]> - - - - providers]]> - - - - - - providers]]> - - - - - - - - - - - - - - - - - - - - - - - - container->get($registration->getService()); - } catch (Throwable $e) { - $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [ - 'exception' => $e, - ]); - return []; - } - - return $provider->getCalendars($principalUri, $calendarUris); - }, $context->getCalendarProviders()) - )]]> - - - - - - container->get($registration->getService()); - } catch (Throwable $e) { - $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [ - 'exception' => $e, - ]); - return []; - } - - return $provider->getCalendars($principalUri, $calendarUris); - }, $context->getCalendarProviders())]]> - - - METHOD]]> - - - VFREEBUSY]]> - VFREEBUSY]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - delete($key)]]> - set($key, $value)]]> - - - - - - - - - request->server]]> - - - - - - - - - - - - - - - - - - - - - - - - - - getCode())]]> - - - - - - - - functionBuilder->lower($x)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - l($type, $timestamp, [ - 'width' => $format, - ])]]> - - - l($type, $timestamp, [ - 'width' => $format, - ])]]> - - - - - - - - - - - - - - - - - - - manager->getFileForToken($this->data['user_id'], $this->data['file_id'], $this->data['file_path'])]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - folder]]> - - - - - - - - querySearchHelper->searchInCaches($query, [$this]))]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - querySearchHelper->searchInCaches($query, [$this]))]]> - - - - - - - - - - - - - - - - - cacheInfoCache[$fileId]]]> - - - - - - - - - - - - - - - - - - - - - view->hash($type, $this->path, $raw)]]> - - - - - - - - createNode($file->getPath(), $file); - }, $files)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - __call(__FUNCTION__, func_get_args())]]> - - - - - - - - - - - - - - - - - - - parent]]> - - - - - - fileInfo]]> - - - - - - fileInfo]]> - fileInfo]]> - - - - - - - - - - - - - - - - - - - - - - - - - - copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getContent())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - syslogId, - 'MESSAGE=' . $this->logDetailsAsJSON($app, $message, $level))]]> - - - - - getPrefix() . $key, $value, $ttl)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - userToNotify]]> - - - - - - - - collectionName]]> - - - - - request->server]]> - - - - - value)]]> - - - - - getDefaultCertificatesBundlePath())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - dbPort)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - circleToTeam(...), $this->circlesManager->probeCircles())]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - server]]> - server]]> - - - manager instanceof PublicEmitter]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - headers)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- a/build/psalm-checker.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -if [ -d "dist" ]; then - missing='' - for app in apps/*; do - if git check-ignore "$app" -q ; then - echo "ℹ️ Ignoring non shipped app: $app" - continue - fi - - grep "directory name=\"$app\"" psalm.xml 2>&1 > /dev/null - if [ $? -ne 0 ]; then - missing="$missing - $app\n" - fi - done - - if [ "$missing" = "" ]; then - echo "✅ All apps will be linted by psalm" - else - echo "❌ Following apps are not setup for linting using psalm:" - echo -e "$missing" - exit 1 - fi -else - echo "⚠️ This script needs to be executed from the root of the repository" - exit 1 -fi - --- a/build/psalm/AppFrameworkTainter.php +++ /dev/null @@ -1,39 +0,0 @@ -getStatementsSource()->getFQCLN() === null) { - return null; - } - if (!$event->getCodebase()->classExtendsOrImplements($event->getStatementsSource()->getFQCLN(), \OCP\AppFramework\Controller::class)) { - return null; - } - if (!($event->getStmt() instanceof PhpParser\Node\Stmt\ClassMethod)) { - return null; - } - if (!$event->getStmt()->isPublic() || $event->getStmt()->isMagic()) { - return null; - } - foreach ($event->getStmt()->params as $i => $param) { - $expr_type = new Psalm\Type\Union([new Psalm\Type\Atomic\TString()]); - $expr_identifier = (strtolower($event->getStatementsSource()->getFQCLN()) . '::' . strtolower($event->getFunctionlikeStorage()->cased_name) . '#' . ($i + 1)); - $event->getCodebase()->addTaintSource( - $expr_type, - $expr_identifier, - TaintKindGroup::ALL_INPUT, - new CodeLocation($event->getStatementsSource(), $param) - ); - } - - return null; - } -} --- a/build/psalm/AttributeNamedParameters.php +++ /dev/null @@ -1,53 +0,0 @@ -getStmt(); - $statementsSource = $event->getStatementsSource(); - - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - self::checkAttribute($attr, $statementsSource); - } - } - - foreach ($stmt->getMethods() as $method) { - foreach ($method->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - self::checkAttribute($attr, $statementsSource); - } - } - } - } - - private static function checkAttribute(Attribute $stmt, FileSource $statementsSource): void { - if ($stmt->name->getLast() === 'Attribute') { - return; - } - - foreach ($stmt->args as $arg) { - if ($arg->name === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Attribute arguments must be named.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - } -} --- a/build/psalm/ITypedQueryBuilderTest.php +++ /dev/null @@ -1,69 +0,0 @@ -getTypedQueryBuilder(); - -$qb->selectColumns('a', 'b'); -$qb->selectColumns('c'); - -$qb->selectColumnsDistinct('d', 'e'); -$qb->selectColumnsDistinct('f'); - -$qb->selectAlias('g', 'h'); -$qb->selectAlias($qb->func()->lower('i'), 'j'); - -$qb - ->setParameter('k', 'l') - ->setParameters([]) - ->setFirstResult(0) - ->setMaxResults(0) - ->delete() - ->update() - ->insert() - ->from('m') - ->join('n', 'o', 'p') - ->innerJoin('q', 'r', 's') - ->leftJoin('t', 'u', 'v') - ->rightJoin('w', 'x', 'y') - ->set('z', '1') - ->where() - ->andWhere() - ->orWhere() - ->groupBy() - ->addGroupBy() - ->setValue('2', '3') - ->values([]) - ->having() - ->andHaving() - ->orHaving() - ->orderBy('4') - ->addOrderBy('5') - ->resetQueryParts() - ->resetQueryPart('6') - ->hintShardKey('7', '8') - ->runAcrossAllShards() - ->forUpdate(); - -/** @psalm-check-type-exact $result = \OCP\DB\IResult<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j'> */ -$result = $qb->executeQuery(); - -/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */ -$rows = $result->fetch(\PDO::FETCH_ASSOC); - -/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */ -$rows = $result->fetchAssociative(); - -/** @psalm-check-type-exact $rows = list> */ -$rows = $result->fetchAll(\PDO::FETCH_ASSOC); - -/** @psalm-check-type-exact $rows = list> */ -$rows = $result->fetchAllAssociative(); --- a/build/psalm/InArrayStrictChecker.php +++ /dev/null @@ -1,56 +0,0 @@ -getExpr(); - if (!$stmt instanceof FuncCall - || !$stmt->name instanceof Name - || strtolower($stmt->name->toString()) !== 'in_array') { - return null; - } - - $hasStrictArg = false; - foreach ($stmt->getArgs() as $index => $arg) { - if ($arg->name !== null) { - if ($arg->name->toString() === 'strict') { - $hasStrictArg = true; - break; - } - continue; - } - if ($index === 2) { - $hasStrictArg = true; - break; - } - } - - if (!$hasStrictArg) { - IssueBuffer::maybeAdd( - new \Psalm\Issue\UnrecognizedExpression( - 'in_array() must be called with an explicit $strict parameter', - new CodeLocation($event->getStatementsSource()->getSource(), $stmt), - ), - $event->getStatementsSource()->getSuppressedIssues(), - ); - } - - return null; - } -} --- a/build/psalm/LogicalOperatorChecker.php +++ /dev/null @@ -1,30 +0,0 @@ -getExpr(); - if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd - || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr) { - IssueBuffer::maybeAdd( - new \Psalm\Issue\UnrecognizedExpression( - 'Logical binary operators AND and OR are not allowed in the Nextcloud codebase', - new CodeLocation($event->getStatementsSource()->getSource(), $stmt), - ), - $event->getStatementsSource()->getSuppressedIssues(), - ); - } - return null; - } -} --- a/build/psalm/NcuExperimentalChecker.php +++ /dev/null @@ -1,131 +0,0 @@ -getStmt(); - $statementsSource = $event->getStatementsSource(); - - self::checkClassComment($classLike, $statementsSource); - - foreach ($classLike->stmts as $stmt) { - if ($stmt instanceof ClassConst && !$stmt->isPrivate()) { - self::checkStatementComment($stmt, $statementsSource, 'constant'); - } - - if ($stmt instanceof ClassMethod && !$stmt->isPrivate()) { - self::checkStatementComment($stmt, $statementsSource, 'method'); - } - - if ($stmt instanceof EnumCase) { - self::checkStatementComment($stmt, $statementsSource, 'enum'); - } - } - } - - private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void { - $docblock = $stmt->getDocComment(); - - if ($docblock === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'PHPDoc is required for classes/interfaces in NCU.', - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - try { - $parsedDocblock = DocComment::parsePreservingLength($docblock); - } catch (DocblockParseException $e) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - $e->getMessage(), - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - if (!isset($parsedDocblock->tags['experimental'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - '@experimental is required for classes/interfaces in NCU.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - - if (isset($parsedDocblock->tags['depreacted'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Typo in @deprecated for classes/interfaces in NCU.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - - private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void { - $docblock = $stmt->getDocComment(); - - if ($docblock === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'PHPDoc is required for ' . $type . 's in NCU.', - new CodeLocation($statementsSource, $stmt) - ), - ); - return; - } - - try { - $parsedDocblock = DocComment::parsePreservingLength($docblock); - } catch (DocblockParseException $e) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - $e->getMessage(), - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - if (!isset($parsedDocblock->tags['experimental'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - '@experimental is required for ' . $type . 's in NCU.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - - if (isset($parsedDocblock->tags['depreacted'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Typo in @deprecated for ' . $type . ' in NCU.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } -} --- a/build/psalm/OcpSinceChecker.php +++ /dev/null @@ -1,221 +0,0 @@ -getStmt(); - $statementsSource = $event->getStatementsSource(); - - if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) { - return; - } - - $isTesting = str_contains($statementsSource->getFilePath(), '/lib/public/Notification/') - || str_contains($statementsSource->getFilePath(), '/lib/public/Config/') - || str_contains($statementsSource->getFilePath(), '/lib/public/Migration/Attributes/') - || str_contains($statementsSource->getFilePath(), 'CalendarEventStatus'); - - if ($isTesting) { - self::checkStatementAttributes($classLike, $statementsSource); - } else { - self::checkClassComment($classLike, $statementsSource); - } - foreach ($classLike->stmts as $stmt) { - if ($stmt instanceof ClassConst) { - self::checkStatementComment($stmt, $statementsSource, 'constant'); - } - - if ($stmt instanceof ClassMethod && ($stmt->isPublic() || $stmt->isProtected())) { - self::checkStatementComment($stmt, $statementsSource, 'method'); - } - - if ($stmt instanceof EnumCase) { - if ($isTesting) { - self::checkStatementAttributes($classLike, $statementsSource); - } else { - self::checkStatementComment($stmt, $statementsSource, 'enum'); - } - } - } - } - - private static function checkStatementAttributes(ClassLike $stmt, FileSource $statementsSource): void { - $hasAppFrameworkAttribute = false; - $mustBeConsumable = false; - $isConsumable = false; - foreach ($stmt->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - if (in_array($attr->name->getLast(), [ - 'Catchable', - 'Consumable', - 'Dispatchable', - 'Implementable', - 'Listenable', - 'Throwable', - ], true)) { - $hasAppFrameworkAttribute = true; - self::checkAttributeHasValidSinceVersion($attr, $statementsSource); - } - if (in_array($attr->name->getLast(), [ - 'Catchable', - 'Consumable', - 'Listenable', - ], true)) { - $isConsumable = true; - } - if ($attr->name->getLast() === 'ExceptionalImplementable') { - $mustBeConsumable = true; - } - } - } - - if ($mustBeConsumable && !$isConsumable) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Attribute OCP\\AppFramework\\Attribute\\ExceptionalImplementable is only valid on classes that also have OCP\\AppFramework\\Attribute\\Consumable', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - - if (!$hasAppFrameworkAttribute) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'At least one of the OCP\\AppFramework\\Attribute attributes is required', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - - private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void { - $docblock = $stmt->getDocComment(); - - if ($docblock === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'PHPDoc is required for classes/interfaces in OCP.', - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - try { - $parsedDocblock = DocComment::parsePreservingLength($docblock); - } catch (DocblockParseException $e) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - $e->getMessage(), - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - if (!isset($parsedDocblock->tags['since'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - '@since is required for classes/interfaces in OCP.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - - if (isset($parsedDocblock->tags['depreacted'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Typo in @deprecated for classes/interfaces in OCP.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - - private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void { - $docblock = $stmt->getDocComment(); - - if ($docblock === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'PHPDoc is required for ' . $type . 's in OCP.', - new CodeLocation($statementsSource, $stmt) - ), - ); - return; - } - - try { - $parsedDocblock = DocComment::parsePreservingLength($docblock); - } catch (DocblockParseException $e) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - $e->getMessage(), - new CodeLocation($statementsSource, $stmt) - ) - ); - return; - } - - if (!isset($parsedDocblock->tags['since'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - '@since is required for ' . $type . 's in OCP.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - - if (isset($parsedDocblock->tags['depreacted'])) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Typo in @deprecated for ' . $type . ' in OCP.', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - - private static function checkAttributeHasValidSinceVersion(\PhpParser\Node\Attribute $stmt, FileSource $statementsSource): void { - foreach ($stmt->args as $arg) { - if ($arg->name?->name === 'since') { - if (!$arg->value instanceof \PhpParser\Node\Scalar\String_) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Attribute since argument is not a valid version string', - new CodeLocation($statementsSource, $stmt) - ) - ); - } else { - if (!preg_match('/^[1-9][0-9]*(\.[0-9]+){0,3}$/', $arg->value->value)) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - 'Attribute since argument is not a valid version string', - new CodeLocation($statementsSource, $stmt) - ) - ); - } - } - } - } - } -} --- a/build/psalm/StaticVarsChecker.php +++ /dev/null @@ -1,46 +0,0 @@ -getStmt(); - if ($stmt instanceof Property) { - if ($stmt->isStatic()) { - IssueBuffer::maybeAdd( - // ImpureStaticProperty is close enough, all static properties are impure to my eyes - new \Psalm\Issue\ImpureStaticProperty( - 'Static property should not be used as they do not follow requests lifecycle', - new CodeLocation($event->getStatementsSource(), $stmt), - ), - $event->getStatementsSource()->getSuppressedIssues(), - ); - } - } elseif ($stmt instanceof Static_) { - IssueBuffer::maybeAdd( - // Same logic - new \Psalm\Issue\ImpureStaticVariable( - 'Static var should not be used as they do not follow requests lifecycle and are hard to reset', - new CodeLocation($event->getStatementsSource(), $stmt), - ), - $event->getStatementsSource()->getSuppressedIssues(), - ); - } - return null; - } -} --- a/build/psalm/StaticVarsTest.php +++ /dev/null @@ -1,23 +0,0 @@ -getExpr(); - if ($stmt instanceof VirtualNode) { - // Synthesized by Psalm itself (e.g. the implicit == of a switch/case), not written in the source - return null; - } - if (!$stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal - && !$stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) { - return null; - } - - if (self::isValueComparisonType($event, $stmt->left) && self::isValueComparisonType($event, $stmt->right)) { - return null; - } - - IssueBuffer::maybeAdd( - new \Psalm\Issue\UnrecognizedExpression( - 'Non-strict comparison operators == and != are not allowed in the Nextcloud codebase, use === and !== instead', - new CodeLocation($event->getStatementsSource()->getSource(), $stmt), - ), - $event->getStatementsSource()->getSuppressedIssues(), - ); - return null; - } - - private static function isValueComparisonType(AfterExpressionAnalysisEvent $event, PhpParser\Node\Expr $expr): bool { - $type = $event->getStatementsSource()->getNodeTypeProvider()->getType($expr); - $atomicTypes = $type?->getAtomicTypes() ?? []; - if ($atomicTypes === []) { - return false; - } - - foreach ($atomicTypes as $atomic) { - if (!$atomic instanceof TNamedObject) { - return false; - } - - $isAllowed = false; - foreach (self::VALUE_COMPARISON_CLASSES as $allowedClass) { - if (is_a($atomic->value, $allowedClass, true)) { - $isAllowed = true; - break; - } - } - if (!$isAllowed) { - return false; - } - } - - return true; - } -} --- a/build/rector-shared.php +++ /dev/null @@ -1,92 +0,0 @@ -getShortName(), $this->skippedClassNames)) { - // Skip common class names to avoid confusion - return true; - } - foreach ($this->namespacePrefixes as $prefix) { - if (str_starts_with($fullyQualifiedObjectType->getClassName(), $prefix . '\\')) { - // Import Nextcloud namespaces - return false; - } - } - // Skip everything else - return true; - } -} - -$config = RectorConfig::configure() - ->withSkip([ - $nextcloudDir . '/apps/*/3rdparty/*', - $nextcloudDir . '/apps/*/build/stubs/*', - $nextcloudDir . '/apps/*/composer/*', - $nextcloudDir . '/apps/*/config/*', - // The mock classes are excluded, as the tests explicitly test the annotations which should not be migrated to attributes - $nextcloudDir . '/tests/lib/AppFramework/Middleware/Mock/*', - $nextcloudDir . '/tests/lib/AppFramework/Middleware/Security/Mock/*', - ]) - // uncomment to reach your current PHP version - // ->withPhpSets() - ->withImportNames(importShortClasses:false) - ->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [ - 'inline_public' => true, - 'rename_property' => true, - ]) - ->withSets([ - NextcloudSets::NEXTCLOUD_32, - PHPUnitSetList::COMPOSER_BASED, - ]); - -$config->registerService(NextcloudNamespaceSkipVoter::class); - -/* Ignore all files ignored by git */ -$ignoredEntries = shell_exec('git status --porcelain --ignored ' . escapeshellarg($nextcloudDir)); -$ignoredEntries = explode("\n", $ignoredEntries); -$ignoredEntries = array_filter($ignoredEntries, static fn (string $line) => str_starts_with($line, '!! ')); -$ignoredEntries = array_map(static fn (string $line) => substr($line, 3), $ignoredEntries); -$ignoredEntries = array_values($ignoredEntries); - -foreach ($ignoredEntries as $ignoredEntry) { - if (str_ends_with($ignoredEntry, '/')) { - $config->withSkip([$ignoredEntry . '*']); - } else { - $config->withSkip([$ignoredEntry . '/*']); - } -} - -return $config; --- a/build/rector-strict.php +++ /dev/null @@ -1,95 +0,0 @@ -withPaths([ - $nextcloudDir . '/build/rector-strict.php', - $nextcloudDir . '/core/BackgroundJobs/ExpirePreviewsJob.php', - $nextcloudDir . '/lib/public/IContainer.php', - $nextcloudDir . '/apps/appstore', - $nextcloudDir . '/apps/dav/lib/Connector/Sabre/Node.php', - $nextcloudDir . '/apps/files_versions/lib/Versions/IMetadataVersion.php', - $nextcloudDir . '/lib/private/Settings/AuthorizedGroup.php', - $nextcloudDir . '/lib/private/Settings/AuthorizedGroupMapper.php', - $nextcloudDir . '/apps/settings/lib/Service/AuthorizedGroupService.php', - $nextcloudDir . '/lib/private/Files/Storage/Storage.php', - $nextcloudDir . '/lib/private/Files/Cache/Storage.php', - $nextcloudDir . '/lib/private/Files/Cache/StorageGlobal.php', - $nextcloudDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php', - $nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php', - $nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php', - $nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php', - $nextcloudDir . '/lib/public/Interaction', - $nextcloudDir . '/tests/lib/Interaction', - $nextcloudDir . '/apps/files/lib/Listener/RestrictInteractionListener.php', - $nextcloudDir . '/apps/files/tests/Listener/RestrictInteractionListenerTest.php', - $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', - $nextcloudDir . '/apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php', - $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', - $nextcloudDir . '/tests/Core/Listener/RestrictInteractionListenerTest.php', - $nextcloudDir . '/lib/unstable/Sharing', - $nextcloudDir . '/lib/private/Sharing', - $nextcloudDir . '/tests/lib/Sharing', - $nextcloudDir . '/apps/sharing', - $nextcloudDir . '/core/Sharing', - $nextcloudDir . '/tests/Core/Sharing', - $nextcloudDir . '/apps/files/lib/Sharing', - $nextcloudDir . '/apps/files/tests/Sharing', - $nextcloudDir . '/lib/public/AppFramework/ORM', - $nextcloudDir . '/lib/private/AppFramework/ORM', - $nextcloudDir . '/apps/oauth2', - $nextcloudDir . '/lib/public/SystemReport', - $nextcloudDir . '/lib/private/SystemReport', - $nextcloudDir . '/tests/lib/SystemReport', - ]) - ->withAutoloadPaths([ - // ensure rector properly autoload the public interfaces - $nextcloudDir . '/lib/public', - ]) - ->withPreparedSets( - deadCode: true, - codeQuality: true, - codingStyle: true, - typeDeclarations: true, - typeDeclarationDocblocks: true, - privatization: true, - instanceOf: true, - earlyReturn: true, - rectorPreset: true, - phpunitCodeQuality: true, - doctrineCodeQuality: true, - symfonyCodeQuality: true, - symfonyConfigs: true, - )->withPhpSets( - php83: true, - )->withSkip([ - AddSeeTestAnnotationRector::class, - ReadOnlyPropertyRector::class => [ - $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', - $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', - ], - ReadOnlyClassRector::class => [ - $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', - $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', - ], - RemoveReturnTagIncompatibleWithNativeTypeRector::class => [ - $nextcloudDir . '/lib/unstable/Sharing/Property/ISharePropertyType.php', - $nextcloudDir . '/lib/unstable/Sharing/Property/ShareProperty.php', - ], - // `@return $this` is more specific than the native `: self` on a - // non-final type; removing it breaks psalm's - // LessSpecificImplementedReturnType check (psalm-strict). - RemoveDuplicatedReturnSelfDocblockRector::class, - ]); --- a/build/rector.php +++ /dev/null @@ -1,45 +0,0 @@ -withPaths([ - $nextcloudDir . '/apps', - $nextcloudDir . '/core', - $nextcloudDir . '/ocs', - $nextcloudDir . '/ocs-provider', - $nextcloudDir . '/console.php', - $nextcloudDir . '/cron.php', - $nextcloudDir . '/index.php', - $nextcloudDir . '/occ', - $nextcloudDir . '/public.php', - $nextcloudDir . '/remote.php', - $nextcloudDir . '/status.php', - $nextcloudDir . '/version.php', - $nextcloudDir . '/lib/private', - $nextcloudDir . '/tests', - $nextcloudDir . '/build/integration/features/bootstrap', - // $nextcloudDir . '/config', - // $nextcloudDir . '/themes', - ]) - ->withTypeCoverageLevel(0) - ->withRules([ - SafeDeclareStrictTypesRector::class, - StrictInArrayRector::class, - UseIdenticalOverEqualWithSameTypeRector::class, - ]) - ->withSkip([ - ReplaceInjectedMethodCallRector::class, - ]); --- a/build/stubs/IntlChar.php +++ /dev/null @@ -1,1368 +0,0 @@ -IntlChar provides access to a number of utility methods that can be used to access information about Unicode characters.

- *

The methods and constants adhere closely to the names and behavior used by the underlying ICU library.

- * @since 7.0 - */ -class IntlChar { - const UNICODE_VERSION = 6.3; - const CODEPOINT_MIN = 0; - const CODEPOINT_MAX = 1114111; - const FOLD_CASE_DEFAULT = 0; - const FOLD_CASE_EXCLUDE_SPECIAL_I = 1; - const PROPERTY_ALPHABETIC = 0; - const PROPERTY_BINARY_START = 0; - const PROPERTY_ASCII_HEX_DIGIT = 1; - const PROPERTY_BIDI_CONTROL = 2; - const PROPERTY_BIDI_MIRRORED = 3; - const PROPERTY_DASH = 4; - const PROPERTY_DEFAULT_IGNORABLE_CODE_POINT = 5; - const PROPERTY_DEPRECATED = 6; - const PROPERTY_DIACRITIC = 7; - const PROPERTY_EXTENDER = 8; - const PROPERTY_FULL_COMPOSITION_EXCLUSION = 9; - const PROPERTY_GRAPHEME_BASE = 10; - const PROPERTY_GRAPHEME_EXTEND = 11; - const PROPERTY_GRAPHEME_LINK = 12; - const PROPERTY_HEX_DIGIT = 13; - const PROPERTY_HYPHEN = 14; - const PROPERTY_ID_CONTINUE = 15; - const PROPERTY_ID_START = 16; - const PROPERTY_IDEOGRAPHIC = 17; - const PROPERTY_IDS_BINARY_OPERATOR = 18; - const PROPERTY_IDS_TRINARY_OPERATOR = 19; - const PROPERTY_JOIN_CONTROL = 20; - const PROPERTY_LOGICAL_ORDER_EXCEPTION = 21; - const PROPERTY_LOWERCASE = 22; - const PROPERTY_MATH = 23; - const PROPERTY_NONCHARACTER_CODE_POINT = 24; - const PROPERTY_QUOTATION_MARK = 25; - const PROPERTY_RADICAL = 26; - const PROPERTY_SOFT_DOTTED = 27; - const PROPERTY_TERMINAL_PUNCTUATION = 28; - const PROPERTY_UNIFIED_IDEOGRAPH = 29; - const PROPERTY_UPPERCASE = 30; - const PROPERTY_WHITE_SPACE = 31; - const PROPERTY_XID_CONTINUE = 32; - const PROPERTY_XID_START = 33; - const PROPERTY_CASE_SENSITIVE = 34; - const PROPERTY_S_TERM = 35; - const PROPERTY_VARIATION_SELECTOR = 36; - const PROPERTY_NFD_INERT = 37; - const PROPERTY_NFKD_INERT = 38; - const PROPERTY_NFC_INERT = 39; - const PROPERTY_NFKC_INERT = 40; - const PROPERTY_SEGMENT_STARTER = 41; - const PROPERTY_PATTERN_SYNTAX = 42; - const PROPERTY_PATTERN_WHITE_SPACE = 43; - const PROPERTY_POSIX_ALNUM = 44; - const PROPERTY_POSIX_BLANK = 45; - const PROPERTY_POSIX_GRAPH = 46; - const PROPERTY_POSIX_PRINT = 47; - const PROPERTY_POSIX_XDIGIT = 48; - const PROPERTY_CASED = 49; - const PROPERTY_CASE_IGNORABLE = 50; - const PROPERTY_CHANGES_WHEN_LOWERCASED = 51; - const PROPERTY_CHANGES_WHEN_UPPERCASED = 52; - const PROPERTY_CHANGES_WHEN_TITLECASED = 53; - const PROPERTY_CHANGES_WHEN_CASEFOLDED = 54; - const PROPERTY_CHANGES_WHEN_CASEMAPPED = 55; - const PROPERTY_CHANGES_WHEN_NFKC_CASEFOLDED = 56; - const PROPERTY_BINARY_LIMIT = 57; - const PROPERTY_BIDI_CLASS = 4096; - const PROPERTY_INT_START = 4096; - const PROPERTY_BLOCK = 4097; - const PROPERTY_CANONICAL_COMBINING_CLASS = 4098; - const PROPERTY_DECOMPOSITION_TYPE = 4099; - const PROPERTY_EAST_ASIAN_WIDTH = 4100; - const PROPERTY_GENERAL_CATEGORY = 4101; - const PROPERTY_JOINING_GROUP = 4102; - const PROPERTY_JOINING_TYPE = 4103; - const PROPERTY_LINE_BREAK = 4104; - const PROPERTY_NUMERIC_TYPE = 4105; - const PROPERTY_SCRIPT = 4106; - const PROPERTY_HANGUL_SYLLABLE_TYPE = 4107; - const PROPERTY_NFD_QUICK_CHECK = 4108; - const PROPERTY_NFKD_QUICK_CHECK = 4109; - const PROPERTY_NFC_QUICK_CHECK = 4110; - const PROPERTY_NFKC_QUICK_CHECK = 4111; - const PROPERTY_LEAD_CANONICAL_COMBINING_CLASS = 4112; - const PROPERTY_TRAIL_CANONICAL_COMBINING_CLASS = 4113; - const PROPERTY_GRAPHEME_CLUSTER_BREAK = 4114; - const PROPERTY_SENTENCE_BREAK = 4115; - const PROPERTY_WORD_BREAK = 4116; - const PROPERTY_BIDI_PAIRED_BRACKET_TYPE = 4117; - const PROPERTY_INT_LIMIT = 4118; - const PROPERTY_GENERAL_CATEGORY_MASK = 8192; - const PROPERTY_MASK_START = 8192; - const PROPERTY_MASK_LIMIT = 8193; - const PROPERTY_NUMERIC_VALUE = 12288; - const PROPERTY_DOUBLE_START = 12288; - const PROPERTY_DOUBLE_LIMIT = 12289; - const PROPERTY_AGE = 16384; - const PROPERTY_STRING_START = 16384; - const PROPERTY_BIDI_MIRRORING_GLYPH = 16385; - const PROPERTY_CASE_FOLDING = 16386; - const PROPERTY_ISO_COMMENT = 16387; - const PROPERTY_LOWERCASE_MAPPING = 16388; - const PROPERTY_NAME = 16389; - const PROPERTY_SIMPLE_CASE_FOLDING = 16390; - const PROPERTY_SIMPLE_LOWERCASE_MAPPING = 16391; - const PROPERTY_SIMPLE_TITLECASE_MAPPING = 16392; - const PROPERTY_SIMPLE_UPPERCASE_MAPPING = 16393; - const PROPERTY_TITLECASE_MAPPING = 16394; - const PROPERTY_UNICODE_1_NAME = 16395; - const PROPERTY_UPPERCASE_MAPPING = 16396; - const PROPERTY_BIDI_PAIRED_BRACKET = 16397; - const PROPERTY_STRING_LIMIT = 16398; - const PROPERTY_SCRIPT_EXTENSIONS = 28672; - const PROPERTY_OTHER_PROPERTY_START = 28672; - const PROPERTY_OTHER_PROPERTY_LIMIT = 28673; - const PROPERTY_INVALID_CODE = -1; - const CHAR_CATEGORY_UNASSIGNED = 0; - const CHAR_CATEGORY_GENERAL_OTHER_TYPES = 0; - const CHAR_CATEGORY_UPPERCASE_LETTER = 1; - const CHAR_CATEGORY_LOWERCASE_LETTER = 2; - const CHAR_CATEGORY_TITLECASE_LETTER = 3; - const CHAR_CATEGORY_MODIFIER_LETTER = 4; - const CHAR_CATEGORY_OTHER_LETTER = 5; - const CHAR_CATEGORY_NON_SPACING_MARK = 6; - const CHAR_CATEGORY_ENCLOSING_MARK = 7; - const CHAR_CATEGORY_COMBINING_SPACING_MARK = 8; - const CHAR_CATEGORY_DECIMAL_DIGIT_NUMBER = 9; - const CHAR_CATEGORY_LETTER_NUMBER = 10; - const CHAR_CATEGORY_OTHER_NUMBER = 11; - const CHAR_CATEGORY_SPACE_SEPARATOR = 12; - const CHAR_CATEGORY_LINE_SEPARATOR = 13; - const CHAR_CATEGORY_PARAGRAPH_SEPARATOR = 14; - const CHAR_CATEGORY_CONTROL_CHAR = 15; - const CHAR_CATEGORY_FORMAT_CHAR = 16; - const CHAR_CATEGORY_PRIVATE_USE_CHAR = 17; - const CHAR_CATEGORY_SURROGATE = 18; - const CHAR_CATEGORY_DASH_PUNCTUATION = 19; - const CHAR_CATEGORY_START_PUNCTUATION = 20; - const CHAR_CATEGORY_END_PUNCTUATION = 21; - const CHAR_CATEGORY_CONNECTOR_PUNCTUATION = 22; - const CHAR_CATEGORY_OTHER_PUNCTUATION = 23; - const CHAR_CATEGORY_MATH_SYMBOL = 24; - const CHAR_CATEGORY_CURRENCY_SYMBOL = 25; - const CHAR_CATEGORY_MODIFIER_SYMBOL = 26; - const CHAR_CATEGORY_OTHER_SYMBOL = 27; - const CHAR_CATEGORY_INITIAL_PUNCTUATION = 28; - const CHAR_CATEGORY_FINAL_PUNCTUATION = 29; - const CHAR_CATEGORY_CHAR_CATEGORY_COUNT = 30; - const CHAR_DIRECTION_LEFT_TO_RIGHT = 0; - const CHAR_DIRECTION_RIGHT_TO_LEFT = 1; - const CHAR_DIRECTION_EUROPEAN_NUMBER = 2; - const CHAR_DIRECTION_EUROPEAN_NUMBER_SEPARATOR = 3; - const CHAR_DIRECTION_EUROPEAN_NUMBER_TERMINATOR = 4; - const CHAR_DIRECTION_ARABIC_NUMBER = 5; - const CHAR_DIRECTION_COMMON_NUMBER_SEPARATOR = 6; - const CHAR_DIRECTION_BLOCK_SEPARATOR = 7; - const CHAR_DIRECTION_SEGMENT_SEPARATOR = 8; - const CHAR_DIRECTION_WHITE_SPACE_NEUTRAL = 9; - const CHAR_DIRECTION_OTHER_NEUTRAL = 10; - const CHAR_DIRECTION_LEFT_TO_RIGHT_EMBEDDING = 11; - const CHAR_DIRECTION_LEFT_TO_RIGHT_OVERRIDE = 12; - const CHAR_DIRECTION_RIGHT_TO_LEFT_ARABIC = 13; - const CHAR_DIRECTION_RIGHT_TO_LEFT_EMBEDDING = 14; - const CHAR_DIRECTION_RIGHT_TO_LEFT_OVERRIDE = 15; - const CHAR_DIRECTION_POP_DIRECTIONAL_FORMAT = 16; - const CHAR_DIRECTION_DIR_NON_SPACING_MARK = 17; - const CHAR_DIRECTION_BOUNDARY_NEUTRAL = 18; - const CHAR_DIRECTION_FIRST_STRONG_ISOLATE = 19; - const CHAR_DIRECTION_LEFT_TO_RIGHT_ISOLATE = 20; - const CHAR_DIRECTION_RIGHT_TO_LEFT_ISOLATE = 21; - const CHAR_DIRECTION_POP_DIRECTIONAL_ISOLATE = 22; - const CHAR_DIRECTION_CHAR_DIRECTION_COUNT = 23; - const BLOCK_CODE_NO_BLOCK = 0; - const BLOCK_CODE_BASIC_LATIN = 1; - const BLOCK_CODE_LATIN_1_SUPPLEMENT = 2; - const BLOCK_CODE_LATIN_EXTENDED_A = 3; - const BLOCK_CODE_LATIN_EXTENDED_B = 4; - const BLOCK_CODE_IPA_EXTENSIONS = 5; - const BLOCK_CODE_SPACING_MODIFIER_LETTERS = 6; - const BLOCK_CODE_COMBINING_DIACRITICAL_MARKS = 7; - const BLOCK_CODE_GREEK = 8; - const BLOCK_CODE_CYRILLIC = 9; - const BLOCK_CODE_ARMENIAN = 10; - const BLOCK_CODE_HEBREW = 11; - const BLOCK_CODE_ARABIC = 12; - const BLOCK_CODE_SYRIAC = 13; - const BLOCK_CODE_THAANA = 14; - const BLOCK_CODE_DEVANAGARI = 15; - const BLOCK_CODE_BENGALI = 16; - const BLOCK_CODE_GURMUKHI = 17; - const BLOCK_CODE_GUJARATI = 18; - const BLOCK_CODE_ORIYA = 19; - const BLOCK_CODE_TAMIL = 20; - const BLOCK_CODE_TELUGU = 21; - const BLOCK_CODE_KANNADA = 22; - const BLOCK_CODE_MALAYALAM = 23; - const BLOCK_CODE_SINHALA = 24; - const BLOCK_CODE_THAI = 25; - const BLOCK_CODE_LAO = 26; - const BLOCK_CODE_TIBETAN = 27; - const BLOCK_CODE_MYANMAR = 28; - const BLOCK_CODE_GEORGIAN = 29; - const BLOCK_CODE_HANGUL_JAMO = 30; - const BLOCK_CODE_ETHIOPIC = 31; - const BLOCK_CODE_CHEROKEE = 32; - const BLOCK_CODE_UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS = 33; - const BLOCK_CODE_OGHAM = 34; - const BLOCK_CODE_RUNIC = 35; - const BLOCK_CODE_KHMER = 36; - const BLOCK_CODE_MONGOLIAN = 37; - const BLOCK_CODE_LATIN_EXTENDED_ADDITIONAL = 38; - const BLOCK_CODE_GREEK_EXTENDED = 39; - const BLOCK_CODE_GENERAL_PUNCTUATION = 40; - const BLOCK_CODE_SUPERSCRIPTS_AND_SUBSCRIPTS = 41; - const BLOCK_CODE_CURRENCY_SYMBOLS = 42; - const BLOCK_CODE_COMBINING_MARKS_FOR_SYMBOLS = 43; - const BLOCK_CODE_LETTERLIKE_SYMBOLS = 44; - const BLOCK_CODE_NUMBER_FORMS = 45; - const BLOCK_CODE_ARROWS = 46; - const BLOCK_CODE_MATHEMATICAL_OPERATORS = 47; - const BLOCK_CODE_MISCELLANEOUS_TECHNICAL = 48; - const BLOCK_CODE_CONTROL_PICTURES = 49; - const BLOCK_CODE_OPTICAL_CHARACTER_RECOGNITION = 50; - const BLOCK_CODE_ENCLOSED_ALPHANUMERICS = 51; - const BLOCK_CODE_BOX_DRAWING = 52; - const BLOCK_CODE_BLOCK_ELEMENTS = 53; - const BLOCK_CODE_GEOMETRIC_SHAPES = 54; - const BLOCK_CODE_MISCELLANEOUS_SYMBOLS = 55; - const BLOCK_CODE_DINGBATS = 56; - const BLOCK_CODE_BRAILLE_PATTERNS = 57; - const BLOCK_CODE_CJK_RADICALS_SUPPLEMENT = 58; - const BLOCK_CODE_KANGXI_RADICALS = 59; - const BLOCK_CODE_IDEOGRAPHIC_DESCRIPTION_CHARACTERS = 60; - const BLOCK_CODE_CJK_SYMBOLS_AND_PUNCTUATION = 61; - const BLOCK_CODE_HIRAGANA = 62; - const BLOCK_CODE_KATAKANA = 63; - const BLOCK_CODE_BOPOMOFO = 64; - const BLOCK_CODE_HANGUL_COMPATIBILITY_JAMO = 65; - const BLOCK_CODE_KANBUN = 66; - const BLOCK_CODE_BOPOMOFO_EXTENDED = 67; - const BLOCK_CODE_ENCLOSED_CJK_LETTERS_AND_MONTHS = 68; - const BLOCK_CODE_CJK_COMPATIBILITY = 69; - const BLOCK_CODE_CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A = 70; - const BLOCK_CODE_CJK_UNIFIED_IDEOGRAPHS = 71; - const BLOCK_CODE_YI_SYLLABLES = 72; - const BLOCK_CODE_YI_RADICALS = 73; - const BLOCK_CODE_HANGUL_SYLLABLES = 74; - const BLOCK_CODE_HIGH_SURROGATES = 75; - const BLOCK_CODE_HIGH_PRIVATE_USE_SURROGATES = 76; - const BLOCK_CODE_LOW_SURROGATES = 77; - const BLOCK_CODE_PRIVATE_USE_AREA = 78; - const BLOCK_CODE_PRIVATE_USE = 78; - const BLOCK_CODE_CJK_COMPATIBILITY_IDEOGRAPHS = 79; - const BLOCK_CODE_ALPHABETIC_PRESENTATION_FORMS = 80; - const BLOCK_CODE_ARABIC_PRESENTATION_FORMS_A = 81; - const BLOCK_CODE_COMBINING_HALF_MARKS = 82; - const BLOCK_CODE_CJK_COMPATIBILITY_FORMS = 83; - const BLOCK_CODE_SMALL_FORM_VARIANTS = 84; - const BLOCK_CODE_ARABIC_PRESENTATION_FORMS_B = 85; - const BLOCK_CODE_SPECIALS = 86; - const BLOCK_CODE_HALFWIDTH_AND_FULLWIDTH_FORMS = 87; - const BLOCK_CODE_OLD_ITALIC = 88; - const BLOCK_CODE_GOTHIC = 89; - const BLOCK_CODE_DESERET = 90; - const BLOCK_CODE_BYZANTINE_MUSICAL_SYMBOLS = 91; - const BLOCK_CODE_MUSICAL_SYMBOLS = 92; - const BLOCK_CODE_MATHEMATICAL_ALPHANUMERIC_SYMBOLS = 93; - const BLOCK_CODE_CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B = 94; - const BLOCK_CODE_CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT = 95; - const BLOCK_CODE_TAGS = 96; - const BLOCK_CODE_CYRILLIC_SUPPLEMENT = 97; - const BLOCK_CODE_CYRILLIC_SUPPLEMENTARY = 97; - const BLOCK_CODE_TAGALOG = 98; - const BLOCK_CODE_HANUNOO = 99; - const BLOCK_CODE_BUHID = 100; - const BLOCK_CODE_TAGBANWA = 101; - const BLOCK_CODE_MISCELLANEOUS_MATHEMATICAL_SYMBOLS_A = 102; - const BLOCK_CODE_SUPPLEMENTAL_ARROWS_A = 103; - const BLOCK_CODE_SUPPLEMENTAL_ARROWS_B = 104; - const BLOCK_CODE_MISCELLANEOUS_MATHEMATICAL_SYMBOLS_B = 105; - const BLOCK_CODE_SUPPLEMENTAL_MATHEMATICAL_OPERATORS = 106; - const BLOCK_CODE_KATAKANA_PHONETIC_EXTENSIONS = 107; - const BLOCK_CODE_VARIATION_SELECTORS = 108; - const BLOCK_CODE_SUPPLEMENTARY_PRIVATE_USE_AREA_A = 109; - const BLOCK_CODE_SUPPLEMENTARY_PRIVATE_USE_AREA_B = 110; - const BLOCK_CODE_LIMBU = 111; - const BLOCK_CODE_TAI_LE = 112; - const BLOCK_CODE_KHMER_SYMBOLS = 113; - const BLOCK_CODE_PHONETIC_EXTENSIONS = 114; - const BLOCK_CODE_MISCELLANEOUS_SYMBOLS_AND_ARROWS = 115; - const BLOCK_CODE_YIJING_HEXAGRAM_SYMBOLS = 116; - const BLOCK_CODE_LINEAR_B_SYLLABARY = 117; - const BLOCK_CODE_LINEAR_B_IDEOGRAMS = 118; - const BLOCK_CODE_AEGEAN_NUMBERS = 119; - const BLOCK_CODE_UGARITIC = 120; - const BLOCK_CODE_SHAVIAN = 121; - const BLOCK_CODE_OSMANYA = 122; - const BLOCK_CODE_CYPRIOT_SYLLABARY = 123; - const BLOCK_CODE_TAI_XUAN_JING_SYMBOLS = 124; - const BLOCK_CODE_VARIATION_SELECTORS_SUPPLEMENT = 125; - const BLOCK_CODE_ANCIENT_GREEK_MUSICAL_NOTATION = 126; - const BLOCK_CODE_ANCIENT_GREEK_NUMBERS = 127; - const BLOCK_CODE_ARABIC_SUPPLEMENT = 128; - const BLOCK_CODE_BUGINESE = 129; - const BLOCK_CODE_CJK_STROKES = 130; - const BLOCK_CODE_COMBINING_DIACRITICAL_MARKS_SUPPLEMENT = 131; - const BLOCK_CODE_COPTIC = 132; - const BLOCK_CODE_ETHIOPIC_EXTENDED = 133; - const BLOCK_CODE_ETHIOPIC_SUPPLEMENT = 134; - const BLOCK_CODE_GEORGIAN_SUPPLEMENT = 135; - const BLOCK_CODE_GLAGOLITIC = 136; - const BLOCK_CODE_KHAROSHTHI = 137; - const BLOCK_CODE_MODIFIER_TONE_LETTERS = 138; - const BLOCK_CODE_NEW_TAI_LUE = 139; - const BLOCK_CODE_OLD_PERSIAN = 140; - const BLOCK_CODE_PHONETIC_EXTENSIONS_SUPPLEMENT = 141; - const BLOCK_CODE_SUPPLEMENTAL_PUNCTUATION = 142; - const BLOCK_CODE_SYLOTI_NAGRI = 143; - const BLOCK_CODE_TIFINAGH = 144; - const BLOCK_CODE_VERTICAL_FORMS = 145; - const BLOCK_CODE_NKO = 146; - const BLOCK_CODE_BALINESE = 147; - const BLOCK_CODE_LATIN_EXTENDED_C = 148; - const BLOCK_CODE_LATIN_EXTENDED_D = 149; - const BLOCK_CODE_PHAGS_PA = 150; - const BLOCK_CODE_PHOENICIAN = 151; - const BLOCK_CODE_CUNEIFORM = 152; - const BLOCK_CODE_CUNEIFORM_NUMBERS_AND_PUNCTUATION = 153; - const BLOCK_CODE_COUNTING_ROD_NUMERALS = 154; - const BLOCK_CODE_SUNDANESE = 155; - const BLOCK_CODE_LEPCHA = 156; - const BLOCK_CODE_OL_CHIKI = 157; - const BLOCK_CODE_CYRILLIC_EXTENDED_A = 158; - const BLOCK_CODE_VAI = 159; - const BLOCK_CODE_CYRILLIC_EXTENDED_B = 160; - const BLOCK_CODE_SAURASHTRA = 161; - const BLOCK_CODE_KAYAH_LI = 162; - const BLOCK_CODE_REJANG = 163; - const BLOCK_CODE_CHAM = 164; - const BLOCK_CODE_ANCIENT_SYMBOLS = 165; - const BLOCK_CODE_PHAISTOS_DISC = 166; - const BLOCK_CODE_LYCIAN = 167; - const BLOCK_CODE_CARIAN = 168; - const BLOCK_CODE_LYDIAN = 169; - const BLOCK_CODE_MAHJONG_TILES = 170; - const BLOCK_CODE_DOMINO_TILES = 171; - const BLOCK_CODE_SAMARITAN = 172; - const BLOCK_CODE_UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED = 173; - const BLOCK_CODE_TAI_THAM = 174; - const BLOCK_CODE_VEDIC_EXTENSIONS = 175; - const BLOCK_CODE_LISU = 176; - const BLOCK_CODE_BAMUM = 177; - const BLOCK_CODE_COMMON_INDIC_NUMBER_FORMS = 178; - const BLOCK_CODE_DEVANAGARI_EXTENDED = 179; - const BLOCK_CODE_HANGUL_JAMO_EXTENDED_A = 180; - const BLOCK_CODE_JAVANESE = 181; - const BLOCK_CODE_MYANMAR_EXTENDED_A = 182; - const BLOCK_CODE_TAI_VIET = 183; - const BLOCK_CODE_MEETEI_MAYEK = 184; - const BLOCK_CODE_HANGUL_JAMO_EXTENDED_B = 185; - const BLOCK_CODE_IMPERIAL_ARAMAIC = 186; - const BLOCK_CODE_OLD_SOUTH_ARABIAN = 187; - const BLOCK_CODE_AVESTAN = 188; - const BLOCK_CODE_INSCRIPTIONAL_PARTHIAN = 189; - const BLOCK_CODE_INSCRIPTIONAL_PAHLAVI = 190; - const BLOCK_CODE_OLD_TURKIC = 191; - const BLOCK_CODE_RUMI_NUMERAL_SYMBOLS = 192; - const BLOCK_CODE_KAITHI = 193; - const BLOCK_CODE_EGYPTIAN_HIEROGLYPHS = 194; - const BLOCK_CODE_ENCLOSED_ALPHANUMERIC_SUPPLEMENT = 195; - const BLOCK_CODE_ENCLOSED_IDEOGRAPHIC_SUPPLEMENT = 196; - const BLOCK_CODE_CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C = 197; - const BLOCK_CODE_MANDAIC = 198; - const BLOCK_CODE_BATAK = 199; - const BLOCK_CODE_ETHIOPIC_EXTENDED_A = 200; - const BLOCK_CODE_BRAHMI = 201; - const BLOCK_CODE_BAMUM_SUPPLEMENT = 202; - const BLOCK_CODE_KANA_SUPPLEMENT = 203; - const BLOCK_CODE_PLAYING_CARDS = 204; - const BLOCK_CODE_MISCELLANEOUS_SYMBOLS_AND_PICTOGRAPHS = 205; - const BLOCK_CODE_EMOTICONS = 206; - const BLOCK_CODE_TRANSPORT_AND_MAP_SYMBOLS = 207; - const BLOCK_CODE_ALCHEMICAL_SYMBOLS = 208; - const BLOCK_CODE_CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D = 209; - const BLOCK_CODE_ARABIC_EXTENDED_A = 210; - const BLOCK_CODE_ARABIC_MATHEMATICAL_ALPHABETIC_SYMBOLS = 211; - const BLOCK_CODE_CHAKMA = 212; - const BLOCK_CODE_MEETEI_MAYEK_EXTENSIONS = 213; - const BLOCK_CODE_MEROITIC_CURSIVE = 214; - const BLOCK_CODE_MEROITIC_HIEROGLYPHS = 215; - const BLOCK_CODE_MIAO = 216; - const BLOCK_CODE_SHARADA = 217; - const BLOCK_CODE_SORA_SOMPENG = 218; - const BLOCK_CODE_SUNDANESE_SUPPLEMENT = 219; - const BLOCK_CODE_TAKRI = 220; - const BLOCK_CODE_BASSA_VAH = 221; - const BLOCK_CODE_CAUCASIAN_ALBANIAN = 222; - const BLOCK_CODE_COPTIC_EPACT_NUMBERS = 223; - const BLOCK_CODE_COMBINING_DIACRITICAL_MARKS_EXTENDED = 224; - const BLOCK_CODE_DUPLOYAN = 225; - const BLOCK_CODE_ELBASAN = 226; - const BLOCK_CODE_GEOMETRIC_SHAPES_EXTENDED = 227; - const BLOCK_CODE_GRANTHA = 228; - const BLOCK_CODE_KHOJKI = 229; - const BLOCK_CODE_KHUDAWADI = 230; - const BLOCK_CODE_LATIN_EXTENDED_E = 231; - const BLOCK_CODE_LINEAR_A = 232; - const BLOCK_CODE_MAHAJANI = 233; - const BLOCK_CODE_MANICHAEAN = 234; - const BLOCK_CODE_MENDE_KIKAKUI = 235; - const BLOCK_CODE_MODI = 236; - const BLOCK_CODE_MRO = 237; - const BLOCK_CODE_MYANMAR_EXTENDED_B = 238; - const BLOCK_CODE_NABATAEAN = 239; - const BLOCK_CODE_OLD_NORTH_ARABIAN = 240; - const BLOCK_CODE_OLD_PERMIC = 241; - const BLOCK_CODE_ORNAMENTAL_DINGBATS = 242; - const BLOCK_CODE_PAHAWH_HMONG = 243; - const BLOCK_CODE_PALMYRENE = 244; - const BLOCK_CODE_PAU_CIN_HAU = 245; - const BLOCK_CODE_PSALTER_PAHLAVI = 246; - const BLOCK_CODE_SHORTHAND_FORMAT_CONTROLS = 247; - const BLOCK_CODE_SIDDHAM = 248; - const BLOCK_CODE_SINHALA_ARCHAIC_NUMBERS = 249; - const BLOCK_CODE_SUPPLEMENTAL_ARROWS_C = 250; - const BLOCK_CODE_TIRHUTA = 251; - const BLOCK_CODE_WARANG_CITI = 252; - const BLOCK_CODE_COUNT = 263; - const BLOCK_CODE_INVALID_CODE = -1; - const BPT_NONE = 0; - const BPT_OPEN = 1; - const BPT_CLOSE = 2; - const BPT_COUNT = 3; - const EA_NEUTRAL = 0; - const EA_AMBIGUOUS = 1; - const EA_HALFWIDTH = 2; - const EA_FULLWIDTH = 3; - const EA_NARROW = 4; - const EA_WIDE = 5; - const EA_COUNT = 6; - const UNICODE_CHAR_NAME = 0; - const UNICODE_10_CHAR_NAME = 1; - const EXTENDED_CHAR_NAME = 2; - const CHAR_NAME_ALIAS = 3; - const CHAR_NAME_CHOICE_COUNT = 4; - const SHORT_PROPERTY_NAME = 0; - const LONG_PROPERTY_NAME = 1; - const PROPERTY_NAME_CHOICE_COUNT = 2; - const DT_NONE = 0; - const DT_CANONICAL = 1; - const DT_COMPAT = 2; - const DT_CIRCLE = 3; - const DT_FINAL = 4; - const DT_FONT = 5; - const DT_FRACTION = 6; - const DT_INITIAL = 7; - const DT_ISOLATED = 8; - const DT_MEDIAL = 9; - const DT_NARROW = 10; - const DT_NOBREAK = 11; - const DT_SMALL = 12; - const DT_SQUARE = 13; - const DT_SUB = 14; - const DT_SUPER = 15; - const DT_VERTICAL = 16; - const DT_WIDE = 17; - const DT_COUNT = 18; - const JT_NON_JOINING = 0; - const JT_JOIN_CAUSING = 1; - const JT_DUAL_JOINING = 2; - const JT_LEFT_JOINING = 3; - const JT_RIGHT_JOINING = 4; - const JT_TRANSPARENT = 5; - const JT_COUNT = 6; - const JG_NO_JOINING_GROUP = 0; - const JG_AIN = 1; - const JG_ALAPH = 2; - const JG_ALEF = 3; - const JG_BEH = 4; - const JG_BETH = 5; - const JG_DAL = 6; - const JG_DALATH_RISH = 7; - const JG_E = 8; - const JG_FEH = 9; - const JG_FINAL_SEMKATH = 10; - const JG_GAF = 11; - const JG_GAMAL = 12; - const JG_HAH = 13; - const JG_TEH_MARBUTA_GOAL = 14; - const JG_HAMZA_ON_HEH_GOAL = 14; - const JG_HE = 15; - const JG_HEH = 16; - const JG_HEH_GOAL = 17; - const JG_HETH = 18; - const JG_KAF = 19; - const JG_KAPH = 20; - const JG_KNOTTED_HEH = 21; - const JG_LAM = 22; - const JG_LAMADH = 23; - const JG_MEEM = 24; - const JG_MIM = 25; - const JG_NOON = 26; - const JG_NUN = 27; - const JG_PE = 28; - const JG_QAF = 29; - const JG_QAPH = 30; - const JG_REH = 31; - const JG_REVERSED_PE = 32; - const JG_SAD = 33; - const JG_SADHE = 34; - const JG_SEEN = 35; - const JG_SEMKATH = 36; - const JG_SHIN = 37; - const JG_SWASH_KAF = 38; - const JG_SYRIAC_WAW = 39; - const JG_TAH = 40; - const JG_TAW = 41; - const JG_TEH_MARBUTA = 42; - const JG_TETH = 43; - const JG_WAW = 44; - const JG_YEH = 45; - const JG_YEH_BARREE = 46; - const JG_YEH_WITH_TAIL = 47; - const JG_YUDH = 48; - const JG_YUDH_HE = 49; - const JG_ZAIN = 50; - const JG_FE = 51; - const JG_KHAPH = 52; - const JG_ZHAIN = 53; - const JG_BURUSHASKI_YEH_BARREE = 54; - const JG_FARSI_YEH = 55; - const JG_NYA = 56; - const JG_ROHINGYA_YEH = 57; - const JG_MANICHAEAN_ALEPH = 58; - const JG_MANICHAEAN_AYIN = 59; - const JG_MANICHAEAN_BETH = 60; - const JG_MANICHAEAN_DALETH = 61; - const JG_MANICHAEAN_DHAMEDH = 62; - const JG_MANICHAEAN_FIVE = 63; - const JG_MANICHAEAN_GIMEL = 64; - const JG_MANICHAEAN_HETH = 65; - const JG_MANICHAEAN_HUNDRED = 66; - const JG_MANICHAEAN_KAPH = 67; - const JG_MANICHAEAN_LAMEDH = 68; - const JG_MANICHAEAN_MEM = 69; - const JG_MANICHAEAN_NUN = 70; - const JG_MANICHAEAN_ONE = 71; - const JG_MANICHAEAN_PE = 72; - const JG_MANICHAEAN_QOPH = 73; - const JG_MANICHAEAN_RESH = 74; - const JG_MANICHAEAN_SADHE = 75; - const JG_MANICHAEAN_SAMEKH = 76; - const JG_MANICHAEAN_TAW = 77; - const JG_MANICHAEAN_TEN = 78; - const JG_MANICHAEAN_TETH = 79; - const JG_MANICHAEAN_THAMEDH = 80; - const JG_MANICHAEAN_TWENTY = 81; - const JG_MANICHAEAN_WAW = 82; - const JG_MANICHAEAN_YODH = 83; - const JG_MANICHAEAN_ZAYIN = 84; - const JG_STRAIGHT_WAW = 85; - const JG_COUNT = 86; - const GCB_OTHER = 0; - const GCB_CONTROL = 1; - const GCB_CR = 2; - const GCB_EXTEND = 3; - const GCB_L = 4; - const GCB_LF = 5; - const GCB_LV = 6; - const GCB_LVT = 7; - const GCB_T = 8; - const GCB_V = 9; - const GCB_SPACING_MARK = 10; - const GCB_PREPEND = 11; - const GCB_REGIONAL_INDICATOR = 12; - const GCB_COUNT = 13; - const WB_OTHER = 0; - const WB_ALETTER = 1; - const WB_FORMAT = 2; - const WB_KATAKANA = 3; - const WB_MIDLETTER = 4; - const WB_MIDNUM = 5; - const WB_NUMERIC = 6; - const WB_EXTENDNUMLET = 7; - const WB_CR = 8; - const WB_EXTEND = 9; - const WB_LF = 10; - const WB_MIDNUMLET = 11; - const WB_NEWLINE = 12; - const WB_REGIONAL_INDICATOR = 13; - const WB_HEBREW_LETTER = 14; - const WB_SINGLE_QUOTE = 15; - const WB_DOUBLE_QUOTE = 16; - const WB_COUNT = 17; - const SB_OTHER = 0; - const SB_ATERM = 1; - const SB_CLOSE = 2; - const SB_FORMAT = 3; - const SB_LOWER = 4; - const SB_NUMERIC = 5; - const SB_OLETTER = 6; - const SB_SEP = 7; - const SB_SP = 8; - const SB_STERM = 9; - const SB_UPPER = 10; - const SB_CR = 11; - const SB_EXTEND = 12; - const SB_LF = 13; - const SB_SCONTINUE = 14; - const SB_COUNT = 15; - const LB_UNKNOWN = 0; - const LB_AMBIGUOUS = 1; - const LB_ALPHABETIC = 2; - const LB_BREAK_BOTH = 3; - const LB_BREAK_AFTER = 4; - const LB_BREAK_BEFORE = 5; - const LB_MANDATORY_BREAK = 6; - const LB_CONTINGENT_BREAK = 7; - const LB_CLOSE_PUNCTUATION = 8; - const LB_COMBINING_MARK = 9; - const LB_CARRIAGE_RETURN = 10; - const LB_EXCLAMATION = 11; - const LB_GLUE = 12; - const LB_HYPHEN = 13; - const LB_IDEOGRAPHIC = 14; - const LB_INSEPARABLE = 15; - const LB_INSEPERABLE = 15; - const LB_INFIX_NUMERIC = 16; - const LB_LINE_FEED = 17; - const LB_NONSTARTER = 18; - const LB_NUMERIC = 19; - const LB_OPEN_PUNCTUATION = 20; - const LB_POSTFIX_NUMERIC = 21; - const LB_PREFIX_NUMERIC = 22; - const LB_QUOTATION = 23; - const LB_COMPLEX_CONTEXT = 24; - const LB_SURROGATE = 25; - const LB_SPACE = 26; - const LB_BREAK_SYMBOLS = 27; - const LB_ZWSPACE = 28; - const LB_NEXT_LINE = 29; - const LB_WORD_JOINER = 30; - const LB_H2 = 31; - const LB_H3 = 32; - const LB_JL = 33; - const LB_JT = 34; - const LB_JV = 35; - const LB_CLOSE_PARENTHESIS = 36; - const LB_CONDITIONAL_JAPANESE_STARTER = 37; - const LB_HEBREW_LETTER = 38; - const LB_REGIONAL_INDICATOR = 39; - const LB_COUNT = 40; - const NT_NONE = 0; - const NT_DECIMAL = 1; - const NT_DIGIT = 2; - const NT_NUMERIC = 3; - const NT_COUNT = 4; - const HST_NOT_APPLICABLE = 0; - const HST_LEADING_JAMO = 1; - const HST_VOWEL_JAMO = 2; - const HST_TRAILING_JAMO = 3; - const HST_LV_SYLLABLE = 4; - const HST_LVT_SYLLABLE = 5; - const HST_COUNT = 6; - - /** - * Check a binary Unicode property for a code point - * @link https://php.net/manual/ru/intlchar.hasbinaryproperty.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @param int $property The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * @return bool|null Returns TRUE or FALSE according to the binary Unicode property value for codepoint. - * Also FALSE if property is out of bounds or if the Unicode version does not have data for the property at all, or not for this code point. - * Or NULL if codepoint is out of bounds. - * @since 7.0 - */ - static public function hasBinaryProperty($codepoint, $property){} - - /** - * @link https://php.net/manual/ru/intlchar.charage.php - * Get the "age" of the code point - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return array|null The Unicode version number, as an array. For example, version 1.3.31.2 would be represented as [1, 3, 31, 2]. - * Or NULL if codepoint is out of bounds. - * @since 7.0 - */ - public static function charAge($codepoint) {} - - /** - * @link https://php.net/manual/ru/intlchar.chardigitvalue.php - * Get the decimal digit value of a decimal digit character - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|null The decimal digit value of codepoint, or -1 if it is not a decimal digit character. - * Or NULL if codepoint is out of bounds. - * @since 7.0 - */ - public static function charDigitValue($codepoint){} - - /** - * Get bidirectional category value for a code point - * @link https://php.net/manual/ru/intlchar.chardirection.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return int|null

The bidirectional category value; one of the following constants: - *

- *
    - *
  • IntlChar::CHAR_DIRECTION_LEFT_TO_RIGHT
  • - *
  • IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT
  • - *
  • IntlChar::CHAR_DIRECTION_EUROPEAN_NUMBER
  • - *
  • IntlChar::CHAR_DIRECTION_EUROPEAN_NUMBER_SEPARATOR
  • - *
  • IntlChar::CHAR_DIRECTION_EUROPEAN_NUMBER_TERMINATOR
  • - *
  • IntlChar::CHAR_DIRECTION_ARABIC_NUMBER
  • - *
  • IntlChar::CHAR_DIRECTION_COMMON_NUMBER_SEPARATOR
  • - *
  • IntlChar::CHAR_DIRECTION_BLOCK_SEPARATOR
  • - *
  • IntlChar::CHAR_DIRECTION_SEGMENT_SEPARATOR
  • - *
  • IntlChar::CHAR_DIRECTION_WHITE_SPACE_NEUTRAL
  • - *
  • IntlChar::CHAR_DIRECTION_OTHER_NEUTRAL
  • - *
  • IntlChar::CHAR_DIRECTION_LEFT_TO_RIGHT_EMBEDDING
  • - *
  • IntlChar::CHAR_DIRECTION_LEFT_TO_RIGHT_OVERRIDE
  • - *
  • IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT_ARABIC
  • - *
  • IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT_EMBEDDING
  • - *
  • IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT_OVERRIDE
  • - *
  • IntlChar::CHAR_DIRECTION_POP_DIRECTIONAL_FORMAT
  • - *
  • IntlChar::CHAR_DIRECTION_DIR_NON_SPACING_MARK
  • - *
  • IntlChar::CHAR_DIRECTION_BOUNDARY_NEUTRAL
  • - *
  • IntlChar::CHAR_DIRECTION_FIRST_STRONG_ISOLATE
  • - *
  • IntlChar::CHAR_DIRECTION_LEFT_TO_RIGHT_ISOLATE
  • - *
  • IntlChar::CHAR_DIRECTION_RIGHT_TO_LEFT_ISOLATE
  • - *
  • IntlChar::CHAR_DIRECTION_POP_DIRECTIONAL_ISOLATE
  • - *
  • IntlChar::CHAR_DIRECTION_CHAR_DIRECTION_COUNT
  • - *
- * Or NULL if codepoint is out of bounds. - * @since 7.0 - */ - public static function charDirection($codepoint) {} - - /** - * @link https://php.net/manual/en/intlchar.charfromname.php - * Find Unicode character by name and return its code point value - * @param string $characterName

Full name of the Unicode character.

- * @param int $nameChoice [optional]

- * Which set of names to use for the lookup. Can be any of these constants: - *

    - *
  • IntlChar::UNICODE_CHAR_NAME (default)
  • - *
  • IntlChar::UNICODE_10_CHAR_NAME
  • - *
  • IntlChar::EXTENDED_CHAR_NAME
  • - *
  • IntlChar::CHAR_NAME_ALIAS
  • - *
  • IntlChar::CHAR_NAME_CHOICE_COUNT
  • - *
- * @return int|null The Unicode value of the code point with the given name (as an integer), or NULL if there is no such code point. - * @since 7.0 - */ - public static function charFromName($characterName, $nameChoice = IntlChar::UNICODE_CHAR_NAME) {} - - /** - * @link https://php.net/manual/ru/intlchar.charmirror.php - * Get the "mirror-image" character for a code point - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|string|null Returns another Unicode code point that may serve as a mirror-image substitute, or codepoint itself if there is no such mapping or codepoint does not have the Bidi_Mirrored property. - * The return type will be integer unless the code point was passed as a UTF-8 string, in which case a string will be returned. - * Or NULL if codepoint will be out of bound. - */ - public static function charMirror($codepoint) {} - - /** - * Retrieve the name of a Unicode character - * @link https://php.net/manual/ru/intlchar.charname.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @param int $nameChoice [optional] Which set of names to use for the lookup. Can be any of these constants:

- *
    - *
  • IntlChar::UNICODE_CHAR_NAME (default)
  • - *
  • IntlChar::UNICODE_10_CHAR_NAME
  • - *
  • IntlChar::EXTENDED_CHAR_NAME
  • - *
  • IntlChar::CHAR_NAME_ALIAS
  • - *
  • IntlChar::CHAR_NAME_CHOICE_COUNT
  • - *
- * @return string|null The corresponding name, or an empty string if there is no name for this character, or NULL if codepoint is out of bounds. - * @since 7.0 - */ - public static function charName($codepoint, $nameChoice = IntlChar::UNICODE_CHAR_NAME) {} - - /** - * Get the general category value for a code point - * @link https://php.net/manual/ru/intlchar.chartype.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|null Returns the general category type, which may be one of the following constants: - *

    - *
  • IntlChar::CHAR_CATEGORY_UNASSIGNED
  • - *
  • IntlChar::CHAR_CATEGORY_GENERAL_OTHER_TYPES
  • - *
  • IntlChar::CHAR_CATEGORY_UPPERCASE_LETTER
  • - *
  • IntlChar::CHAR_CATEGORY_LOWERCASE_LETTER
  • - *
  • IntlChar::CHAR_CATEGORY_TITLECASE_LETTER
  • - *
  • IntlChar::CHAR_CATEGORY_MODIFIER_LETTER
  • - *
  • IntlChar::CHAR_CATEGORY_OTHER_LETTER
  • - *
  • IntlChar::CHAR_CATEGORY_NON_SPACING_MARK
  • - *
  • IntlChar::CHAR_CATEGORY_ENCLOSING_MARK
  • - *
  • IntlChar::CHAR_CATEGORY_COMBINING_SPACING_MARK
  • - *
  • IntlChar::CHAR_CATEGORY_DECIMAL_DIGIT_NUMBER
  • - *
  • IntlChar::CHAR_CATEGORY_LETTER_NUMBER
  • - *
  • IntlChar::CHAR_CATEGORY_OTHER_NUMBER
  • - *
  • IntlChar::CHAR_CATEGORY_SPACE_SEPARATOR
  • - *
  • IntlChar::CHAR_CATEGORY_LINE_SEPARATOR
  • - *
  • IntlChar::CHAR_CATEGORY_PARAGRAPH_SEPARATOR
  • - *
  • IntlChar::CHAR_CATEGORY_CONTROL_CHAR
  • - *
  • IntlChar::CHAR_CATEGORY_FORMAT_CHAR
  • - *
  • IntlChar::CHAR_CATEGORY_PRIVATE_USE_CHAR
  • - *
  • IntlChar::CHAR_CATEGORY_SURROGATE
  • - *
  • IntlChar::CHAR_CATEGORY_DASH_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_START_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_END_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_CONNECTOR_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_OTHER_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_MATH_SYMBOL
  • - *
  • IntlChar::CHAR_CATEGORY_CURRENCY_SYMBOL
  • - *
  • IntlChar::CHAR_CATEGORY_MODIFIER_SYMBOL
  • - *
  • IntlChar::CHAR_CATEGORY_OTHER_SYMBOL
  • - *
  • IntlChar::CHAR_CATEGORY_INITIAL_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_FINAL_PUNCTUATION
  • - *
  • IntlChar::CHAR_CATEGORY_CHAR_CATEGORY_COUNT
- *

Or NULL if codepoint is out of bound.

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return string|null A string containing the single character specified by the Unicode code point value. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function chr ($codepoint) - { - - } - - /** - * Get the decimal digit value of a code point for a given radix - * @link https://php.net/manual/ru/intlchar.digit.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @param int $radix

The radix (defaults to 10).

- * @return int|false|null Returns the numeric value represented by the character in the specified radix, - * or FALSE if there is no value or if the value exceeds the radix, - * or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function digit ($codepoint,$radix = 10 ) {} - - /** - * Enumerate all assigned Unicode characters within a range - * @link https://php.net/manual/ru/intlchar.enumcharnames.php - * @param int|string $start The first code point in the enumeration range. - * @param int|string $limit One more than the last code point in the enumeration range (the first one after the range). - * @param callable $callback

- * The function that is to be called for each character name. The following three arguments will be passed into it: - *

    - *
  • integer $codepoint - The numeric code point value
  • - *
  • integer $nameChoice - The same value as the nameChoice parameter below
  • - *
  • string $name - The name of the character
  • - *
- * @param int $nameChoice [optional]

- * Selector for which kind of names to enumerate. Can be any of these constants: - *

    - *
  • IntlChar::UNICODE_CHAR_NAME (default)
  • - *
  • IntlChar::UNICODE_10_CHAR_NAME
  • - *
  • IntlChar::EXTENDED_CHAR_NAME
  • - *
  • IntlChar::CHAR_NAME_ALIAS
  • - *
  • IntlChar::CHAR_NAME_CHOICE_COUNT
  • - *
- * @since 7.0 - */ - public static function enumCharNames ($start, $limit, $callback, $nameChoice = IntlChar::UNICODE_CHAR_NAME) {} - - /** - * Enumerate all code points with their Unicode general categories - * @link https://php.net/manual/ru/intlchar.enumchartypes.php - * @param callable $callable

- * The function that is to be called for each contiguous range of code points with the same general category. - * The following three arguments will be passed into it: - *

    - *
  • integer $start - The starting code point of the range
  • - *
  • integer $end - The ending code point of the range
  • - *
  • integer $name - The category type (one of the IntlChar::CHAR_CATEGORY_* constants)
  • - *
- * @since 7.0 - */ - public static function enumCharTypes ($callable) {} - - /** - * Perform case folding on a code point - * @link https://php.net/manual/en/intlchar.foldcase.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @param int $options [optional] Either IntlChar::FOLD_CASE_DEFAULT (default) or IntlChar::FOLD_CASE_EXCLUDE_SPECIAL_I. - * @return int|string|null Returns the Simple_Case_Folding of the code point, if any; otherwise the code point itself. - * Returns NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function foldCase ($codepoint, $options = IntlChar::FOLD_CASE_DEFAULT ) {} - - /** - * Get character representation for a given digit and radix - * @link https://php.net/manual/ru/intlchar.fordigit.php - * @param int $digit

The number to convert to a character.

- * @param int $radix [optional]

The radix (defaults to 10).

- * @return int The character representation (as a string) of the specified digit in the specified radix. - * @since 7.0 - */ - public static function forDigit ($digit, $radix = 10) {} - - /** - * Get the paired bracket character for a code point - * @link https://php.net/manual/ru/intlchar.getbidipairedbracket.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return int|string|null Returns the paired bracket code point, or codepoint itself if there is no such mapping. - * The return type will be integer unless the code point was passed as a UTF-8 string, in which case a string will be returned. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function getBidiPairedBracket($codepoint) {} - - /** - * Get the Unicode allocation block containing a code point - * @link https://php.net/manual/ru/intlchar.getblockcode.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|null Returns the block value for codepoint, or NULL if codepoint is out of bound. - * See the IntlChar::BLOCK_CODE_* constants for possible return values. - * @since 7.0 - */ - public static function getBlockCode($codepoint) {} - - /** - * Get the combining class of a code point - * @link https://php.net/manual/ru/intlchar.getcombiningclass.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|null Returns the combining class of the character. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function getCombiningClass ($codepoint) {} - - /** - * Get the FC_NFKC_Closure property for a code point - * @link https://php.net/manual/ru/intlchar.getfc-nfkc-closure.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return string|false|null Returns the FC_NFKC_Closure property string for the codepoint, or an empty string if there is none, - * or NULL if codepoint is out of bound, - * or FALSE if there was an error. - * @since 7.0 - */ - public static function getFC_NFKC_Closure ($codepoint) {} - - /** - * Get the max value for a Unicode property - * @link https://php.net/manual/ru/intlchar.getintpropertymaxvalue.php - * @param int $property The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * @return int The maximum value returned by {@see IntlChar::getIntPropertyValue()} for a Unicode property. <=0 if the property selector is out of range. - * @since 7.0 - */ - public static function getIntPropertyMaxValue ($property) {} - - /** - * Get the min value for a Unicode property - * @link https://php.net/manual/ru/intlchar.getintpropertyminvalue.php - * @param int $property The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * @return int The minimum value returned by {@see IntlChar::getIntPropertyValue()} for a Unicode property. 0 if the property selector is out of range. - * @since 7.0 - */ - public static function getIntPropertyMinValue ($property) {} - - /** - * Get the value for a Unicode property for a code point - * @link https://php.net/manual/ru/intlchar.getintpropertyvalue.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @param int $property The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * @return int|null

- * Returns the numeric value that is directly the property value or, for enumerated properties, corresponds to the - * numeric value of the enumerated constant of the respective property value enumeration type. - *

- *

- * Returns 0 or 1 (for FALSE/TRUE) for binary Unicode properties. - *

- *

- * Returns a bit-mask for mask properties. - *

- *

- * Returns 0 if property is out of bounds or if the Unicode version does not - * have data for the property at all, or not for this code point. - *

- *

- * Returns NULL if codepoint is out of bound. - *

- * @since 7.0 - */ - public static function getIntPropertyValue ($codepoint, $property ) {} - - /** - * Get the numeric value for a Unicode code point - * @link https://php.net/manual/ru/intlchar.getnumericvalue.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return float|null Numeric value of codepoint, or float(-123456789) if none is defined, or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function getNumericValue ($codepoint) {} - - /** - * Get the property constant value for a given property name - * @link https://php.net/manual/ru/intlchar.getpropertyenum.php - * @param string $alias The property name to be matched. The name is compared using "loose matching" as described in PropertyAliases.txt. - * @return int Returns an IntlChar::PROPERTY_ constant value, or IntlChar::PROPERTY_INVALID_CODE if the given name does not match any property. - * @since 7.0 - */ - public static function getPropertyEnum ($alias ) {} - - /** - * Get the Unicode name for a property - * @link https://php.net/manual/ru/intlchar.getpropertyname.php - * @param int $property

The Unicode property to lookup (see the IntlChar::PROPERTY_* constants).

- *

IntlChar::PROPERTY_INVALID_CODE should not be used. Also, if property is out of range, FALSE is returned.

- * @param int $nameChoice

Selector for which name to get. If out of range, FALSE is returned.

- *

All properties have a long name. Most have a short name, but some do not. Unicode allows for additional names; if present these will be returned by adding 1, 2, etc. to IntlChar::LONG_PROPERTY_NAME.

- * @return string|false

- * Returns the name, or FALSE if either the property or the nameChoice - * is out of range. - *

- *

- * If a given nameChoice returns FALSE, then all larger values of - * nameChoice will return FALSE, with one exception: if FALSE is returned for - * IntlChar::SHORT_PROPERTY_NAME, then IntlChar::LONG_PROPERTY_NAME - * (and higher) may still return a non-FALSE value. - *

- * @since 7.0 - */ - public static function getPropertyName ($property, $nameChoice = IntlChar::LONG_PROPERTY_NAME) {} - - /** - * Get the property value for a given value name - * @link https://php.net/manual/ru/intlchar.getpropertyvalueenum.php - * @param int $property

The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * If out of range, or this method doesn't work with the given value, IntlChar::PROPERTY_INVALID_CODE is returned

- * @param string $name

The value name to be matched. The name is compared using "loose matching" as described in PropertyValueAliases.txt.

- * @return int Returns the corresponding value integer, or IntlChar::PROPERTY_INVALID_CODE if the given name does not match any value of the given property, or if the property is invalid. - * @since 7.0 - */ - public static function getPropertyValueEnum ($property, $name) {} - - /** - * Get the Unicode name for a property value - * @link https://php.net/manual/ru/intlchar.getpropertyvaluename.php - * @param int $property

- * The Unicode property to lookup (see the IntlChar::PROPERTY_* constants). - * If out of range, or this method doesn't work with the given value, FALSE is returned. - *

- * @param int $value

- * Selector for a value for the given property. If out of range, FALSE is returned. - *

- *

- * In general, valid values range from 0 up to some maximum. There are a couple exceptions: - *

    - *
  • - * IntlChar::PROPERTY_BLOCK values begin at the non-zero value IntlChar::BLOCK_CODE_BASIC_LATIN - *
  • - *
  • - * IntlChar::PROPERTY_CANONICAL_COMBINING_CLASS values are not contiguous and range from 0..240. - *
  • - *
- * @param int $nameChoice [optional]

- * Selector for which name to get. If out of range, FALSE is returned. - * All values have a long name. Most have a short name, but some do not. Unicode allows for additional names; if present these will be returned by adding 1, 2, etc. to IntlChar::LONG_PROPERTY_NAME. - *

- * @return string|false Returns the name, or FALSE if either the property or the nameChoice is out of range. - * If a given nameChoice returns FALSE, then all larger values of nameChoice will return FALSE, with one exception: if FALSE is returned for IntlChar::SHORT_PROPERTY_NAME, then IntlChar::LONG_PROPERTY_NAME (and higher) may still return a non-FALSE value. - * @since 7.0 - */ - public static function getPropertyValueName ($property, $value, $nameChoice = IntlChar::LONG_PROPERTY_NAME) {} - - /** - * Get the Unicode version - * @link https://php.net/manual/ru/intlchar.getunicodeversion.php - * @return array An array containing the Unicode version number. - * @since 7.0 - */ - public static function getUnicodeVersion() {} - - /** - * Check if code point is an alphanumeric character - * @link https://php.net/manual/ru/intlchar.isalnum.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is an alphanumeric character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isalnum ($codepoint) {} - - /** - * Check if code point is a letter character - * @link https://php.net/manual/ru/intlchar.isalpha.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a letter character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isalpha ($codepoint) {} - /** - * Check if code point is a base character - * @link https://php.net/manual/ru/intlchar.isbase.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a base character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isbase ($codepoint ){} - /** - * Check if code point is a "blank" or "horizontal space" character - * @link https://php.net/manual/ru/intlchar.isblank.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is either a "blank" or "horizontal space" character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isblank ($codepoint){} - - /** - * Check if code point is a control character - * @link https://php.net/manual/ru/intlchar.iscntrl.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a control character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function iscntrl ($codepoint ) {} - - /** - * Check whether the code point is defined - * @link https://php.net/manual/ru/intlchar.isdefined.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a defined character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isdefined ($codepoint ) {} - - /** - * Check if code point is a digit character - * @link https://php.net/manual/ru/intlchar.isdigit.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a digit character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isdigit ($codepoint) {} - /** - * Check if code point is a graphic character - * @link https://php.net/manual/ru/intlchar.isgraph.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a "graphic" character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isgraph ($codepoint ) {} - /** - * Check if code point is an ignorable character - * @link https://php.net/manual/ru/intlchar.isidignorable.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is ignorable in identifiers, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isIDIgnorable ($codepoint ) {} - /** - * Check if code point is permissible in an identifier - * @link https://php.net/manual/ru/intlchar.isidpart.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is the code point may occur in an identifier, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isIDPart ($codepoint ) {} - - /** - * Check if code point is permissible as the first character in an identifier - * @link https://php.net/manual/ru/intlchar.isidstart.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint may start an identifier, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isIDStart ($codepoint ) {} - /** - * Check if code point is an ISO control code - * @link https://php.net/manual/ru/intlchar.isisocontrol.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is an ISO control code, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isISOControl ($codepoint ) {} - /** - * Check if code point is permissible in a Java identifier - * @link https://php.net/manual/ru/intlchar.isjavaidpart.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint may occur in a Java identifier, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isJavaIDPart ($codepoint ) {} - /** - * Check if code point is permissible as the first character in a Java identifier - * @link https://php.net/manual/ru/intlchar.isjavaidstart.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint may start a Java identifier, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isJavaIDStart ($codepoint ) {} - /** - * Check if code point is a space character according to Java - * @link https://php.net/manual/ru/intlchar.isjavaspacechar.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a space character according to Java, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isJavaSpaceChar ($codepoint ) {} - - /** - * Check if code point is a lowercase letter - * @link https://php.net/manual/ru/intlchar.islower.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), - * or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return bool|null Returns TRUE if codepoint is an Ll lowercase letter, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function islower ($codepoint ) {} - /** - * Check if code point has the Bidi_Mirrored property - * @link https://php.net/manual/ru/intlchar.ismirrored.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return bool|null Returns TRUE if codepoint has the Bidi_Mirrored property, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isMirrored ($codepoint ) {} - - /** - * Check if code point is a printable character - * @link https://php.net/manual/ru/intlchar.isprint.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return bool|null Returns TRUE if codepoint is a printable character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isprint ($codepoint ) {} - - /** - * Check if code point is punctuation character - * @link https://php.net/manual/ru/intlchar.ispunct.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), - * or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return bool|null Returns TRUE if codepoint is a punctuation character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function ispunct ($codepoint ) {} - /** - * Check if code point is a space character - * @link https://php.net/manual/ru/intlchar.isspace.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a space character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isspace ($codepoint ) {} - /** - * Check if code point is a titlecase letter - * @link https://php.net/manual/ru/intlchar.istitle.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a titlecase letter, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function istitle ($codepoint ){} - - /** - * Check if code point has the Alphabetic Unicode property - * @link https://php.net/manual/ru/intlchar.isualphabetic.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint has the Alphabetic Unicode property, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isUAlphabetic ($codepoint ) {} - /** - * Check if code point has the Lowercase Unicode property - * @link https://php.net/manual/ru/intlchar.isulowercase.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint has the Lowercase Unicode property, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isULowercase ($codepoint ) {} - /** - * Check if code point has the general category "Lu" (uppercase letter) - * @link https://php.net/manual/ru/intlchar.isupper.php - * @param int|string $codepoint

The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), - * or the character encoded as a UTF-8 string (e.g. "\u{2603}")

- * @return bool|null Returns TRUE if codepoint is an Lu uppercase letter, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isupper ($codepoint) {} - /** - * Check if code point has the Uppercase Unicode property - * @link https://php.net/manual/ru/intlchar.isuuppercase.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint has the Uppercase Unicode property, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isUUppercase ($codepoint) {} - /** - * Check if code point has the White_Space Unicode property - * @link https://php.net/manual/ru/intlchar.isuwhitespace.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint has the White_Space Unicode property, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isUWhiteSpace ($codepoint ) {} - /** - * Check if code point is a whitespace character according to ICU - * @link https://php.net/manual/ru/intlchar.iswhitespace.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a whitespace character according to ICU, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isWhitespace($codepoint) {} - - /** - * Check if code point is a hexadecimal digit - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return bool|null Returns TRUE if codepoint is a hexadecimal character, FALSE if not, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function isxdigit ($codepoint){} - - /** - * Return Unicode code point value of character - * @link https://php.net/manual/ru/intlchar.ord.php - * @param int|string $character

A Unicode character.

- * @return int|null Returns the Unicode code point value as an integer, NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function ord ($character) {} - - /** - * Make Unicode character lowercase - * @link https://php.net/manual/en/intlchar.tolower.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|string|null Returns the Simple_Lowercase_Mapping of the code point, if any; otherwise the code point itself. - * The return type will be integer unless the code point was passed as a UTF-8 string, in which case a string will be returned. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function tolower($codepoint) {} - /** - * Make Unicode character titlecase - * @link https://php.net/manual/ru/intlchar.totitle.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|string|null Returns the Simple_Titlecase_Mapping of the code point, if any; otherwise the code point itself. - * The return type will be integer unless the code point was passed as a UTF-8 string, in which case a string will be returned. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function totitle ($codepoint ) {} - - /** - * Make Unicode character uppercase - * @link https://php.net/manual/ru/intlchar.toupper.php - * @param int|string $codepoint The integer codepoint value (e.g. 0x2603 for U+2603 SNOWMAN), or the character encoded as a UTF-8 string (e.g. "\u{2603}") - * @return int|string|null Returns the Simple_Uppercase_Mapping of the code point, if any; otherwise the code point itself. - * The return type will be integer unless the code point was passed as a UTF-8 string, in which case a string will be returned. - * Or NULL if codepoint is out of bound. - * @since 7.0 - */ - public static function toupper ($codepoint ) {} -} --- a/build/stubs/SensitiveParameter.phpstub +++ /dev/null @@ -1,5 +0,0 @@ - value pairs. - * The constant_name must follow the normal constant naming rules. Value must evaluate to a scalar value. - * @param bool $case_sensitive The default behaviour for constants is to be declared case-sensitive; - * i.e. CONSTANT and Constant represent different values. If this parameter evaluates to FALSE - * the constants will be declared as case-insensitive symbols. - * @return bool Returns TRUE on success or FALSE on failure. - */ -function apc_define_constants($key, array $constants, $case_sensitive = true){} - -/** - * Caches a variable in the data store, only if it's not already stored - * @link https://php.net/manual/en/function.apc-add.php - * @param string $key Store the variable using this name. Keys are cache-unique, - * so attempting to use apc_add() to store data with a key that already exists will not - * overwrite the existing data, and will instead return FALSE. (This is the only difference - * between apc_add() and apc_store().) - * @param mixed $var The variable to store - * @param int $ttl Time To Live; store var in the cache for ttl seconds. After the ttl has passed, - * the stored variable will be expunged from the cache (on the next request). If no ttl is supplied - * (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @return bool - */ -function apc_add($key, $var, $ttl = 0){} - -/** - * Stores a file in the bytecode cache, bypassing all filters - * @link https://php.net/manual/en/function.apc-compile-file.php - * @param string|string[] $filename Full or relative path to a PHP file that will be - * compiled and stored in the bytecode cache. - * @param bool $atomic - * @return bool Returns TRUE on success or FALSE on failure. - */ -function apc_compile_file($filename, $atomic = true){} - -/** - * Loads a set of constants from the cache - * @link https://php.net/manual/en/function.apc-load-constants.php - * @param string $key The name of the constant set (that was stored - * with apc_define_constants()) to be retrieved. - * @param bool $case_sensitive The default behaviour for constants is to be declared case-sensitive; - * i.e. CONSTANT and Constant represent different values. If this parameter evaluates to FALSE - * the constants will be declared as case-insensitive symbols. - * @return bool Returns TRUE on success or FALSE on failure. - */ -function apc_load_constants($key, $case_sensitive = true){} - -/** - * Checks if APC key exists - * @link https://php.net/manual/en/function.apc-exists.php - * @param bool|string[] $keys A string, or an array of strings, that contain keys. - * @return bool|string[] Returns TRUE if the key exists, otherwise FALSE - * Or if an array was passed to keys, then an array is returned that - * contains all existing keys, or an empty array if none exist. - */ -function apc_exists($keys){} - -/** - * Deletes the given files from the opcode cache - * - * Accepts a string, array of strings, or APCIterator object. - * Returns True/False, or for an Array an Array of failed files. - * - * @link https://php.net/manual/en/function.apc-delete-file.php - * @param string|string[]|APCIterator $keys - * @return bool|string[] - */ -function apc_delete_file($keys){} - -/** - * Increase a stored number - * @link https://php.net/manual/en/function.apc-inc.php - * @param string $key The key of the value being increased. - * @param int $step The step, or value to increase. - * @param bool $success Optionally pass the success or fail boolean value to this referenced variable. - * @return int|bool Returns the current value of key's value on success, or FALSE on failure. - */ -function apc_inc($key, $step = 1, &$success = null){} - -/** - * Decrease a stored number - * @link https://php.net/manual/en/function.apc-dec.php - * @param string $key The key of the value being decreased. - * @param int $step The step, or value to decrease. - * @param bool $success Optionally pass the success or fail boolean value to this referenced variable. - * @return int|bool Returns the current value of key's value on success, or FALSE on failure. - */ -function apc_dec($key, $step = 1, &$success = null){} - -/** - * @link https://php.net/manual/en/function.apc-cas.php - * @param string $key - * @param int $old - * @param int $new - * @return bool - */ -function apc_cas($key, $old, $new){} - -/** - * Returns a binary dump of the given files and user variables from the APC cache - * - * A NULL for files or user_vars signals a dump of every entry, while array() will dump nothing. - * - * @link https://php.net/manual/en/function.apc-bin-dump.php - * @param string[]|null $files The files. Passing in NULL signals a dump of every entry, while passing in array() will dump nothing. - * @param string[]|null $user_vars The user vars. Passing in NULL signals a dump of every entry, while passing in array() will dump nothing. - * @return string|bool|null Returns a binary dump of the given files and user variables from the APC cache, FALSE if APC is not enabled, or NULL if an unknown error is encountered. - */ -function apc_bin_dump($files = null, $user_vars = null){} - -/** - * Output a binary dump of the given files and user variables from the APC cache to the named file - * @link https://php.net/manual/en/function.apc-bin-dumpfile.php - * @param string[]|null $files The file names being dumped. - * @param string[]|null $user_vars The user variables being dumped. - * @param string $filename The filename where the dump is being saved. - * @param int $flags Flags passed to the filename stream. See the file_put_contents() documentation for details. - * @param resource $context The context passed to the filename stream. See the file_put_contents() documentation for details. - * @return int|bool The number of bytes written to the file, otherwise FALSE if APC - * is not enabled, filename is an invalid file name, filename can't be opened, - * the file dump can't be completed (e.g., the hard drive is out of disk space), - * or an unknown error was encountered. - */ -function apc_bin_dumpfile($files, $user_vars, $filename, $flags = 0, $context = null){} - -/** - * Load the given binary dump into the APC file/user cache - * @link https://php.net/manual/en/function.apc-bin-load.php - * @param string $data The binary dump being loaded, likely from apc_bin_dump(). - * @param int $flags Either APC_BIN_VERIFY_CRC32, APC_BIN_VERIFY_MD5, or both. - * @return bool Returns TRUE if the binary dump data was loaded with success, otherwise FALSE is returned. - * FALSE is returned if APC is not enabled, or if the data is not a valid APC binary dump (e.g., unexpected size). - */ -function apc_bin_load($data, $flags = 0){} - -/** - * Load the given binary dump from the named file into the APC file/user cache - * @link https://php.net/manual/en/function.apc-bin-loadfile.php - * @param string $filename The file name containing the dump, likely from apc_bin_dumpfile(). - * @param resource $context The files context. - * @param int $flags Either APC_BIN_VERIFY_CRC32, APC_BIN_VERIFY_MD5, or both. - * @return bool Returns TRUE on success, otherwise FALSE Reasons it may return FALSE include APC - * is not enabled, filename is an invalid file name or empty, filename can't be opened, - * the file dump can't be completed, or if the data is not a valid APC binary dump (e.g., unexpected size). - */ -function apc_bin_loadfile($filename, $context = null, $flags = 0){} - -/** - * The APCIterator class - * - * The APCIterator class makes it easier to iterate over large APC caches. - * This is helpful as it allows iterating over large caches in steps, while grabbing a defined number - * of entries per lock instance, so it frees the cache locks for other activities rather than hold up - * the entire cache to grab 100 (the default) entries. Also, using regular expression matching is more - * efficient as it's been moved to the C level. - * - * @link https://php.net/manual/en/class.apciterator.php - */ -class APCIterator implements Iterator -{ - /** - * Constructs an APCIterator iterator object - * @link https://php.net/manual/en/apciterator.construct.php - * @param string $cache The cache type, which will be 'user' or 'file'. - * @param string|string[]|null $search A PCRE regular expression that matches against APC key names, - * either as a string for a single regular expression, or as an array of regular expressions. - * Or, optionally pass in NULL to skip the search. - * @param int $format The desired format, as configured with one ore more of the APC_ITER_* constants. - * @param int $chunk_size The chunk size. Must be a value greater than 0. The default value is 100. - * @param int $list The type to list. Either pass in APC_LIST_ACTIVE or APC_LIST_INACTIVE. - */ - public function __construct($cache, $search = null, $format = APC_ITER_ALL, $chunk_size = 100, $list = APC_LIST_ACTIVE){} - - /** - * Rewinds back the iterator to the first element - * @link https://php.net/manual/en/apciterator.rewind.php - */ - public function rewind(){} - - /** - * Checks if the current iterator position is valid - * @link https://php.net/manual/en/apciterator.valid.php - * @return bool Returns TRUE if the current iterator position is valid, otherwise FALSE. - */ - public function valid(){} - - /** - * Gets the current item from the APCIterator stack - * @link https://php.net/manual/en/apciterator.current.php - * @return mixed Returns the current item on success, or FALSE if no more items or exist, or on failure. - */ - public function current(){} - - /** - * Gets the current iterator key - * @link https://php.net/manual/en/apciterator.key.php - * @return string|int|bool Returns the key on success, or FALSE upon failure. - */ - public function key(){} - - /** - * Moves the iterator pointer to the next element - * @link https://php.net/manual/en/apciterator.next.php - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function next(){} - - /** - * Gets the total number of cache hits - * @link https://php.net/manual/en/apciterator.gettotalhits.php - * @return int|bool The number of hits on success, or FALSE on failure. - */ - public function getTotalHits(){} - - /** - * Gets the total cache size - * @link https://php.net/manual/en/apciterator.gettotalsize.php - * @return int|bool The total cache size. - */ - public function getTotalSize(){} - - /** - * Get the total count - * @link https://php.net/manual/en/apciterator.gettotalcount.php - * @return int|bool The total count. - */ - public function getTotalCount(){} -} - -/** - * Stubs for APCu 5.0.0 - */ - -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_LIST_ACTIVE', 1); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_LIST_DELETED', 2); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_TYPE', 1); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_KEY', 2); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_FILENAME', 4); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_DEVICE', 8); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_INODE', 16); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_VALUE', 32); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_MD5', 64); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_NUM_HITS', 128); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_MTIME', 256); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_CTIME', 512); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_DTIME', 1024); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_ATIME', 2048); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_REFCOUNT', 4096); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_MEM_SIZE', 8192); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_TTL', 16384); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_NONE', 0); -/** - * @link https://php.net/manual/en/apcu.constants.php - */ -define('APC_ITER_ALL', -1); - - -/** - * Clears the APCu cache - * @link https://php.net/manual/en/function.apcu-clear-cache.php - * - * @return bool Returns TRUE always. - */ -function apcu_clear_cache(){} - -/** - * Retrieves APCu Shared Memory Allocation information - * @link https://php.net/manual/en/function.apcu-sma-info.php - * @param bool $limited When set to FALSE (default) apcu_sma_info() will - * return a detailed information about each segment. - * - * @return array|false Array of Shared Memory Allocation data; FALSE on failure. - */ -function apcu_sma_info($limited = false){} - -/** - * Cache a variable in the data store - * @link https://php.net/manual/en/function.apcu-store.php - * @param string|array $key String: Store the variable using this name. Keys are cache-unique, - * so storing a second value with the same key will overwrite the original value. - * Array: Names in key, variables in value. - * @param mixed $var [optional] The variable to store - * @param int $ttl [optional] Time To Live; store var in the cache for ttl seconds. After the ttl has passed, - * the stored variable will be expunged from the cache (on the next request). If no ttl is supplied - * (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @return bool|array Returns TRUE on success or FALSE on failure | array with error keys. - */ -function apcu_store($key, $var, $ttl = 0){} - -/** - * Fetch a stored variable from the cache - * @link https://php.net/manual/en/function.apcu-fetch.php - * @param string|string[] $key The key used to store the value (with apcu_store()). - * If an array is passed then each element is fetched and returned. - * @param bool $success Set to TRUE in success and FALSE in failure. - * @return mixed The stored variable or array of variables on success; FALSE on failure. - */ -function apcu_fetch($key, &$success = null){} - -/** - * Removes a stored variable from the cache - * @link https://php.net/manual/en/function.apcu-delete.php - * @param string|string[]|APCuIterator $key The key used to store the value (with apcu_store()). - * @return bool|string[] Returns TRUE on success or FALSE on failure. For array of keys returns list of failed keys. - */ -function apcu_delete($key){} - -/** - * Caches a variable in the data store, only if it's not already stored - * @link https://php.net/manual/en/function.apcu-add.php - * @param string|array $key Store the variable using this name. Keys are cache-unique, - * so attempting to use apcu_add() to store data with a key that already exists will not - * overwrite the existing data, and will instead return FALSE. (This is the only difference - * between apcu_add() and apcu_store().) - * Array: Names in key, variables in value. - * @param mixed $var The variable to store - * @param int $ttl Time To Live; store var in the cache for ttl seconds. After the ttl has passed, - * the stored variable will be expunged from the cache (on the next request). If no ttl is supplied - * (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @return bool|array Returns TRUE if something has effectively been added into the cache, FALSE otherwise. - * Second syntax returns array with error keys. - */ -function apcu_add($key, $var, $ttl = 0){} - -/** - * Checks if APCu key exists - * @link https://php.net/manual/en/function.apcu-exists.php - * @param string|string[] $keys A string, or an array of strings, that contain keys. - * @return bool|string[] Returns TRUE if the key exists, otherwise FALSE - * Or if an array was passed to keys, then an array is returned that - * contains all existing keys, or an empty array if none exist. - */ -function apcu_exists($keys){} - -/** - * Increase a stored number - * @link https://php.net/manual/en/function.apcu-inc.php - * @param string $key The key of the value being increased. - * @param int $step The step, or value to increase. - * @param int $ttl Time To Live; store var in the cache for ttl seconds. After the ttl has passed, - * the stored variable will be expunged from the cache (on the next request). If no ttl is supplied - * (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @param bool $success Optionally pass the success or fail boolean value to this referenced variable. - * @return int|false Returns the current value of key's value on success, or FALSE on failure. - */ -function apcu_inc($key, $step = 1, &$success = null, $ttl = 0){} - -/** - * Decrease a stored number - * @link https://php.net/manual/en/function.apcu-dec.php - * @param string $key The key of the value being decreased. - * @param int $step The step, or value to decrease. - * @param int $ttl Time To Live; store var in the cache for ttl seconds. After the ttl has passed, - * the stored variable will be expunged from the cache (on the next request). If no ttl is supplied - * (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @param bool $success Optionally pass the success or fail boolean value to this referenced variable. - * @return int|false Returns the current value of key's value on success, or FALSE on failure. - */ -function apcu_dec($key, $step = 1, &$success = null, $ttl = 0){} - -/** - * Updates an old value with a new value - * - * apcu_cas() updates an already existing integer value if the old parameter matches the currently stored value - * with the value of the new parameter. - * - * @link https://php.net/manual/en/function.apcu-cas.php - * @param string $key The key of the value being updated. - * @param int $old The old value (the value currently stored). - * @param int $new The new value to update to. - * @return bool Returns TRUE on success or FALSE on failure. - */ -function apcu_cas($key, $old, $new){} - -/** - * Atomically fetch or generate a cache entry - * - *

Atomically attempts to find key in the cache, if it cannot be found generator is called, - * passing key as the only argument. The return value of the call is then cached with the optionally - * specified ttl, and returned. - *

- * - *

Note: When control enters apcu_entry() the lock for the cache is acquired exclusively, it is released when - * control leaves apcu_entry(): In effect, this turns the body of generator into a critical section, - * disallowing two processes from executing the same code paths concurrently. - * In addition, it prohibits the concurrent execution of any other APCu functions, - * since they will acquire the same lock. - *

- * - * @link https://php.net/manual/en/function.apcu-entry.php - * - * @param string $key Identity of cache entry - * @param callable $generator A callable that accepts key as the only argument and returns the value to cache. - *

Warning - * The only APCu function that can be called safely by generator is apcu_entry().

- * @param int $ttl [optional] Time To Live; store var in the cache for ttl seconds. - * After the ttl has passed, the stored variable will be expunged from the cache (on the next request). - * If no ttl is supplied (or if the ttl is 0), the value will persist until it is removed from the cache manually, - * or otherwise fails to exist in the cache (clear, restart, etc.). - * @return mixed Returns the cached value - * @since APCu 5.1.0 - */ -function apcu_entry($key, callable $generator, $ttl = 0){} - -/** - * Retrieves cached information from APCu's data store - * - * @link https://php.net/manual/en/function.apcu-cache-info.php - * - * @param bool $limited If limited is TRUE, the return value will exclude the individual list of cache entries. - * This is useful when trying to optimize calls for statistics gathering. - * @return array|false Array of cached data (and meta-data) or FALSE on failure - */ -function apcu_cache_info($limited = false){} - -/** - * Whether APCu is usable in the current environment - * - * @link https://www.php.net/manual/en/function.apcu-enabled.php - * - * @return bool - */ -function apcu_enabled(){} - -/** - * @param string $key - */ -function apcu_key_info($key){} - -/** - * The APCuIterator class - * - * The APCuIterator class makes it easier to iterate over large APCu caches. - * This is helpful as it allows iterating over large caches in steps, while grabbing a defined number - * of entries per lock instance, so it frees the cache locks for other activities rather than hold up - * the entire cache to grab 100 (the default) entries. Also, using regular expression matching is more - * efficient as it's been moved to the C level. - * - * @link https://php.net/manual/en/class.apcuiterator.php - * @since APCu 5.0.0 - */ -class APCuIterator implements Iterator -{ - /** - * Constructs an APCuIterator iterator object - * @link https://php.net/manual/en/apcuiterator.construct.php - * @param string|string[]|null $search A PCRE regular expression that matches against APCu key names, - * either as a string for a single regular expression, or as an array of regular expressions. - * Or, optionally pass in NULL to skip the search. - * @param int $format The desired format, as configured with one ore more of the APC_ITER_* constants. - * @param int $chunk_size The chunk size. Must be a value greater than 0. The default value is 100. - * @param int $list The type to list. Either pass in APC_LIST_ACTIVE or APC_LIST_DELETED. - */ - public function __construct($search = null, $format = APC_ITER_ALL, $chunk_size = 100, $list = APC_LIST_ACTIVE){} - - /** - * Rewinds back the iterator to the first element - * @link https://php.net/manual/en/apcuiterator.rewind.php - */ - public function rewind(){} - - /** - * Checks if the current iterator position is valid - * @link https://php.net/manual/en/apcuiterator.valid.php - * @return bool Returns TRUE if the current iterator position is valid, otherwise FALSE. - */ - public function valid(){} - - /** - * Gets the current item from the APCuIterator stack - * @link https://php.net/manual/en/apcuiterator.current.php - * @return mixed Returns the current item on success, or FALSE if no more items or exist, or on failure. - */ - public function current(){} - - /** - * Gets the current iterator key - * @link https://php.net/manual/en/apcuiterator.key.php - * @return string|int|false Returns the key on success, or FALSE upon failure. - */ - public function key(){} - - /** - * Moves the iterator pointer to the next element - * @link https://php.net/manual/en/apcuiterator.next.php - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function next(){} - - /** - * Gets the total number of cache hits - * @link https://php.net/manual/en/apcuiterator.gettotalhits.php - * @return int|false The number of hits on success, or FALSE on failure. - */ - public function getTotalHits(){} - - /** - * Gets the total cache size - * @link https://php.net/manual/en/apcuiterator.gettotalsize.php - * @return int|false The total cache size. - */ - public function getTotalSize(){} - - /** - * Get the total count - * @link https://php.net/manual/en/apcuiterator.gettotalcount.php - * @return int|false The total count. - */ - public function getTotalCount(){} -} --- a/build/stubs/app_api.php +++ /dev/null @@ -1,92 +0,0 @@ -exAppService->getExApp($appId); - if ($exApp === null) { - return ['error' => sprintf('ExApp `%s` not found', $appId)]; - } - return $this->service->requestToExApp($exApp, $route, $userId, $method, $params, $options, $request); - } - - /** - * Async request to ExApp with AppAPI auth headers - * - * @throws \Exception if ExApp not found - */ - public function asyncExAppRequest( - string $appId, - string $route, - ?string $userId = null, - string $method = 'POST', - array $params = [], - array $options = [], - ?IRequest $request = null, - ): IPromise { - $exApp = $this->exAppService->getExApp($appId); - if ($exApp === null) { - throw new \Exception(sprintf('ExApp `%s` not found', $appId)); - } - return $this->service->requestToExAppAsync($exApp, $route, $userId, $method, $params, $options, $request); - } - - /** - * Get basic ExApp info by appid - * - * @param string $appId - * - * @return array|null ExApp info (appid, version, name, enabled) or null if no ExApp found - */ - public function getExApp(string $appId): ?array { - $exApp = $this->exAppService->getExApp($appId); - if ($exApp !== null) { - $info = $exApp->jsonSerialize(); - return [ - 'appid' => $info['appid'], - 'version' => $info['version'], - 'name' => $info['name'], - 'enabled' => $info['enabled'], - ]; - } - return null; - } - } -} --- a/build/stubs/excimer.php +++ /dev/null @@ -1,282 +0,0 @@ -getLog() as $entry ) { - * var_dump( $entry->getTrace() ); - * } - */ -class ExcimerLog implements ArrayAccess, Iterator { - /** - * ExcimerLog is not constructible by user code. Objects of this type - * are available via: - * - ExcimerProfiler::getLog() - * - ExcimerProfiler::flush() - * - The callback to ExcimerProfiler::setFlushCallback() - */ - final private function __construct() { - } - - /** - * Aggregate the stack traces and convert them to a line-based format - * understood by Brendan Gregg's FlameGraph utility. Each stack trace is - * represented as a series of function names, separated by semicolons. - * After this identifier, there is a single space character, then a number - * giving the number of times the stack appeared. Then there is a line - * break. This is repeated for each unique stack trace. - * - * @return string - */ - public function formatCollapsed() { - } - - /** - * Produce an array with an element for every function which appears in - * the log. The key is a human-readable unique identifier for the function, - * method or closure. The value is an associative array with the following - * elements: - * - * - self: The number of events in which the function itself was running, - * no other userspace function was being called. This includes time - * spent in internal functions that this function called. - * - inclusive: The number of events in which this function appeared - * somewhere in the stack. - * - * And optionally the following elements, if they are relevant: - * - * - file: The filename in which the function appears - * - line: The exact line number at which the first relevant event - * occurred. - * - class: The class name in which the method is defined - * - function: The name of the function or method - * - closure_line: The line number at which the closure was defined - * - * The event counts in the "self" and "inclusive" fields are adjusted for - * overruns. They represent an estimate of the number of profiling periods - * in which those functions were present. - * - * @return array - */ - public function aggregateByFunction() { - } - - /** - * Get an array which can be JSON encoded for import into speedscope - * - * @return array - */ - public function getSpeedscopeData() { - } - - /** - * Get the total number of profiling periods represented by this log. - * - * @return int - */ - public function getEventCount() { - } - - /** - * Get the current ExcimerLogEntry object. Part of the Iterator interface. - * - * @return ExcimerLogEntry|null - */ - public function current() { - } - - /** - * Get the current integer key or null. Part of the Iterator interface. - * - * @return int|null - */ - public function key() { - } - - /** - * Advance to the next log entry. Part of the Iterator interface. - */ - public function next() { - } - - /** - * Rewind back to the first log entry. Part of the Iterator interface. - */ - public function rewind() { - } - - /** - * Check if the current position is valid. Part of the Iterator interface. - * - * @return bool - */ - public function valid() { - } - - /** - * Get the number of log entries contained in this log. This is always less - * than or equal to the number returned by getEventCount(), which includes - * overruns. - * - * @return int - */ - public function count() { - } - - /** - * Determine whether a log entry exists at the specified array offset. - * Part of the ArrayAccess interface. - * - * @param int $offset - * @return bool - */ - public function offsetExists($offset) { - } - - /** - * Get the ExcimerLogEntry object at the specified array offset. - * - * @param int $offset - * @return ExcimerLogEntry|null - */ - public function offsetGet($offset) { - } - - /** - * This function is included for compliance with the ArrayAccess interface. - * It raises a warning and does nothing. - * - * @param int $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) { - } - - /** - * This function is included for compliance with the ArrayAccess interface. - * It raises a warning and does nothing. - * - * @param int $offset - */ - public function offsetUnset($offset) { - } -} --- a/build/stubs/ftp.php +++ /dev/null @@ -1,95 +0,0 @@ -|null - * @refcount 1 - */ - function ftp_raw(FTP\Connection $ftp, string $command): ?array {} - function ftp_mkdir(FTP\Connection $ftp, string $directory): string|false {} - function ftp_rmdir(FTP\Connection $ftp, string $directory): bool {} - function ftp_chmod(FTP\Connection $ftp, int $permissions, string $filename): int|false {} - - /** @param string $response */ - function ftp_alloc(FTP\Connection $ftp, int $size, &$response = null): bool {} - - /** - * @return array|false - * @refcount 1 - */ - function ftp_nlist(FTP\Connection $ftp, string $directory): array|false {} - - /** - * @return array|false - * @refcount 1 - */ - function ftp_rawlist(FTP\Connection $ftp, string $directory, bool $recursive = false): array|false {} - - /** - * @return array|false - * @refcount 1 - */ - function ftp_mlsd(FTP\Connection $ftp, string $directory): array|false {} - - function ftp_systype(FTP\Connection $ftp): string|false {} - - /** @param resource $stream */ - function ftp_fget(FTP\Connection $ftp, $stream, string $remote_filename, int $mode = FTP_BINARY, int $offset = 0): bool {} - - /** @param resource $stream */ - function ftp_nb_fget(FTP\Connection $ftp, $stream, string $remote_filename, int $mode = FTP_BINARY, int $offset = 0): int {} - function ftp_pasv(FTP\Connection $ftp, bool $enable): bool {} - function ftp_get(FTP\Connection $ftp, string $local_filename, string $remote_filename, int $mode = FTP_BINARY, int $offset = 0): bool {} - function ftp_nb_get(FTP\Connection $ftp, string $local_filename, string $remote_filename, int $mode = FTP_BINARY, int $offset = 0): int|false {} - function ftp_nb_continue(FTP\Connection $ftp): int {} - - /** @param resource $stream */ - function ftp_fput(FTP\Connection $ftp, string $remote_filename, $stream, int $mode = FTP_BINARY, int $offset = 0): bool {} - - /** @param resource $stream */ - function ftp_nb_fput(FTP\Connection $ftp, string $remote_filename, $stream, int $mode = FTP_BINARY, int $offset = 0): int {} - function ftp_put(FTP\Connection $ftp, string $remote_filename, string $local_filename, int $mode = FTP_BINARY, int $offset = 0): bool {} - function ftp_append(FTP\Connection $ftp, string $remote_filename, string $local_filename, int $mode = FTP_BINARY): bool {} - function ftp_nb_put(FTP\Connection $ftp, string $remote_filename, string $local_filename, int $mode = FTP_BINARY, int $offset = 0): int|false {} - function ftp_size(FTP\Connection $ftp, string $filename): int {} - function ftp_mdtm(FTP\Connection $ftp, string $filename): int {} - function ftp_rename(FTP\Connection $ftp, string $from, string $to): bool {} - function ftp_delete(FTP\Connection $ftp, string $filename): bool {} - function ftp_site(FTP\Connection $ftp, string $command): bool {} - function ftp_close(FTP\Connection $ftp): bool {} - - /** @alias ftp_close */ - function ftp_quit(FTP\Connection $ftp): bool {} - - /** @param int|bool $value */ - function ftp_set_option(FTP\Connection $ftp, int $option, $value): bool {} - function ftp_get_option(FTP\Connection $ftp, int $option): int|bool {} - -} --- a/build/stubs/gd.php +++ /dev/null @@ -1,334 +0,0 @@ - - * @refcount 1 - */ -function gd_info(): array {} - -function imageloadfont(string $filename): GdFont|false {} - -function imagesetstyle(GdImage $image, array $style): bool {} - -/** @refcount 1 */ -function imagecreatetruecolor(int $width, int $height): GdImage|false {} - -function imageistruecolor(GdImage $image): bool {} - -function imagetruecolortopalette(GdImage $image, bool $dither, int $num_colors): bool {} - -function imagepalettetotruecolor(GdImage $image): bool {} - -function imagecolormatch(GdImage $image1, GdImage $image2): bool {} - -function imagesetthickness(GdImage $image, int $thickness): bool {} - -function imagefilledellipse(GdImage $image, int $center_x, int $center_y, int $width, int $height, int $color): bool {} - -function imagefilledarc(GdImage $image, int $center_x, int $center_y, int $width, int $height, int $start_angle, int $end_angle, int $color, int $style): bool {} - -function imagealphablending(GdImage $image, bool $enable): bool {} - -function imagesavealpha(GdImage $image, bool $enable): bool {} - -function imagelayereffect(GdImage $image, int $effect): bool {} - -function imagecolorallocatealpha(GdImage $image, int $red, int $green, int $blue, int $alpha): int|false {} - -function imagecolorresolvealpha(GdImage $image, int $red, int $green, int $blue, int $alpha): int {} - -function imagecolorclosestalpha(GdImage $image, int $red, int $green, int $blue, int $alpha): int {} - -function imagecolorexactalpha(GdImage $image, int $red, int $green, int $blue, int $alpha): int {} - -function imagecopyresampled(GdImage $dst_image, GdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $dst_width, int $dst_height, int $src_width, int $src_height): bool {} - -#ifdef PHP_WIN32 - -/** @refcount 1 */ -function imagegrabwindow(int $handle, bool $client_area = false): GdImage|false {} - -/** @refcount 1 */ -function imagegrabscreen(): GdImage|false {} - -#endif - -// TODO: $ignore_transparent is ignored??? -/** @refcount 1 */ -function imagerotate(GdImage $image, float $angle, int $background_color, bool $ignore_transparent = false): GdImage|false {} - -function imagesettile(GdImage $image, GdImage $tile): bool {} - -function imagesetbrush(GdImage $image, GdImage $brush): bool {} - -/** @refcount 1 */ -function imagecreate(int $width, int $height): GdImage|false {} - -function imagetypes(): int {} - -/** @refcount 1 */ -function imagecreatefromstring(string $data): GdImage|false {} - -#ifdef HAVE_GD_AVIF -/** @refcount 1 */ -function imagecreatefromavif(string $filename): GdImage|false {} -#endif - -/** @refcount 1 */ -function imagecreatefromgif(string $filename): GdImage|false {} - -#ifdef HAVE_GD_JPG -/** @refcount 1 */ -function imagecreatefromjpeg(string $filename): GdImage|false {} -#endif - -#ifdef HAVE_GD_PNG -/** @refcount 1 */ -function imagecreatefrompng(string $filename): GdImage|false {} -#endif - -#ifdef HAVE_GD_WEBP -/** @refcount 1 */ -function imagecreatefromwebp(string $filename): GdImage|false {} -#endif - -/** @refcount 1 */ -function imagecreatefromxbm(string $filename): GdImage|false {} - -#ifdef HAVE_GD_XPM -/** @refcount 1 */ -function imagecreatefromxpm(string $filename): GdImage|false {} -#endif - -/** @refcount 1 */ -function imagecreatefromwbmp(string $filename): GdImage|false {} - -/** @refcount 1 */ -function imagecreatefromgd(string $filename): GdImage|false {} - -/** @refcount 1 */ -function imagecreatefromgd2(string $filename): GdImage|false {} - -/** @refcount 1 */ -function imagecreatefromgd2part(string $filename, int $x, int $y, int $width, int $height): GdImage|false {} - -#ifdef HAVE_GD_BMP -/** @refcount 1 */ -function imagecreatefrombmp(string $filename): GdImage|false {} -#endif - -#ifdef HAVE_GD_TGA -function imagecreatefromtga(string $filename): GdImage|false {} -#endif - -function imagexbm(GdImage $image, ?string $filename, ?int $foreground_color = null): bool {} - -#ifdef HAVE_GD_AVIF -/** @param resource|string|null $file */ -function imageavif(GdImage $image, $file = null, int $quality = -1, int $speed = -1): bool {} -#endif - -/** @param resource|string|null $file */ -function imagegif(GdImage $image, $file = null): bool {} - -#ifdef HAVE_GD_PNG -/** @param resource|string|null $file */ -function imagepng(GdImage $image, $file = null, int $quality = -1, int $filters = -1): bool {} -#endif - -#ifdef HAVE_GD_WEBP -/** @param resource|string|null $file */ -function imagewebp(GdImage $image, $file = null, int $quality = -1): bool {} -#endif - -#ifdef HAVE_GD_JPG -/** @param resource|string|null $file */ -function imagejpeg(GdImage $image, $file = null, int $quality = -1): bool {} -#endif - -/** @param resource|string|null $file */ -function imagewbmp(GdImage $image, $file = null, ?int $foreground_color = null): bool {} - -function imagegd(GdImage $image, ?string $file = null): bool {} - -function imagegd2(GdImage $image, ?string $file = null, int $chunk_size = UNKNOWN, int $mode = UNKNOWN): bool {} - -#ifdef HAVE_GD_BMP -/** @param resource|string|null $file */ -function imagebmp(GdImage $image, $file = null, bool $compressed = true): bool {} -#endif - -function imagedestroy(GdImage $image): bool {} - -function imagecolorallocate(GdImage $image, int $red, int $green, int $blue): int|false {} - -function imagepalettecopy(GdImage $dst, GdImage $src): void {} - -function imagecolorat(GdImage $image, int $x, int $y): int|false {} - -function imagecolorclosest(GdImage $image, int $red, int $green, int $blue): int {} - -function imagecolorclosesthwb(GdImage $image, int $red, int $green, int $blue): int {} - -function imagecolordeallocate(GdImage $image, int $color): bool {} - -function imagecolorresolve(GdImage $image, int $red, int $green, int $blue): int {} - -function imagecolorexact(GdImage $image, int $red, int $green, int $blue): int {} - -/** @return false|null */ -function imagecolorset(GdImage $image, int $color, int $red, int $green, int $blue, int $alpha = 0): ?bool {} - -/** - * @return array - * @refcount 1 - */ -function imagecolorsforindex(GdImage $image, int $color): array {} - -function imagegammacorrect(GdImage $image, float $input_gamma, float $output_gamma): bool {} - -function imagesetpixel(GdImage $image, int $x, int $y, int $color): bool {} - -function imageline(GdImage $image, int $x1, int $y1, int $x2, int $y2, int $color): bool {} - -function imagedashedline(GdImage $image, int $x1, int $y1, int $x2, int $y2, int $color): bool {} - -function imagerectangle(GdImage $image, int $x1, int $y1, int $x2, int $y2, int $color): bool {} - -function imagefilledrectangle(GdImage $image, int $x1, int $y1, int $x2, int $y2, int $color): bool {} - -function imagearc(GdImage $image, int $center_x, int $center_y, int $width, int $height, int $start_angle, int $end_angle, int $color): bool {} - -function imageellipse(GdImage $image, int $center_x, int $center_y, int $width, int $height, int $color): bool {} - -function imagefilltoborder(GdImage $image, int $x, int $y, int $border_color, int $color): bool {} - -function imagefill(GdImage $image, int $x, int $y, int $color): bool {} - -function imagecolorstotal(GdImage $image): int {} - -function imagecolortransparent(GdImage $image, ?int $color = null): int {} - -function imageinterlace(GdImage $image, ?bool $enable = null): bool {} - -function imagepolygon(GdImage $image, array $points, int $num_points_or_color, ?int $color = null): bool {} - -function imageopenpolygon(GdImage $image, array $points, int $num_points_or_color, ?int $color = null): bool {} - -function imagefilledpolygon(GdImage $image, array $points, int $num_points_or_color, ?int $color = null): bool {} - -function imagefontwidth(GdFont|int $font): int {} - -function imagefontheight(GdFont|int $font): int {} - -function imagechar(GdImage $image, GdFont|int $font, int $x, int $y, string $char, int $color): bool {} - -function imagecharup(GdImage $image, GdFont|int $font, int $x, int $y, string $char, int $color): bool {} - -function imagestring(GdImage $image, GdFont|int $font, int $x, int $y, string $string, int $color): bool {} - -function imagestringup(GdImage $image, GdFont|int $font, int $x, int $y, string $string, int $color): bool {} - -function imagecopy(GdImage $dst_image, GdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height): bool {} - -function imagecopymerge(GdImage $dst_image, GdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height, int $pct): bool {} - -function imagecopymergegray(GdImage $dst_image, GdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height, int $pct): bool {} - -function imagecopyresized(GdImage $dst_image, GdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $dst_width, int $dst_height, int $src_width, int $src_height): bool {} - -function imagesx(GdImage $image): int {} - -function imagesy(GdImage $image): int {} - -function imagesetclip(GdImage $image, int $x1, int $y1, int $x2, int $y2): bool {} - -/** - * @return array - * @refcount 1 - */ -function imagegetclip(GdImage $image): array {} - -#ifdef HAVE_GD_FREETYPE -/** - * @return array|false - * @refcount 1 - */ -function imageftbbox(float $size, float $angle, string $font_filename, string $string, array $options = []): array|false {} - -/** - * @return array|false - * @refcount 1 - */ -function imagefttext(GdImage $image, float $size, float $angle, int $x, int $y, int $color, string $font_filename, string $text, array $options = []): array|false {} - -/** - * @return array|false - * @alias imageftbbox - */ -function imagettfbbox(float $size, float $angle, string $font_filename, string $string, array $options = []): array|false {} - -/** - * @return array|false - * @alias imagefttext - */ -function imagettftext(GdImage $image, float $size, float $angle, int $x, int $y, int $color, string $font_filename, string $text, array $options = []): array|false {} -#endif - -/** @param array|int|float|bool $args */ -function imagefilter(GdImage $image, int $filter, ...$args): bool {} - -function imageconvolution(GdImage $image, array $matrix, float $divisor, float $offset): bool {} - -function imageflip(GdImage $image, int $mode): bool {} - -function imageantialias(GdImage $image, bool $enable): bool {} - -/** @refcount 1 */ -function imagecrop(GdImage $image, array $rectangle): GdImage|false {} - -/** @refcount 1 */ -function imagecropauto(GdImage $image, int $mode = IMG_CROP_DEFAULT, float $threshold = 0.5, int $color = -1): GdImage|false {} - -/** @refcount 1 */ -function imagescale(GdImage $image, int $width, int $height = -1, int $mode = IMG_BILINEAR_FIXED): GdImage|false {} - -/** @refcount 1 */ -function imageaffine(GdImage $image, array $affine, ?array $clip = null): GdImage|false {} - -/** - * @param array|float $options - * @refcount 1 - * @return array|false - */ -function imageaffinematrixget(int $type, $options): array|false {} - -/** - * @return array|false - * @refcount 1 - */ -function imageaffinematrixconcat(array $matrix1, array $matrix2): array|false {} - -function imagegetinterpolation(GdImage $image): int {} - -function imagesetinterpolation(GdImage $image, int $method = IMG_BILINEAR_FIXED): bool {} - -/** - * @return array|true - * @refcount 1 - */ -function imageresolution(GdImage $image, ?int $resolution_x = null, ?int $resolution_y = null): array|bool {} --- a/build/stubs/imagick.php +++ /dev/null @@ -1,1386 +0,0 @@ - 0x628 - public function optimizeImageLayers(): bool {} - - // METRIC_* - public function compareImageLayers(int $metric): Imagick {} - - public function pingImageBlob(string $image): bool {} - - public function pingImageFile(resource $filehandle, ?string $filename = null): bool {} - - public function transposeImage(): bool {} - - public function transverseImage(): bool {} - - public function trimImage(float $fuzz): bool {} - - public function waveImage(float $amplitude, float $length): bool {} - - public function vignetteImage(float $black_point, float $white_point, int $x, int $y): bool {} - - public function uniqueImageColors(): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 -// PHP_ME(imagick, getimagematte, imagick_zero_args, ZEND_ACC_PUBLIC | ZEND_ACC_DEPRECATED) - /** @deprecated */ - public function getImageMatte(): bool {} -#endif -#endif - - // TODO - enabled? - public function setImageMatte(bool $matte): bool {} - - public function adaptiveResizeImage( - int $columns, - int $rows, - bool $bestfit = false, - bool $legacy = false): bool {} - - public function sketchImage(float $radius, float $sigma, float $angle): bool {} - - public function shadeImage(bool $gray, float $azimuth, float $elevation): bool {} - - public function getSizeOffset(): int {} - - public function setSizeOffset(int $columns, int $rows, int $offset): bool {} - - - public function adaptiveBlurImage( - float $radius, - float $sigma, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - public function contrastStretchImage( - float $black_point, - float $white_point, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - public function adaptiveSharpenImage( - float $radius, - float $sigma, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - - public function randomThresholdImage( - float $low, - float $high, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function roundCornersImage( - float $x_rounding, - float $y_rounding, - float $stroke_width = 10, - float $displace = 5, - float $size_correction = -6): bool {} - - /* This alias is due to BWC */ - /** - * @deprecated - * @alias Imagick::roundCornersImage - */ - public function roundCorners( - float $x_rounding, - float $y_rounding, - float $stroke_width = 10, - float $displace = 5, - float $size_correction = -6): bool {} - -#endif - - public function setIteratorIndex(int $index): bool {} - - public function getIteratorIndex(): int {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function transformImage(string $crop, string $geometry): Imagick {} -#endif -#endif - -#if MagickLibVersion > 0x630 -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageOpacity(float $opacity): bool {} -#endif - -#if MagickLibVersion >= 0x700 - public function setImageAlpha(float $alpha): bool {} -#endif - -#if MagickLibVersion < 0x700 - - /** @deprecated */ - public function orderedPosterizeImage( - string $threshold_map, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif -#endif - -#if MagickLibVersion > 0x631 - // TODO - ImagickDraw .... - public function polaroidImage(ImagickDraw $settings, float $angle): bool {} - - public function getImageProperty(string $name): string {} - - public function setImageProperty(string $name, string $value): bool {} - - public function deleteImageProperty(string $name): bool {} - - // Replaces any embedded formatting characters with the appropriate - // image property and returns the interpreted text. - // See http://www.imagemagick.org/script/escape.php for escape sequences. - // -format "%m:%f %wx%h" - public function identifyFormat(string $format): string {} - - -#if IM_HAVE_IMAGICK_SETIMAGEINTERPOLATEMETHOD - // INTERPOLATE_* - public function setImageInterpolateMethod(int $method): bool {} -#endif - - // why does this not need to be inside the 'if' for IM_HAVE_IMAGICK_SETIMAGEINTERPOLATEMETHOD ..? - public function getImageInterpolateMethod(): int {} - - public function linearStretchImage(float $black_point, float $white_point): bool {} - - public function getImageLength(): int {} - - public function extentImage(int $width, int $height, int $x, int $y): bool {} -#endif -#if MagickLibVersion > 0x633 - public function getImageOrientation(): int {} - - public function setImageOrientation(int $orientation): bool {} -#endif - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion > 0x634 -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function paintFloodfillImage( - ImagickPixel|string $fill_color, - float $fuzz, - ImagickPixel|string $border_color, - int $x, - int $y, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif -#endif -#endif - -#if MagickLibVersion > 0x635 - - // TODO - Imagick - public function clutImage(Imagick $lookup_table, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function getImageProperties(string $pattern = "*", bool $include_values = true): array {} - - public function getImageProfiles(string $pattern = "*", bool $include_values = true): array {} - - // DISTORTION_* - public function distortImage(int $distortion, array $arguments, bool $bestfit): bool {} - - public function writeImageFile(resource $filehandle, ?string $format = null): bool {} - - public function writeImagesFile(resource $filehandle, ?string $format = null): bool {} - - public function resetImagePage(string $page): bool {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageClipMask(imagick $clip_mask): bool {} - - /** @deprecated */ - public function getImageClipMask(): Imagick {} -#endif - - // TODO - x server? - public function animateImages(string $x_server): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function recolorImage(array $matrix): bool {} -#endif -#endif -#endif - -#if MagickLibVersion > 0x636 - public function setFont(string $font): bool {} - - public function getFont(): string {} - - public function setPointSize(float $point_size): bool {} - - public function getPointSize(): float {} - - // LAYERMETHOD_* - public function mergeImageLayers(int $layermethod): Imagick {} -#endif - -#if MagickLibVersion > 0x637 - // ALPHACHANNEL_* - public function setImageAlphaChannel(int $alphachannel): bool {} - - // TODO - ImagickPixel ugh -// TODO - ugh MagickBooleanType MagickFloodfillPaintImage(MagickWand *wand, -// const PixelWand *fill,const double fuzz,const PixelWand *bordercolor, -// const ssize_t x,const ssize_t y,const MagickBooleanType invert) - - public function floodfillPaintImage( - ImagickPixel|string $fill_color, - float $fuzz, - ImagickPixel|string $border_color, - int $x, - int $y, - bool $invert, - ?int $channel = Imagick::CHANNEL_DEFAULT - ): bool{} - - - - public function opaquePaintImage( - ImagickPixel|string $target_color, - ImagickPixel|string $fill_color, - float $fuzz, - bool $invert, - int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function transparentPaintImage( - ImagickPixel|string $target_color, - float $alpha, - float $fuzz, - bool $invert - ): bool {} -#endif -#if MagickLibVersion > 0x638 - public function liquidRescaleImage(int $width, int $height, float $delta_x, float $rigidity): bool {} - - public function encipherImage(string $passphrase): bool {} - -// PHP_ME(imagick, decipherimage, imagick_decipherimage_args, ZEND_ACC_PUBLIC) - public function decipherImage(string $passphrase): bool {} -#endif - -#if MagickLibVersion > 0x639 - - // GRAVITY_* - public function setGravity(int $gravity): bool {} - - public function getGravity(): int {} - - // CHANNEL_ - public function getImageChannelRange(int $channel): array {} - - public function getImageAlphaChannel(): int {} -#endif - -#if MagickLibVersion > 0x642 - public function getImageChannelDistortions( - Imagick $reference_image, - int $metric, - int $channel = Imagick::CHANNEL_DEFAULT - ): float {} -#endif - -#if MagickLibVersion > 0x643 - // GRAVITY_ - public function setImageGravity(int $gravity): bool {} - - public function getImageGravity(): int {} -#endif - -#if MagickLibVersion > 0x645 - /** - * @param int $x - * @param int $y - * @param int $width - * @param int $height - * @param string $map - * @param int $pixelstorage // PIXELSTORAGE - * @param array $pixels - * @return bool - */ - public function importImagePixels( - int $x, - int $y, - int $width, - int $height, - string $map, - int $pixelstorage, - array $pixels): bool {} - - public function deskewImage(float $threshold): bool {} - - /** - * @param int $colorspace // COLORSPACE - * @param float $cluster_threshold - * @param float $smooth_threshold - * @param bool $verbose - * @return bool - */ - public function segmentImage( - int $colorspace, - float $cluster_threshold, - float $smooth_threshold, - bool $verbose = false - ): bool {} - - // SPARSECOLORMETHOD_* - public function sparseColorImage( - int $sparsecolormethod, - array $arguments, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - public function remapImage(Imagick $replacement, int $dither_method): bool {} -#endif - - -#if PHP_IMAGICK_HAVE_HOUGHLINE - public function houghLineImage(int $width, int $height, float $threshold): bool {} -#endif - -#if MagickLibVersion > 0x646 - /** - * @param int $x - * @param int $y - * @param int $width - * @param int $height - * @param string $map e.g. "RGB" - * @param int $pixelstorage // PIXELSTORAGE - * @return array - */ - public function exportImagePixels( - int $x, - int $y, - int $width, - int $height, - string $map, - int $pixelstorage - ): array {} -#endif - -#if MagickLibVersion > 0x648 - public function getImageChannelKurtosis(int $channel = Imagick::CHANNEL_DEFAULT): array {} - - public function functionImage( - int $function, - array $parameters, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif - -#if MagickLibVersion > 0x651 - // COLORSPACE_* - public function transformImageColorspace(int $colorspace): bool {} -#endif - -#if MagickLibVersion > 0x652 - public function haldClutImage(Imagick $clut, int $channel = Imagick::CHANNEL_DEFAULT): bool {} -#endif - -#if MagickLibVersion > 0x655 - public function autoLevelImage(int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function blueShiftImage(float $factor = 1.5): bool {} -#endif - -#if MagickLibVersion > 0x656 - /** - * @param string $artifact example 'compose:args' - * @return string - */ - public function getImageArtifact(string $artifact): string {} - - /** - * @param string $artifact example 'compose:args' - * @param string $value example "1,0,-0.5,0.5" - * @return bool - */ - public function setImageArtifact(string $artifact, string $value): bool {} - - public function deleteImageArtifact(string $artifact): bool {} - - // Will return CHANNEL_* - public function getColorspace(): int {} - -// PHP_ME(imagick, setcolorspace, imagick_setcolorspace_args, ZEND_ACC_PUBLIC) - public function setColorspace(int $colorspace): bool {} - - // CHANNEL_* - public function clampImage(int $channel = Imagick::CHANNEL_DEFAULT): bool {} -#endif - -#if MagickLibVersion > 0x667 - // stack By default, images are stacked left-to-right. Set stack to MagickTrue to stack them top-to-bottom. - //offset minimum distance in pixels between images. - public function smushImages(bool $stack, int $offset): Imagick {} -#endif - -// PHP_ME(imagick, __construct, imagick_construct_args, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR) - // TODO int|float? :spocks_eyebrow.gif: - public function __construct(string|array|int|float|null $files = null) {} - - public function __toString(): string {} - -#if PHP_VERSION_ID >= 50600 - // This calls MagickGetNumberImages underneath - // mode is unused. Remove at next major release - // https://github.com/Imagick/imagick/commit/13302500c0ab0ce58e6502e68871187180f7987c - public function count(int $mode = 0): int {} -#else - public function count(): int {} -#endif - - public function getPixelIterator(): ImagickPixelIterator {} - - public function getPixelRegionIterator(int $x, int $y, int $columns, int $rows): ImagickPixelIterator {} - - public function readImage(string $filename): bool {} - - public function readImages(array $filenames): bool {} - - public function readImageBlob(string $image, ?string $filename = null): bool {} - - public function setImageFormat(string $format): bool {} - - public function scaleImage(int $columns, int $rows, bool $bestfit = false, bool $legacy = false): bool {} - - public function writeImage(?string $filename = null): bool {} - - public function writeImages(string $filename, bool $adjoin): bool {} - - // CHANNEL_ - public function blurImage(float $radius, float $sigma, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function thumbnailImage( - ?int $columns, - ?int $rows, - bool $bestfit = false, - bool $fill = false, - bool $legacy = false): bool {} - - public function cropThumbnailImage(int $width, int $height, bool $legacy = false): bool {} - - public function getImageFilename(): string {} - - public function setImageFilename(string $filename): bool {} - - public function getImageFormat(): string {} - - public function getImageMimeType(): string {} - - public function removeImage(): bool {} - - /** @alias Imagick::clear */ - public function destroy(): bool {} - - public function clear(): bool {} - - public function clone(): Imagick {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function getImageSize(): int {} -#endif -#endif - - public function getImageBlob(): string {} - - public function getImagesBlob(): string {} - - public function setFirstIterator(): bool {} - - public function setLastIterator(): bool {} - - public function resetIterator(): void {} - - public function previousImage(): bool {} - - public function nextImage(): bool {} - - public function hasPreviousImage(): bool {} - - public function hasNextImage(): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageIndex(int $index): bool {} - - /** @deprecated */ - public function getImageIndex(): int {} -#endif -#endif - - public function commentImage(string $comment): bool {} - - public function cropImage(int $width, int $height, int $x, int $y): bool {} - - public function labelImage(string $label): bool {} - - public function getImageGeometry(): array {} - - public function drawImage(ImagickDraw $drawing): bool {} - - public function setImageCompressionQuality(int $quality): bool {} - - public function getImageCompressionQuality(): int {} - - public function setImageCompression(int $compression): bool {} - - public function getImageCompression(): int {} - - public function annotateImage( - ImagickDraw $settings, - float $x, - float $y, - float $angle, - string $text - ): bool {} - - public function compositeImage( - Imagick $composite_image, - int $composite, - int $x, - int $y, - int $channel = Imagick::CHANNEL_DEFAULT): bool{} - - public function modulateImage(float $brightness, float $saturation, float $hue): bool {} - - public function getImageColors(): int {} - - - - /** - * @param ImagickDraw $settings - * @param string $tile_geometry e.g. "3x2+0+0" - * @param string $thumbnail_geometry e.g. "200x160+3+3>" - * @param int $monatgemode // MONTAGEMODE_ - * @param string $frame // "10x10+2+2" - * @return Imagick - */ - public function montageImage( - ImagickDraw $settings, - string $tile_geometry, - string $thumbnail_geometry, - int $monatgemode, - string $frame - ): Imagick {} - - public function identifyImage(bool $append_raw_output = false): array {} - - public function thresholdImage(float $threshold, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function adaptiveThresholdImage(int $width, int $height, int $offset): bool {} - - public function blackThresholdImage(ImagickPixel|string $threshold_color): bool {} - - public function whiteThresholdImage(ImagickPixel|string $threshold_color): bool {} - - public function appendImages(bool $stack): Imagick {} - - public function charcoalImage(float $radius, float $sigma): bool {} - - public function normalizeImage(int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function oilPaintImage(float $radius): bool {} - - public function posterizeImage(int $levels, bool $dither): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function radialBlurImage(float $angle, int $channel = Imagick::CHANNEL_DEFAULT): bool {} -#endif -#endif - - public function raiseImage(int $width, int $height, int $x, int $y, bool $raise): bool {} - - public function resampleImage(float $x_resolution, float $y_resolution, int $filter, float $blur): bool {} - - public function resizeImage( - int $columns, - int $rows, - int $filter, - float $blur, - bool $bestfit = false, - bool $legacy = false): bool {} - - public function rollImage(int $x, int $y): bool {} - - public function rotateImage(ImagickPixel|string $background_color, float $degrees): bool {} - - public function sampleImage(int $columns, int $rows): bool {} - - public function solarizeImage(int $threshold): bool {} - - public function shadowImage(float $opacity, float $sigma, int $x, int $y): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageAttribute(string $key, string $value): bool {} -#endif -#endif - - public function setImageBackgroundColor(ImagickPixel|string $background_color): bool {} - -#if MagickLibVersion >= 0x700 - public function setImageChannelMask(int $channel): int {} -#endif - - public function setImageCompose(int $compose): bool {} - - public function setImageDelay(int $delay): bool {} - - public function setImageDepth(int $depth): bool {} - - public function setImageGamma(float $gamma): bool {} - - public function setImageIterations(int $iterations): bool {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageMatteColor(ImagickPixel|string $matte_color): bool {} -#endif - - public function setImagePage(int $width, int $height, int $x, int $y): bool {} - - // TODO test this. - public function setImageProgressMonitor(string $filename): bool {} - -#if MagickLibVersion > 0x653 - public function setProgressMonitor(callable $callback): bool {} -#endif - - public function setImageResolution(float $x_resolution, float $y_resolution): bool {} - - // I have no idea what scene does. - public function setImageScene(int $scene): bool {} - - public function setImageTicksPerSecond(int $ticks_per_second): bool {} - - // IMGTYPE_* - public function setImageType(int $image_type): bool {} - - public function setImageUnits(int $units): bool {} - - public function sharpenImage(float $radius, float $sigma, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function shaveImage(int $columns, int $rows): bool {} - - public function shearImage(ImagickPixel|string $background_color, float $x_shear, float $y_shear): bool {} - - public function spliceImage(int $width, int $height, int $x, int $y): bool {} - - public function pingImage(string $filename): bool {} - - public function readImageFile(resource $filehandle, ?string $filename = null): bool {} - - public function displayImage(string $servername): bool {} - - public function displayImages(string $servername): bool {} - - public function spreadImage(float $radius): bool {} - - public function swirlImage(float $degrees): bool {} - - public function stripImage(): bool {} - - public static function queryFormats(string $pattern = "*"): array {} - - public static function queryFonts(string $pattern = "*"): array {} - - /* TODO $multiline == null, means we should autodetect */ - public function queryFontMetrics(ImagickDraw $settings, string $text, ?bool $multiline = null): array {} - - public function steganoImage(Imagick $watermark, int $offset): Imagick {} - - // NOISE_* - public function addNoiseImage(int $noise, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function motionBlurImage( - float $radius, - float $sigma, - float $angle, - int $channel = Imagick::CHANNEL_DEFAULT - ):bool {} - -#if MagickLibVersion < 0x700 -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) - /** @deprecated */ - public function mosaicImages(): Imagick {} -#endif -#endif - - public function morphImages(int $number_frames): Imagick {} - - public function minifyImage(): bool {} - - public function affineTransformImage(ImagickDraw $settings): bool {} - -#if MagickLibVersion < 0x700 -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) - /** @deprecated */ - public function averageImages(): Imagick {} -#endif -#endif - - public function borderImage(ImagickPixel|string $border_color, int $width, int $height): bool {} - - public static function calculateCrop( - int $original_width, - int $original_height, - int $desired_width, - int $desired_height, - bool $legacy = false): array {} - - public function chopImage(int $width, int $height, int $x, int $y): bool {} - - public function clipImage(): bool {} - - public function clipPathImage(string $pathname, bool $inside): bool {} - - /* clippathimage has been deprecated. Create alias here and use the newer API function if present */ - /** @alias Imagick::clipPathImage */ - public function clipImagePath(string $pathname, bool $inside): void {} - - public function coalesceImages(): Imagick {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function colorFloodfillImage( - ImagickPixel|string $fill_color, - float $fuzz, - ImagickPixel|string $border_color, - int $x, - int $y - ): bool {} -#endif -#endif - - // TODO - opacity is actually float if legacy is true... - public function colorizeImage( - ImagickPixel|string $colorize_color, - ImagickPixel|string|false $opacity_color, - ?bool $legacy = false ): bool {} - - public function compareImageChannels(Imagick $reference, int $channel, int $metric): array {} - - public function compareImages(Imagick $reference, int $metric): array {} - - public function contrastImage(bool $sharpen): bool {} - - public function combineImages(int $colorspace): Imagick {} - - // kernel is a 2d array of float values - public function convolveImage(array $kernel, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function cycleColormapImage(int $displace): bool {} - - public function deconstructImages(): Imagick {} - - public function despeckleImage(): bool {} - - public function edgeImage(float $radius): bool {} - - public function embossImage(float $radius, float $sigma): bool {} - - public function enhanceImage(): bool {} - - public function equalizeImage(): bool {} - - // EVALUATE_* - public function evaluateImage(int $evaluate, float $constant, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - -#if MagickLibVersion >= 0x687 -// Merge multiple images of the same size together with the selected operator. -//http://www.imagemagick.org/Usage/layers/#evaluate-sequence - - // EVALUATE_* - public function evaluateImages(int $evaluate): bool {} - -#endif - -#if MagickLibVersion < 0x700 -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) - /** @deprecated */ - public function flattenImages(): Imagick {} -#endif -#endif - public function flipImage(): bool {} - - public function flopImage(): bool {} - -#if MagickLibVersion >= 0x655 - public function forwardFourierTransformImage(bool $magnitude): bool {} -#endif - - public function frameImage( - ImagickPixel|string $matte_color, - int $width, - int $height, - int $inner_bevel, - int $outer_bevel - ): bool {} - - - public function fxImage(string $expression, int $channel = Imagick::CHANNEL_DEFAULT): Imagick {} - - public function gammaImage(float $gamma, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - - public function gaussianBlurImage(float $radius, float $sigma, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - -#if MagickLibVersion < 0x700 -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) - /** @deprecated */ - public function getImageAttribute(string $key): string {} -#endif -#endif - - public function getImageBackgroundColor(): ImagickPixel {} - - public function getImageBluePrimary(): array {} - - public function getImageBorderColor(): ImagickPixel {} - - public function getImageChannelDepth(int $channel): int {} - - public function getImageChannelDistortion(Imagick $reference, int $channel, int $metric): float {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function getImageChannelExtrema(int $channel): array {} -#endif -#endif - - public function getImageChannelMean(int $channel): array {} - - public function getImageChannelStatistics(): array {} - - // index - the offset into the image colormap. I have no idea. - public function getImageColormapColor(int $index): ImagickPixel {} - - public function getImageColorspace(): int {} - - public function getImageCompose(): int {} - - public function getImageDelay(): int {} - - public function getImageDepth(): int {} - - // METRIC_ - public function getImageDistortion(Imagick $reference, int $metric): float {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function getImageExtrema(): array {} -#endif -#endif - - public function getImageDispose(): int {} - - public function getImageGamma(): float {} - - public function getImageGreenPrimary(): array {} - - public function getImageHeight(): int {} - - public function getImageHistogram(): array {} - - public function getImageInterlaceScheme(): int {} - - public function getImageIterations(): int {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function getImageMatteColor(): ImagickPixel {} -#endif - - public function getImagePage(): array {} - - public function getImagePixelColor(int $x, int $y): ImagickPixel {} - - -#if IM_HAVE_IMAGICK_SETIMAGEPIXELCOLOR - // TODO - needs a test. - public function setImagePixelColor(int $x, int $y, ImagickPixel|string $color): ImagickPixel {} -#endif - - public function getImageProfile(string $name): string {} - - public function getImageRedPrimary(): array {} - - public function getImageRenderingIntent(): int {} - - public function getImageResolution(): array {} - - public function getImageScene(): int {} - - public function getImageSignature(): string {} - - public function getImageTicksPerSecond(): int {} - - public function getImageType(): int {} - - public function getImageUnits(): int {} - - public function getImageVirtualPixelMethod(): int {} - - public function getImageWhitePoint(): array {} - - public function getImageWidth(): int {} - - public function getNumberImages(): int {} - - public function getImageTotalInkDensity(): float {} - - public function getImageRegion(int $width, int $height, int $x, int $y): Imagick {} - - public function implodeImage(float $radius): bool {} - -#if MagickLibVersion >= 0x658 - // TODO MagickWand *magnitude_wand,MagickWand *phase_wand, - public function inverseFourierTransformImage(Imagick $complement, bool $magnitude): bool {} -#endif - - public function levelImage( - float $black_point, - float $gamma, - float $white_point, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - public function magnifyImage(): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function mapImage(imagick $map, bool $dither): bool {} - - /** @deprecated */ - public function matteFloodfillImage( - float $alpha, - float $fuzz, - ImagickPixel|string $border_color, - int $x, - int $y - ): bool {} -#endif -#endif - -#if MagickLibVersion < 0x700 -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) - /** @deprecated */ - public function medianFilterImage(float $radius): bool {} -#endif -#endif - - public function negateImage(bool $gray, int $channel = Imagick::CHANNEL_DEFAULT): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function paintOpaqueImage( - ImagickPixel|string $target_color, - ImagickPixel|string $fill_color, - float $fuzz, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - /** @deprecated */ - public function paintTransparentImage(ImagickPixel|string $target_color, float $alpha, float $fuzz): bool {} -#endif -#endif - - // PREVIEW_* - public function previewImages(int $preview): bool {} - - public function profileImage(string $name, string $profile): bool {} - - public function quantizeImage( - int $number_colors, - int $colorspace, - int $tree_depth, - bool $dither, - bool $measure_error - ): bool {} - - - public function quantizeImages( - int $number_colors, - int $colorspace, - int $tree_depth, - bool $dither, - bool $measure_error): bool {} - -#if !defined(MAGICKCORE_EXCLUDE_DEPRECATED) -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function reduceNoiseImage(float $radius): bool {} -#endif -#endif - - public function removeImageProfile(string $name): string {} - - public function separateImageChannel(int $channel): bool {} - - public function sepiaToneImage(float $threshold): bool {} - -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function setImageBias(float $bias): bool {} - - /** @deprecated */ - public function setImageBiasQuantum(string $bias): void {} -#endif - - public function setImageBluePrimary(float $x, float $y): bool {} - /* {{{ proto bool Imagick::setImageBluePrimary(float x,float y) -For IM7 the prototype is -proto bool Imagick::setImageBluePrimary(float x, float y, float z) */ - - public function setImageBorderColor(ImagickPixel|string $border_color): bool {} - - public function setImageChannelDepth(int $channel, int $depth): bool {} - - public function setImageColormapColor(int $index, ImagickPixel|string $color): bool {} - - public function setImageColorspace(int $colorspace): bool {} - - public function setImageDispose(int $dispose): bool {} - - public function setImageExtent(int $columns, int $rows): bool {} - - public function setImageGreenPrimary(float $x, float $y): bool {} - - // INTERLACE_* - public function setImageInterlaceScheme(int $interlace): bool {} - - public function setImageProfile(string $name, string $profile): bool {} - - public function setImageRedPrimary(float $x, float $y): bool {} - - // RENDERINGINTENT - public function setImageRenderingIntent(int $rendering_intent): bool {} - - public function setImageVirtualPixelMethod(int $method): bool {} - - public function setImageWhitePoint(float $x, float $y): bool {} - - public function sigmoidalContrastImage( - bool $sharpen, - float $alpha, - float $beta, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool{} - - // TODO - MagickStereoImage() composites two images and produces a single - // image that is the composite of a left and right image of a stereo pair - public function stereoImage(Imagick $offset_image): bool {} - - public function textureImage(Imagick $texture): Imagick {} - - public function tintImage( - ImagickPixel|string $tint_color, - ImagickPixel|string $opacity_color, - bool $legacy = false - ): bool {} - - public function unsharpMaskImage( - float $radius, - float $sigma, - float $amount, - float $threshold, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - - public function getImage(): Imagick {} - - public function addImage(Imagick $image): bool {} - - public function setImage(Imagick $image): bool {} - - - public function newImage( - int $columns, - int $rows, - ImagickPixel|string $background_color, - string $format = null - ): bool {} - - // TODO - canvas? description - public function newPseudoImage(int $columns, int $rows, string $pseudo_format): bool {} - - public function getCompression(): int {} - - public function getCompressionQuality(): int {} - - public static function getCopyright(): string {} - - public static function getConfigureOptions(string $pattern = "*"): string {} - - -#if MagickLibVersion > 0x660 - public static function getFeatures(): string {} -#endif - - public function getFilename(): string {} - - public function getFormat(): string {} - - public static function getHomeURL(): string {} - - public function getInterlaceScheme(): int {} - - public function getOption(string $key): string {} - - public static function getPackageName(): string {} - - public function getPage(): array {} - - public static function getQuantum(): int {} - - public static function getHdriEnabled(): bool {} - - public static function getQuantumDepth(): array {} - - public static function getQuantumRange(): array {} - - public static function getReleaseDate(): string {} - - public static function getResource(int $type): int {} - - public static function getResourceLimit(int $type): int {} - - public function getSamplingFactors(): array {} - - public function getSize(): array {} - - public static function getVersion(): array {} - - public function setBackgroundColor(ImagickPixel|string $background_color): bool {} - - public function setCompression(int $compression): bool {} - - public function setCompressionQuality(int $quality): bool {} - - public function setFilename(string $filename): bool {} - - public function setFormat(string $format): bool {} - - // INTERLACE_* - public function setInterlaceScheme(int $interlace): bool {} - - public function setOption(string $key, string $value): bool {} - - public function setPage(int $width, int $height, int $x, int $y): bool {} - - public static function setResourceLimit(int $type, int $limit): bool {} - - public function setResolution(float $x_resolution, float $y_resolution): bool {} - - public function setSamplingFactors(array $factors): bool {} - - public function setSize(int $columns, int $rows): bool {} - - // IMGTYPE_* - public function setType(int $imgtype): bool {} - -#if MagickLibVersion > 0x628 - /** @alias Imagick::getIteratorIndex */ - public function key(): int {} - -//#else -//# if defined(MAGICKCORE_EXCLUDE_DEPRECATED) -//# error "MAGICKCORE_EXCLUDE_DEPRECATED should not be defined with ImageMagick version below 6.2.8" -//# else -//// PHP_MALIAS(imagick, key, getimageindex, imagick_zero_args, ZEND_ACC_PUBLIC) -// /** @alias Imagick::getImageIndex */ -// public function key(): int {} -// -//# endif -//#endif - - /** @alias Imagick::nextImage - * @tentative-return-type - */ - public function next(): void {} - - /** @alias Imagick::setFirstIterator - * @tentative-return-type - */ - public function rewind(): void {} - - public function valid(): bool {} - - public function current(): Imagick {} - -#if MagickLibVersion >= 0x659 - public function brightnessContrastImage( - float $brightness, - float $contrast, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif - -#if MagickLibVersion > 0x661 - public function colorMatrixImage(array $color_matrix): bool {} -#endif - - public function selectiveBlurImage( - float $radius, - float $sigma, - float $threshold, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} - -#if MagickLibVersion >= 0x689 - public function rotationalBlurImage(float $angle, int $channel = Imagick::CHANNEL_DEFAULT): bool {} -#endif - -#if MagickLibVersion >= 0x683 - public function statisticImage( - int $type, - int $width, - int $height, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif - -#if MagickLibVersion >= 0x652 - public function subimageMatch(Imagick $image, ?array &$offset = null, ?float &$similarity = null, float $threshold = 0.0, int $metric = 0): Imagick {} - - /** @alias Imagick::subimageMatch */ - public function similarityimage(Imagick $image, ?array &$offset = null, ?float &$similarity = null, float $threshold = 0.0, int $metric = 0): Imagick {} -#endif - - public static function setRegistry(string $key, string $value): bool {} - - public static function getRegistry(string $key): string {} - - public static function listRegistry(): array {} - -#if MagickLibVersion >= 0x680 - - /** - * @param int $morphology MORPHOLOGY_* - * @param int $iterations - * @param ImagickKernel $kernel - * @param int $channel - * @return bool - */ - public function morphology( - int $morphology, - int $iterations, - ImagickKernel $kernel, - int $channel = Imagick::CHANNEL_DEFAULT - ): bool {} -#endif - -#ifdef IMAGICK_WITH_KERNEL -#if MagickLibVersion < 0x700 - /** @deprecated */ - public function filter(ImagickKernel $kernel, int $channel = Imagick::CHANNEL_UNDEFINED): bool {} -#endif -#endif - - public function setAntialias(bool $antialias): void {} - - public function getAntialias(): bool {} - -#if MagickLibVersion > 0x676 - /** - * @param string $color_correction_collection - * - * - * - * 0.9 1.2 0.5 - * 0.4 -0.5 0.6 - * 1.0 0.8 1.5 - * - * - * 0.85 - * - * - * - * - * @return bool - */ - public function colorDecisionListImage(string $color_correction_collection): bool {} -#endif - -#if MagickLibVersion >= 0x687 - public function optimizeImageTransparency(): void {} -#endif - -#if MagickLibVersion >= 0x660 - public function autoGammaImage(?int $channel = Imagick::CHANNEL_ALL): void {} -#endif - -#if MagickLibVersion >= 0x692 - public function autoOrient(): void {} - - /** @alias Imagick::autoOrient */ - public function autoOrientate(): void {} - - // COMPOSITE_* - public function compositeImageGravity(Imagick $image, int $composite_constant, int $gravity): bool {} - -#endif - -#if MagickLibVersion >= 0x693 - public function localContrastImage(float $radius, float $strength): void {} -#endif - -#if MagickLibVersion >= 0x700 - // Identifies the potential image type, returns one of the Imagick::IMGTYPE_* constants - public function identifyImageType(): int {} -#endif - - -#if IM_HAVE_IMAGICK_GETSETIMAGEMASK - // PIXELMASK_* - public function getImageMask(int $pixelmask): ?Imagick {} - - // PIXELMASK_* - public function setImageMask(Imagick $clip_mask, int $pixelmask): void {} -#endif - - - // TODO - needs deleting from docs. -// public function getImageMagickLicense(): string {} - - // TODO - needs deleting from docs. -// public function render(): bool {} - -// public function floodfillPaintImage( -// ImagickPixel|string $fill, -// float $fuzz, -// ImagickPixel|string $bordercolor, -// int $x, -// int $y, -// bool $invert, -// int $channel = Imagick::CHANNEL_DEFAULT): null {} -} --- a/build/stubs/intl.php +++ /dev/null @@ -1,7226 +0,0 @@ - - * Sort strings with different accents from the back of the string. This - * attribute is automatically set to - * On - * for the French locales and a few others. Users normally would not need - * to explicitly set this attribute. There is a string comparison - * performance cost when it is set On, - * but sort key length is unaffected. Possible values are: - * Collator::ON - * Collator::OFF(default) - * Collator::DEFAULT_VALUE - *

- *

- * FRENCH_COLLATION rules - *

- * F=OFF cote < coté < côte < côté - * F=ON cote < côte < coté < côté - *

- *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const FRENCH_COLLATION = 0; - - /** - *

- * The Alternate attribute is used to control the handling of the so called - * variable characters in the UCA: whitespace, punctuation and symbols. If - * Alternate is set to NonIgnorable - * (N), then differences among these characters are of the same importance - * as differences among letters. If Alternate is set to - * Shifted - * (S), then these characters are of only minor importance. The - * Shifted value is often used in combination with - * Strength - * set to Quaternary. In such a case, whitespace, punctuation, and symbols - * are considered when comparing strings, but only if all other aspects of - * the strings (base letters, accents, and case) are identical. If - * Alternate is not set to Shifted, then there is no difference between a - * Strength of 3 and a Strength of 4. For more information and examples, - * see Variable_Weighting in the - * UCA. - * The reason the Alternate values are not simply - * On and Off - * is that additional Alternate values may be added in the future. The UCA - * option Blanked is expressed with Strength set to 3, and Alternate set to - * Shifted. The default for most locales is NonIgnorable. If Shifted is - * selected, it may be slower if there are many strings that are the same - * except for punctuation; sort key length will not be affected unless the - * strength level is also increased. - *

- *

- * Possible values are: - * Collator::NON_IGNORABLE(default) - * Collator::SHIFTED - * Collator::DEFAULT_VALUE - *

- *

- * ALTERNATE_HANDLING rules - *

- * S=3, A=N di Silva < Di Silva < diSilva < U.S.A. < USA - * S=3, A=S di Silva = diSilva < Di Silva < U.S.A. = USA - * S=4, A=S di Silva < diSilva < Di Silva < U.S.A. < USA - *

- *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const ALTERNATE_HANDLING = 1; - - /** - *

- * The Case_First attribute is used to control whether uppercase letters - * come before lowercase letters or vice versa, in the absence of other - * differences in the strings. The possible values are - * Uppercase_First - * (U) and Lowercase_First - * (L), plus the standard Default - * and Off. - * There is almost no difference between the Off and Lowercase_First - * options in terms of results, so typically users will not use - * Lowercase_First: only Off or Uppercase_First. (People interested in the - * detailed differences between X and L should consult the Collation - * Customization). Specifying either L or U won't affect string comparison - * performance, but will affect the sort key length. - *

- *

- * Possible values are: - * Collator::OFF(default) - * Collator::LOWER_FIRST - * Collator::UPPER_FIRST - * Collator:DEFAULT - *

- *

- * CASE_FIRST rules - *

- * C=X or C=L "china" < "China" < "denmark" < "Denmark" - * C=U "China" < "china" < "Denmark" < "denmark" - *

- *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const CASE_FIRST = 2; - - /** - *

- * The Case_Level attribute is used when ignoring accents but not case. In - * such a situation, set Strength to be Primary, - * and Case_Level to be On. - * In most locales, this setting is Off by default. There is a small - * string comparison performance and sort key impact if this attribute is - * set to be On. - *

- *

- * Possible values are: - * Collator::OFF(default) - * Collator::ON - * Collator::DEFAULT_VALUE - *

- *

- * CASE_LEVEL rules - *

- * S=1, E=X role = Role = rôle - * S=1, E=O role = rôle < Role - *

- *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const CASE_LEVEL = 3; - - /** - *

- * The Normalization setting determines whether text is thoroughly - * normalized or not in comparison. Even if the setting is off (which is - * the default for many locales), text as represented in common usage will - * compare correctly (for details, see UTN #5). Only if the accent marks - * are in noncanonical order will there be a problem. If the setting is - * On, - * then the best results are guaranteed for all possible text input. - * There is a medium string comparison performance cost if this attribute - * is On, - * depending on the frequency of sequences that require normalization. - * There is no significant effect on sort key length. If the input text is - * known to be in NFD or NFKD normalization forms, there is no need to - * enable this Normalization option. - *

- *

- * Possible values are: - * Collator::OFF(default) - * Collator::ON - * Collator::DEFAULT_VALUE - *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const NORMALIZATION_MODE = 4; - - /** - *

- * The ICU Collation Service supports many levels of comparison (named - * "Levels", but also known as "Strengths"). Having these categories - * enables ICU to sort strings precisely according to local conventions. - * However, by allowing the levels to be selectively employed, searching - * for a string in text can be performed with various matching conditions. - * For more detailed information, see - * collator_set_strength chapter. - *

- *

- * Possible values are: - * Collator::PRIMARY - * Collator::SECONDARY - * Collator::TERTIARY(default) - * Collator::QUATERNARY - * Collator::IDENTICAL - * Collator::DEFAULT_VALUE - *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const STRENGTH = 5; - - /** - *

- * Compatibility with JIS x 4061 requires the introduction of an additional - * level to distinguish Hiragana and Katakana characters. If compatibility - * with that standard is required, then this attribute should be set - * On, - * and the strength set to Quaternary. This will affect sort key length - * and string comparison string comparison performance. - *

- *

- * Possible values are: - * Collator::OFF(default) - * Collator::ON - * Collator::DEFAULT_VALUE - *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const HIRAGANA_QUATERNARY_MODE = 6; - - /** - *

- * When turned on, this attribute generates a collation key for the numeric - * value of substrings of digits. This is a way to get '100' to sort AFTER - * '2'. - *

- *

- * Possible values are: - * Collator::OFF(default) - * Collator::ON - * Collator::DEFAULT_VALUE - *

- * @link https://php.net/manual/en/class.collator.php#intl.collator-constants - */ - public const NUMERIC_COLLATION = 7; - public const SORT_REGULAR = 0; - public const SORT_STRING = 1; - public const SORT_NUMERIC = 2; - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a collator - * @link https://php.net/manual/en/collator.construct.php - * @param string $locale - */ - public function __construct(string $locale) {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a collator - * @link https://php.net/manual/en/collator.create.php - * @param string $locale

- * The locale containing the required collation rules. Special values for - * locales can be passed in - if null is passed for the locale, the - * default locale collation rules will be used. If empty string ("") or - * "root" are passed, UCA rules will be used. - *

- * @return Collator|null Return new instance of Collator object, or NULL - * on error. - */ - public static function create(string $locale): ?Collator {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Compare two Unicode strings - * @link https://php.net/manual/en/collator.compare.php - * @param string $string1

- * The first string to compare. - *

- * @param string $string2

- * The second string to compare. - *

- * @return int|false Return comparison result:

- *

- *

- * 1 if str1 is greater than - * str2 ; - *

- *

- * 0 if str1 is equal to - * str2; - *

- *

- * -1 if str1 is less than - * str2 . - *

- * On error - * boolean - * FALSE - * is returned. - */ - public function compare( - string $string1, - string $string2 - ): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array using specified collator - * @link https://php.net/manual/en/collator.sort.php - * @param string[] &$array

- * Array of strings to sort. - *

- * @param int $flags [optional]

- * Optional sorting type, one of the following: - *

- *

- * Collator::SORT_REGULAR - * - compare items normally (don't change types) - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function sort( - array &$array, - int $flags = 0 - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array using specified collator and sort keys - * @link https://php.net/manual/en/collator.sortwithsortkeys.php - * @param string[] &$array

Array of strings to sort

- * @return bool TRUE on success or FALSE on failure. - */ - public function sortWithSortKeys( - array &$array, - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array maintaining index association - * @link https://php.net/manual/en/collator.asort.php - * @param string[] &$array

Array of strings to sort.

- * @param int $flags [optional]

- * Optional sorting type, one of the following: - * Collator::SORT_REGULAR - * - compare items normally (don't change types) - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function asort( - array &$array, - int $flags = 0 - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get collation attribute value - * @link https://php.net/manual/en/collator.getattribute.php - * @param int $attribute

- * Attribute to get value for. - *

- * @return int|false Attribute value, or boolean FALSE on error. - */ - public function getAttribute(int $attribute): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set collation attribute - * @link https://php.net/manual/en/collator.setattribute.php - * @param int $attribute

Attribute.

- * @param int $value

- * Attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setAttribute( - int $attribute, - int $value - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get current collation strength - * @link https://php.net/manual/en/collator.getstrength.php - * @return int current collation strength, or boolean FALSE on error. - */ - public function getStrength(): int {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set collation strength - * @link https://php.net/manual/en/collator.setstrength.php - * @param int $strength

Strength to set.

- *

- * Possible values are: - * Collator::PRIMARY - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setStrength(int $strength): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get collator's last error code - * @link https://php.net/manual/en/collator.geterrorcode.php - * @return int|false Error code returned by the last Collator API function call. - */ - public function getErrorCode(): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale name of the collator - * @link https://php.net/manual/en/collator.getlocale.php - * @param int $type

- * You can choose between valid and actual locale ( - * Locale::VALID_LOCALE and - * Locale::ACTUAL_LOCALE, - * respectively). - *

- * @return string|false Real locale name from which the collation data comes. If the collator was - * instantiated from rules or an error occurred, returns - * boolean FALSE. - */ - public function getLocale( - int $type - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get text for collator's last error code - * @link https://php.net/manual/en/collator.geterrormessage.php - * @return string|false Description of an error occurred in the last Collator API function call. - */ - public function getErrorMessage(): string|false {} - - /** - * (PHP 5 >= 5.3.2, PECL intl >= 1.0.3)
- * Get sorting key for a string - * @link https://php.net/manual/en/collator.getsortkey.php - * @param string $string

- * The string to produce the key from. - *

- * @return string|false the collation key for the string. Collation keys can be compared directly instead of strings. - */ - public function getSortKey( - string $string, - ): string|false {} -} - -class NumberFormatter -{ - public const CURRENCY_ACCOUNTING = 12; - - /** - * Decimal format defined by pattern - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PATTERN_DECIMAL = 0; - - /** - * Decimal format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DECIMAL = 1; - - /** - * Currency format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const CURRENCY = 2; - - /** - * Percent format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PERCENT = 3; - - /** - * Scientific format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SCIENTIFIC = 4; - - /** - * Spellout rule-based format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SPELLOUT = 5; - - /** - * Ordinal rule-based format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ORDINAL = 6; - - /** - * Duration rule-based format - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DURATION = 7; - - /** - * Rule-based format defined by pattern - * @link https://php.net/manual/en/class.locale.php#intl.locale-constants - */ - public const PATTERN_RULEBASED = 9; - - /** - * Alias for PATTERN_DECIMAL - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const IGNORE = 0; - - /** - * Default format for the locale - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DEFAULT_STYLE = 1; - - /** - * Rounding mode to round towards positive infinity. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_CEILING = 0; - - /** - * Rounding mode to round towards negative infinity. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_FLOOR = 1; - - /** - * Rounding mode to round towards zero. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_DOWN = 2; - - /** - * Rounding mode to round away from zero. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_UP = 3; - - /** - * Rounding mode to round towards the "nearest neighbor" unless both - * neighbors are equidistant, in which case, round towards the even - * neighbor. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_HALFEVEN = 4; - - /** - * Rounding mode to round towards "nearest neighbor" unless both neighbors - * are equidistant, in which case round down. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_HALFDOWN = 5; - - /** - * Rounding mode to round towards "nearest neighbor" unless both neighbors - * are equidistant, in which case round up. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUND_HALFUP = 6; - - /** - * Pad characters inserted before the prefix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PAD_BEFORE_PREFIX = 0; - - /** - * Pad characters inserted after the prefix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PAD_AFTER_PREFIX = 1; - - /** - * Pad characters inserted before the suffix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PAD_BEFORE_SUFFIX = 2; - - /** - * Pad characters inserted after the suffix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PAD_AFTER_SUFFIX = 3; - - /** - * Parse integers only. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PARSE_INT_ONLY = 0; - - /** - * Use grouping separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const GROUPING_USED = 1; - - /** - * Always show decimal point. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DECIMAL_ALWAYS_SHOWN = 2; - - /** - * Maximum integer digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MAX_INTEGER_DIGITS = 3; - - /** - * Minimum integer digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MIN_INTEGER_DIGITS = 4; - - /** - * Integer digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const INTEGER_DIGITS = 5; - - /** - * Maximum fraction digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MAX_FRACTION_DIGITS = 6; - - /** - * Minimum fraction digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MIN_FRACTION_DIGITS = 7; - - /** - * Fraction digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const FRACTION_DIGITS = 8; - - /** - * Multiplier. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MULTIPLIER = 9; - - /** - * Grouping size. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const GROUPING_SIZE = 10; - - /** - * Rounding Mode. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUNDING_MODE = 11; - - /** - * Rounding increment. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ROUNDING_INCREMENT = 12; - - /** - * The width to which the output of format() is padded. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const FORMAT_WIDTH = 13; - - /** - * The position at which padding will take place. See pad position - * constants for possible argument values. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PADDING_POSITION = 14; - - /** - * Secondary grouping size. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SECONDARY_GROUPING_SIZE = 15; - - /** - * Use significant digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SIGNIFICANT_DIGITS_USED = 16; - - /** - * Minimum significant digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MIN_SIGNIFICANT_DIGITS = 17; - - /** - * Maximum significant digits. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MAX_SIGNIFICANT_DIGITS = 18; - - /** - * Lenient parse mode used by rule-based formats. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const LENIENT_PARSE = 19; - - /** - * Positive prefix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const POSITIVE_PREFIX = 0; - - /** - * Positive suffix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const POSITIVE_SUFFIX = 1; - - /** - * Negative prefix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const NEGATIVE_PREFIX = 2; - - /** - * Negative suffix. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const NEGATIVE_SUFFIX = 3; - - /** - * The character used to pad to the format width. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PADDING_CHARACTER = 4; - - /** - * The ISO currency code. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const CURRENCY_CODE = 5; - - /** - * The default rule set. This is only available with rule-based - * formatters. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DEFAULT_RULESET = 6; - - /** - * The public rule sets. This is only available with rule-based - * formatters. This is a read-only attribute. The public rulesets are - * returned as a single string, with each ruleset name delimited by ';' - * (semicolon). - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PUBLIC_RULESETS = 7; - - /** - * The decimal separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DECIMAL_SEPARATOR_SYMBOL = 0; - - /** - * The grouping separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const GROUPING_SEPARATOR_SYMBOL = 1; - - /** - * The pattern separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PATTERN_SEPARATOR_SYMBOL = 2; - - /** - * The percent sign. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PERCENT_SYMBOL = 3; - - /** - * Zero. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ZERO_DIGIT_SYMBOL = 4; - - /** - * Character representing a digit in the pattern. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DIGIT_SYMBOL = 5; - - /** - * The minus sign. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MINUS_SIGN_SYMBOL = 6; - - /** - * The plus sign. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PLUS_SIGN_SYMBOL = 7; - - /** - * The currency symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const CURRENCY_SYMBOL = 8; - - /** - * The international currency symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const INTL_CURRENCY_SYMBOL = 9; - - /** - * The monetary separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MONETARY_SEPARATOR_SYMBOL = 10; - - /** - * The exponential symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const EXPONENTIAL_SYMBOL = 11; - - /** - * Per mill symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PERMILL_SYMBOL = 12; - - /** - * Escape padding character. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PAD_ESCAPE_SYMBOL = 13; - - /** - * Infinity symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const INFINITY_SYMBOL = 14; - - /** - * Not-a-number symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const NAN_SYMBOL = 15; - - /** - * Significant digit symbol. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SIGNIFICANT_DIGIT_SYMBOL = 16; - - /** - * The monetary grouping separator. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17; - - /** - * Derive the type from variable type - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const TYPE_DEFAULT = 0; - - /** - * Format/parse as 32-bit integer - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const TYPE_INT32 = 1; - - /** - * Format/parse as 64-bit integer - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const TYPE_INT64 = 2; - - /** - * Format/parse as floating point value - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const TYPE_DOUBLE = 3; - - /** - * Format/parse as currency value - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - * @deprecated 8.3 - */ - public const TYPE_CURRENCY = 4; - - /** - * @since 8.4 - */ - public const ROUND_TOWARD_ZERO = 2; - - /** - * @since 8.4 - */ - public const ROUND_AWAY_FROM_ZERO = 3; - - /** - * @since 8.4 - */ - public const ROUND_HALFODD = 8; - - /** - * @link https://www.php.net/manual/en/class.numberformatter.php - * @param string $locale - * @param int $style - * @param string $pattern [optional] - */ - public function __construct( - string $locale, - int $style, - string|null $pattern = null - ) {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a number formatter - * @link https://php.net/manual/en/numberformatter.create.php - * @param string $locale

- * Locale in which the number would be formatted (locale name, e.g. en_CA). - *

- * @param int $style

- * Style of the formatting, one of the - * format style constants. If - * NumberFormatter::PATTERN_DECIMAL - * or NumberFormatter::PATTERN_RULEBASED - * is passed then the number format is opened using the given pattern, - * which must conform to the syntax described in - * ICU DecimalFormat - * documentation or - * ICU RuleBasedNumberFormat - * documentation, respectively. - *

- * @param string $pattern [optional]

- * Pattern string if the chosen style requires a pattern. - *

- * @return NumberFormatter|false NumberFormatter object or FALSE on error. - */ - public static function create( - string $locale, - int $style, - string|null $pattern = null - ): ?NumberFormatter {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format a number - * @link https://php.net/manual/en/numberformatter.format.php - * @param int|float $num

- * The value to format. Can be integer or float, - * other values will be converted to a numeric value. - *

- * @param int $type [optional]

- * The - * formatting type to use. - *

- * @return string|false the string containing formatted value, or FALSE on error. - */ - public function format( - int|float $num, - int $type = 0 - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse a number - * @link https://php.net/manual/en/numberformatter.parse.php - * @param string $string - * @param int $type [optional]

- * The - * formatting type to use. By default, - * NumberFormatter::TYPE_DOUBLE is used. - *

- * @param int &$offset [optional]

- * Offset in the string at which to begin parsing. On return, this value - * will hold the offset at which parsing ended. - *

- * @return mixed The value of the parsed number or FALSE on error. - */ - public function parse( - string $string, - int $type = NumberFormatter::TYPE_DOUBLE, - &$offset = null - ): int|float|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format a currency value - * @link https://php.net/manual/en/numberformatter.formatcurrency.php - * @param float $amount

- * The numeric currency value. - *

- * @param string $currency

- * The 3-letter ISO 4217 currency code indicating the currency to use. - *

- * @return string|false String representing the formatted currency value. - */ - public function formatCurrency( - float $amount, - string $currency - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse a currency number - * @link https://php.net/manual/en/numberformatter.parsecurrency.php - * @param string $string - * @param string &$currency

- * Parameter to receive the currency name (3-letter ISO 4217 currency - * code). - *

- * @param int &$offset [optional]

- * Offset in the string at which to begin parsing. On return, this value - * will hold the offset at which parsing ended. - *

- * @return float|false The parsed numeric value or FALSE on error. - */ - public function parseCurrency(string $string, &$currency, &$offset = null): float|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set an attribute - * @link https://php.net/manual/en/numberformatter.setattribute.php - * @param int $attribute

- * Attribute specifier - one of the - * numeric attribute constants. - *

- * @param int $value

- * The attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setAttribute( - int $attribute, - int|float $value - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get an attribute - * @link https://php.net/manual/en/numberformatter.getattribute.php - * @param int $attribute

- * Attribute specifier - one of the - * numeric attribute constants. - *

- * @return int|float|false Return attribute value on success, or FALSE on error. - */ - public function getAttribute(int $attribute): int|float|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set a text attribute - * @link https://php.net/manual/en/numberformatter.settextattribute.php - * @param int $attribute

- * Attribute specifier - one of the - * text attribute - * constants. - *

- * @param string $value

- * Text for the attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setTextAttribute( - int $attribute, - string $value - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get a text attribute - * @link https://php.net/manual/en/numberformatter.gettextattribute.php - * @param int $attribute

- * Attribute specifier - one of the - * text attribute constants. - *

- * @return string|false Return attribute value on success, or FALSE on error. - */ - public function getTextAttribute(int $attribute): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set a symbol value - * @link https://php.net/manual/en/numberformatter.setsymbol.php - * @param int $symbol

- * Symbol specifier, one of the - * format symbol constants. - *

- * @param string $value

- * Text for the symbol. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setSymbol( - int $symbol, - string $value - ): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get a symbol value - * @link https://php.net/manual/en/numberformatter.getsymbol.php - * @param int $symbol

- * Symbol specifier, one of the - * format symbol constants. - *

- * @return string|false The symbol string or FALSE on error. - */ - public function getSymbol(int $symbol): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set formatter pattern - * @link https://php.net/manual/en/numberformatter.setpattern.php - * @param string $pattern

- * Pattern in syntax described in - * ICU DecimalFormat - * documentation. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setPattern(string $pattern): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter pattern - * @link https://php.net/manual/en/numberformatter.getpattern.php - * @return string|false Pattern string that is used by the formatter, or FALSE if an error happens. - */ - public function getPattern(): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter locale - * @link https://php.net/manual/en/numberformatter.getlocale.php - * @param int $type [optional]

- * You can choose between valid and actual locale ( - * Locale::VALID_LOCALE, - * Locale::ACTUAL_LOCALE, - * respectively). The default is the actual locale. - *

- * @return string|false The locale name used to create the formatter. - */ - public function getLocale( - int $type = 0 - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter's last error code. - * @link https://php.net/manual/en/numberformatter.geterrorcode.php - * @return int error code from last formatter call. - */ - public function getErrorCode(): int {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter's last error message. - * @link https://php.net/manual/en/numberformatter.geterrormessage.php - * @return string error message from last formatter call. - */ - public function getErrorMessage(): string {} -} - -class Normalizer -{ - public const NFKC_CF = 48; - public const FORM_KC_CF = 48; - - /** - * Default normalization options - * @link https://secure.php.net/manual/en/class.normalizer.php - */ - public const OPTION_DEFAULT = ""; - - /** - * Normalization Form D (NFD) - Canonical Decomposition - * @link https://secure.php.net/manual/en/class.normalizer.php - */ - public const FORM_D = 4; - public const NFD = 4; - - /** - * Normalization Form KD (NFKD) - Compatibility Decomposition - * @link https://secure.php.net/manual/en/class.normalizer.php - */ - public const FORM_KD = 8; - public const NFKD = 8; - - /** - * Normalization Form C (NFC) - Canonical Decomposition followed by - * Canonical Composition - * @link https://secure.php.net/manual/en/class.normalizer.php - */ - public const FORM_C = 16; - public const NFC = 16; - - /** - * Normalization Form KC (NFKC) - Compatibility Decomposition, followed by - * Canonical Composition - * @link https://secure.php.net/manual/en/class.normalizer.php - */ - public const FORM_KC = 32; - public const NFKC = 32; - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Normalizes the input provided and returns the normalized string - * @link https://php.net/manual/en/normalizer.normalize.php - * @param string $string

The input string to normalize

- * @param int $form

One of the normalization forms.

- * @return string|false The normalized string or FALSE if an error occurred. - */ - public static function normalize( - string $string, - int $form = Normalizer::FORM_C, - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Checks if the provided string is already in the specified normalization form. - * @link https://php.net/manual/en/normalizer.isnormalized.php - * @param string $string

The input string to normalize

- * @param int $form

- * One of the normalization forms. - *

- * @return bool TRUE if normalized, FALSE otherwise or if there an error - */ - public static function isNormalized( - string $string, - int $form = Normalizer::FORM_C, - ): bool {} - - /** - * @param string $string

The input string to normalize

- * @param int $form - * @return string|null

Returns a string containing the Decomposition_Mapping property, if present in the UCD. - * Returns null if there is no Decomposition_Mapping property for the character.

- * @link https://www.php.net/manual/en/normalizer.getrawdecomposition.php - * @since 7.3 - */ - public static function getRawDecomposition( - string $string, - int $form = 16 - ): ?string {} -} - -class Locale -{ - /** - * This is locale the data actually comes from. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const ACTUAL_LOCALE = 0; - - /** - * This is the most specific locale supported by ICU. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const VALID_LOCALE = 1; - - /** - * Used as locale parameter with the methods of the various locale affected classes, - * such as NumberFormatter. This constant would make the methods to use default - * locale. - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const DEFAULT_LOCALE = null; - - /** - * Language subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const LANG_TAG = "language"; - - /** - * Extended language subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const EXTLANG_TAG = "extlang"; - - /** - * Script subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const SCRIPT_TAG = "script"; - - /** - * Region subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const REGION_TAG = "region"; - - /** - * Variant subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const VARIANT_TAG = "variant"; - - /** - * Grandfathered Language subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const GRANDFATHERED_LANG_TAG = "grandfathered"; - - /** - * Private subtag - * @link https://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants - */ - public const PRIVATE_TAG = "private"; - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the default locale value from the INTL global 'default_locale' - * @link https://php.net/manual/en/locale.getdefault.php - * @return string The current runtime locale - */ - public static function getDefault(): string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * sets the default runtime locale - * @link https://php.net/manual/en/locale.setdefault.php - * @param string $locale

- * Is a BCP 47 compliant language tag containing the - *

- * @return bool TRUE on success or FALSE on failure. - */ - public static function setDefault(string $locale): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the primary language for the input locale - * @link https://php.net/manual/en/locale.getprimarylanguage.php - * @param string $locale

- * The locale to extract the primary language code from - *

- * @return string|null The language code associated with the language or NULL in case of error. - */ - public static function getPrimaryLanguage(string $locale): ?string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the script for the input locale - * @link https://php.net/manual/en/locale.getscript.php - * @param string $locale

- * The locale to extract the script code from - *

- * @return string|null The script subtag for the locale or NULL if not present - */ - public static function getScript(string $locale): ?string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the region for the input locale - * @link https://php.net/manual/en/locale.getregion.php - * @param string $locale

- * The locale to extract the region code from - *

- * @return string|null The region subtag for the locale or NULL if not present - */ - public static function getRegion(string $locale): ?string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the keywords for the input locale - * @link https://php.net/manual/en/locale.getkeywords.php - * @param string $locale

- * The locale to extract the keywords from - *

- * @return array|false|null Associative array containing the keyword-value pairs for this locale - */ - public static function getKeywords(string $locale): array|false|null {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for script of the input locale - * @link https://php.net/manual/en/locale.getdisplayscript.php - * @param string $locale

- * The locale to return a display script for - *

- * @param string $displayLocale

- * Optional format locale to use to display the script name - *

- * @return string|false Display name of the script for the $locale in the format appropriate for - * $in_locale. - */ - public static function getDisplayScript( - string $locale, - string|null $displayLocale = null - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for region of the input locale - * @link https://php.net/manual/en/locale.getdisplayregion.php - * @param string $locale

- * The locale to return a display region for. - *

- * @param string $displayLocale

- * Optional format locale to use to display the region name - *

- * @return string|false display name of the region for the $locale in the format appropriate for - * $in_locale. - */ - public static function getDisplayRegion( - string $locale, - string|null $displayLocale = null - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for the input locale - * @link https://php.net/manual/en/locale.getdisplayname.php - * @param string $locale

- * The locale to return a display name for. - *

- * @param string $displayLocale

optional format locale

- * @return string|false Display name of the locale in the format appropriate for $in_locale. - */ - public static function getDisplayName( - string $locale, - string|null $displayLocale = null - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for language of the inputlocale - * @link https://php.net/manual/en/locale.getdisplaylanguage.php - * @param string $locale

- * The locale to return a display language for - *

- * @param string $displayLocale

- * Optional format locale to use to display the language name - *

- * @return string|false display name of the language for the $locale in the format appropriate for - * $in_locale. - */ - public static function getDisplayLanguage( - string $locale, - string|null $displayLocale = null - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for variants of the input locale - * @link https://php.net/manual/en/locale.getdisplayvariant.php - * @param string $locale

- * The locale to return a display variant for - *

- * @param string $displayLocale

- * Optional format locale to use to display the variant name - *

- * @return string|false Display name of the variant for the $locale in the format appropriate for - * $in_locale. - */ - public static function getDisplayVariant( - string $locale, - string|null $displayLocale = null - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns a correctly ordered and delimited locale ID - * @link https://php.net/manual/en/locale.composelocale.php - * @param array $subtags

- * an array containing a list of key-value pairs, where the keys identify - * the particular locale ID subtags, and the values are the associated - * subtag values. - *

- * The 'variant' and 'private' subtags can take maximum 15 values - * whereas 'extlang' can take maximum 3 values.e.g. Variants are allowed - * with the suffix ranging from 0-14. Hence the keys for the input array - * can be variant0, variant1, ...,variant14. In the returned locale id, - * the subtag is ordered by suffix resulting in variant0 followed by - * variant1 followed by variant2 and so on. - *

- *

- * The 'variant', 'private' and 'extlang' multiple values can be specified both - * as array under specific key (e.g. 'variant') and as multiple numbered keys - * (e.g. 'variant0', 'variant1', etc.). - *

- *

- * @return string|false The corresponding locale identifier. - */ - public static function composeLocale(array $subtags): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns a key-value array of locale ID subtag elements. - * @link https://php.net/manual/en/locale.parselocale.php - * @param string $locale

- * The locale to extract the subtag array from. Note: The 'variant' and - * 'private' subtags can take maximum 15 values whereas 'extlang' can take - * maximum 3 values. - *

- * @return array|null an array containing a list of key-value pairs, where the keys - * identify the particular locale ID subtags, and the values are the - * associated subtag values. The array will be ordered as the locale id - * subtags e.g. in the locale id if variants are '-varX-varY-varZ' then the - * returned array will have variant0=>varX , variant1=>varY , - * variant2=>varZ - */ - public static function parseLocale(string $locale): ?array {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the variants for the input locale - * @link https://php.net/manual/en/locale.getallvariants.php - * @param string $locale

- * The locale to extract the variants from - *

- * @return array|null The array containing the list of all variants subtag for the locale - * or NULL if not present - */ - public static function getAllVariants(string $locale): ?array {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Checks if a language tag filter matches with locale - * @link https://php.net/manual/en/locale.filtermatches.php - * @param string $languageTag

- * The language tag to check - *

- * @param string $locale

- * The language range to check against - *

- * @param bool $canonicalize

- * If true, the arguments will be converted to canonical form before - * matching. - *

- * @return bool|null TRUE if $locale matches $langtag FALSE otherwise. - */ - public static function filterMatches( - string $languageTag, - string $locale, - bool $canonicalize = false - ): ?bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Searches the language tag list for the best match to the language - * @link https://php.net/manual/en/locale.lookup.php - * @param array $languageTag

- * An array containing a list of language tags to compare to - * locale. Maximum 100 items allowed. - *

- * @param string $locale

- * The locale to use as the language range when matching. - *

- * @param bool $canonicalize

- * If true, the arguments will be converted to canonical form before - * matching. - *

- * @param string $defaultLocale

- * The locale to use if no match is found. - *

- * @return string|null The closest matching language tag or default value. - */ - public static function lookup( - array $languageTag, - string $locale, - bool $canonicalize = false, - string|null $defaultLocale = null - ): ?string {} - - /** - * @link https://php.net/manual/en/locale.canonicalize.php - * @param string $locale - * @return string|null - */ - public static function canonicalize(string $locale): ?string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Tries to find out best available locale based on HTTP "Accept-Language" header - * @link https://php.net/manual/en/locale.acceptfromhttp.php - * @param string $header

- * The string containing the "Accept-Language" header according to format in RFC 2616. - *

- * @return string|false The corresponding locale identifier. - */ - public static function acceptFromHttp(string $header): string|false {} -} - -class MessageFormatter -{ - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Constructs a new Message Formatter - * @link https://php.net/manual/en/messageformatter.create.php - * @param string $locale

- * The locale to use when formatting arguments - *

- * @param string $pattern

- * The pattern string to stick arguments into. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @throws IntlException on failure. - */ - public function __construct( - string $locale, - string $pattern - ) {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Constructs a new Message Formatter - * @link https://php.net/manual/en/messageformatter.create.php - * @param string $locale

- * The locale to use when formatting arguments - *

- * @param string $pattern

- * The pattern string to stick arguments into. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @return MessageFormatter|null The formatter object - */ - public static function create( - string $locale, - string $pattern - ): ?MessageFormatter {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format the message - * @link https://php.net/manual/en/messageformatter.format.php - * @param array $values

- * Arguments to insert into the format string - *

- * @return string|false The formatted string, or FALSE if an error occurred - */ - public function format(array $values): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Quick format message - * @link https://php.net/manual/en/messageformatter.formatmessage.php - * @param string $locale

- * The locale to use for formatting locale-dependent parts - *

- * @param string $pattern

- * The pattern string to insert things into. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @param array $values

- * The array of values to insert into the format string - *

- * @return string|false The formatted pattern string or FALSE if an error occurred - */ - public static function formatMessage( - string $locale, - string $pattern, - array $values - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse input string according to pattern - * @link https://php.net/manual/en/messageformatter.parse.php - * @param string $string

- * The string to parse - *

- * @return array|false An array containing the items extracted, or FALSE on error - */ - public function parse(string $string): array|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Quick parse input string - * @link https://php.net/manual/en/messageformatter.parsemessage.php - * @param string $locale

- * The locale to use for parsing locale-dependent parts - *

- * @param string $pattern

- * The pattern with which to parse the value. - *

- * @param string $message

- * The string to parse, conforming to the pattern. - *

- * @return array|false An array containing items extracted, or FALSE on error - */ - public static function parseMessage( - string $locale, - string $pattern, - string $message - ): array|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the pattern used by the formatter - * @link https://php.net/manual/en/messageformatter.setpattern.php - * @param string $pattern

- * The pattern string to use in this message formatter. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setPattern(string $pattern): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the pattern used by the formatter - * @link https://php.net/manual/en/messageformatter.getpattern.php - * @return string|false The pattern string for this message formatter - */ - public function getPattern(): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale for which the formatter was created. - * @link https://php.net/manual/en/messageformatter.getlocale.php - * @return string The locale name - */ - public function getLocale(): string {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error code from last operation - * @link https://php.net/manual/en/messageformatter.geterrorcode.php - * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR. - */ - public function getErrorCode(): int {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error text from the last operation - * @link https://php.net/manual/en/messageformatter.geterrormessage.php - * @return string Description of the last error. - */ - public function getErrorMessage(): string {} -} - -class IntlDateFormatter -{ - /** - * Completely specified style (Tuesday, April 12, 1952 AD or 3:30:42pm PST) - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const FULL = 0; - - /** - * Long style (January 12, 1952 or 3:30:32pm) - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const LONG = 1; - - /** - * Medium style (Jan 12, 1952) - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const MEDIUM = 2; - - /** - * Most abbreviated style, only essential data (12/13/52 or 3:30pm) - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const SHORT = 3; - - /** - * Do not include this element - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const NONE = -1; - - /** - * Gregorian Calendar - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const GREGORIAN = 1; - - /** - * Non-Gregorian Calendar - * @link https://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants - */ - public const TRADITIONAL = 0; - public const RELATIVE_FULL = 128; - public const RELATIVE_LONG = 129; - public const RELATIVE_MEDIUM = 130; - public const RELATIVE_SHORT = 131; - public const PATTERN = -2; - - /** - * @param string|null $locale - * @param int $dateType - * @param int $timeType - * @param mixed|null $timezone [optional] - * @param mixed|null $calendar [optional] - * @param string $pattern [optional] - */ - public function __construct( - string|null $locale, - int $dateType = 0, - int $timeType = 0, - $timezone = null, - $calendar = null, - string|null $pattern = null - ) {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a date formatter - * @link https://php.net/manual/en/intldateformatter.create.php - * @param string $locale

- * Locale to use when formatting or parsing; default is specified in the ini setting intl.default_locale. - *

- * @param int $dateType

- * Date type to use (none, - * short, medium, - * long, full). - * This is one of the - * IntlDateFormatter constants. - *

- * @param int $timeType

- * Time type to use (none, - * short, medium, - * long, full). - * This is one of the - * IntlDateFormatter constants. - *

- * @param string $timezone [optional]

- * Time zone ID, default is system default. - *

- * @param int $calendar [optional]

- * Calendar to use for formatting or parsing; default is Gregorian. - * This is one of the - * IntlDateFormatter calendar constants. - *

- * @param string $pattern [optional]

- * Optional pattern to use when formatting or parsing. - * Possible patterns are documented at http://userguide.icu-project.org/formatparse/datetime. - *

- * @return IntlDateFormatter|null - */ - public static function create( - string|null $locale, - int $dateType = 0, - int $timeType = 0, - $timezone = null, - IntlCalendar|int|null $calendar = null, - string|null $pattern = null - ): ?IntlDateFormatter {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the datetype used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getdatetype.php - * @return int|false The current date type value of the formatter. - */ - public function getDateType(): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the timetype used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.gettimetype.php - * @return int|false The current date type value of the formatter. - */ - public function getTimeType(): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the calendar used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getcalendar.php - * @return int|false The calendar being used by the formatter. - */ - public function getCalendar(): int|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * sets the calendar used to the appropriate calendar, which must be - * @link https://php.net/manual/en/intldateformatter.setcalendar.php - * @param int $calendar

- * The calendar to use. - * Default is IntlDateFormatter::GREGORIAN. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setCalendar(IntlCalendar|int|null $calendar): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the timezone-id used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.gettimezoneid.php - * @return string|false ID string for the time zone used by this formatter. - */ - public function getTimeZoneId(): string|false {} - - /** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Get copy of formatter's calendar object - * @link https://secure.php.net/manual/en/intldateformatter.getcalendarobject.php - * @return IntlCalendar|false|null A copy of the internal calendar object used by this formatter. - */ - public function getCalendarObject(): IntlCalendar|false|null {} - - /** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Get formatter's timezone - * @link https://secure.php.net/manual/en/intldateformatter.gettimezone.php - * @return IntlTimeZone|false The associated IntlTimeZone object or FALSE on failure. - */ - public function getTimeZone(): IntlTimeZone|false {} - - /** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Sets formatter's timezone - * @link https://php.net/manual/en/intldateformatter.settimezone.php - * @param mixed $timezone

- * The timezone to use for this formatter. This can be specified in the - * following forms: - *

    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link "https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone" date.timezone} or - * through the function {@link "https://secure.php.net/manual/en/function.date-default-timezone-set.php" date_default_timezone_set()} and as - * returned by {@link "https://secure.php.net/manual/en/function.date-default-timezone-get.php" date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link "https://secure.php.net/manual/en/class.intltimezone.php" IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link "https://secure.php.net/manual/en/class.datetimezone.php" DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link "https://secure.php.net/manual/en/language.types.string.php" string}, which should be a valid ICU timezone identifier. - * See IntlTimeZone::createTimeZoneIDEnumeration(). Raw offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- *

- * @return bool|null TRUE on success (since 8.3 - previously null) or FALSE on failure. - */ - public function setTimeZone($timezone): ?bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the pattern used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.setpattern.php - * @param string $pattern

- * New pattern string to use. - * Possible patterns are documented at http://userguide.icu-project.org/formatparse/datetime. - *

- * @return bool TRUE on success or FALSE on failure. - * Bad formatstrings are usually the cause of the failure. - */ - public function setPattern(string $pattern): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the pattern used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getpattern.php - * @return string|false The pattern string being used to format/parse. - */ - public function getPattern(): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale used by formatter - * @link https://php.net/manual/en/intldateformatter.getlocale.php - * @param int $type [optional] - * @return string|false the locale of this formatter or 'false' if error - */ - public function getLocale( - int $type = 0 - ): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the leniency of the parser - * @link https://php.net/manual/en/intldateformatter.setlenient.php - * @param bool $lenient

- * Sets whether the parser is lenient or not, default is TRUE (lenient). - *

- * @return void - */ - public function setLenient(bool $lenient): void {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the lenient used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.islenient.php - * @return bool TRUE if parser is lenient, FALSE if parser is strict. By default the parser is lenient. - */ - public function isLenient(): bool {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format the date/time value as a string - * @link https://php.net/manual/en/intldateformatter.format.php - * @param mixed $datetime

- * Value to format. This may be a DateTime object, - * an integer representing a Unix timestamp value (seconds - * since epoch, UTC) or an array in the format output by - * localtime. - *

- * @return string|false The formatted string or, if an error occurred, FALSE. - */ - public function format( - $datetime, - ): string|false {} - - /** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Formats an object - * @link https://secure.php.net/manual/en/intldateformatter.formatobject.php - * @param object $datetime

- * An object of type {@link "https://secure.php.net/manual/en/class.intlcalendar.php" IntlCalendar} or {@link "https://secure.php.net/manual/en/class.datetime.php" DateTime}. The timezone information in the object will be used. - *

- * @param mixed $format [optional]

- * How to format the date/time. This can either be an {@link "https://secure.php.net/manual/en/language.types.array.php" array} with - * two elements (first the date style, then the time style, these being one - * of the constants IntlDateFormatter::NONE, - * IntlDateFormatter::SHORT, - * IntlDateFormatter::MEDIUM, - * IntlDateFormatter::LONG, - * IntlDateFormatter::FULL), a long with - * the value of one of these constants (in which case it will be used both - * for the time and the date) or a {@link "https://secure.php.net/manual/en/language.types.string.php" string} with the format - * described in {@link "http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details" the ICU documentation}. - * If NULL, the default style will be used. - *

- * @param string|null $locale [optional]

- * The locale to use, or NULL to use the {@link "https://secure.php.net/manual/en/intl.configuration.php#ini.intl.default-locale" default one}.

- * @return string|false A string with result or FALSE on failure. - */ - public static function formatObject($datetime, $format = null, string|null $locale = null): string|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse string to a timestamp value - * @link https://php.net/manual/en/intldateformatter.parse.php - * @param string $string

- * string to convert to a time - *

- * @param int &$offset [optional]

- * Position at which to start the parsing in $value (zero-based). - * If no error occurs before $value is consumed, $parse_pos will contain -1 - * otherwise it will contain the position at which parsing ended (and the error occurred). - * This variable will contain the end position if the parse fails. - * If $parse_pos > strlen($value), the parse fails immediately. - *

- * @return int|float|false timestamp parsed value - */ - public function parse(string $string, &$offset = null): int|float|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse string to a field-based time value - * @link https://php.net/manual/en/intldateformatter.localtime.php - * @param string $string

- * string to convert to a time - *

- * @param int &$offset [optional]

- * Position at which to start the parsing in $value (zero-based). - * If no error occurs before $value is consumed, $parse_pos will contain -1 - * otherwise it will contain the position at which parsing ended . - * If $parse_pos > strlen($value), the parse fails immediately. - *

- * @return array|false Localtime compatible array of integers : contains 24 hour clock value in tm_hour field - */ - public function localtime(string $string, &$offset = null): array|false {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error code from last operation - * @link https://php.net/manual/en/intldateformatter.geterrorcode.php - * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR. - */ - public function getErrorCode(): int {} - - /** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error text from the last operation. - * @link https://php.net/manual/en/intldateformatter.geterrormessage.php - * @return string Description of the last error. - */ - public function getErrorMessage(): string {} - - /** - * @since 8.4 - */ - public function parseToCalendar(string $string, &$offset = null): int|float|false {} -} - -class ResourceBundle implements IteratorAggregate, Countable -{ - /** - * @link https://www.php.net/manual/en/resourcebundle.create.php - * @param string $locale

Locale for which the resources should be loaded (locale name, e.g. en_CA).

- * @param string $bundle

The directory where the data is stored or the name of the .dat file.

- * @param bool $fallback [optional]

Whether locale should match exactly or fallback to parent locale is allowed.

- */ - public function __construct( - string|null $locale, - string|null $bundle, - bool $fallback = true - ) {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Create a resource bundle - * @link https://php.net/manual/en/resourcebundle.create.php - * @param string $locale

- * Locale for which the resources should be loaded (locale name, e.g. en_CA). - *

- * @param string $bundle

- * The directory where the data is stored or the name of the .dat file. - *

- * @param bool $fallback [optional]

- * Whether locale should match exactly or fallback to parent locale is allowed. - *

- * @return ResourceBundle|null ResourceBundle object or null on error. - */ - public static function create( - string|null $locale, - string|null $bundle, - bool $fallback = true - ): ?ResourceBundle {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get data from the bundle - * @link https://php.net/manual/en/resourcebundle.get.php - * @param string|int $index

- * Data index, must be string or integer. - *

- * @param bool $fallback - * @return mixed the data located at the index or NULL on error. Strings, integers and binary data strings - * are returned as corresponding PHP types, integer array is returned as PHP array. Complex types are - * returned as ResourceBundle object. - */ - public function get($index, bool $fallback = true): mixed {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get number of elements in the bundle - * @link https://php.net/manual/en/resourcebundle.count.php - * @return int<0,max> number of elements in the bundle. - */ - public function count(): int {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get supported locales - * @link https://php.net/manual/en/resourcebundle.locales.php - * @param string $bundle

- * Path of ResourceBundle for which to get available locales, or - * empty string for default locales list. - *

- * @return array|false the list of locales supported by the bundle. - */ - public static function getLocales(string $bundle): array|false {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get bundle's last error code. - * @link https://php.net/manual/en/resourcebundle.geterrorcode.php - * @return int error code from last bundle object call. - */ - public function getErrorCode(): int {} - - /** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get bundle's last error message. - * @link https://php.net/manual/en/resourcebundle.geterrormessage.php - * @return string error message from last bundle object's call. - */ - public function getErrorMessage(): string {} - - /** - * @return Iterator - * @since 8.0 - */ - public function getIterator(): Iterator {} -} - -/** - * @since 5.4 - */ -class Transliterator -{ - public const FORWARD = 0; - public const REVERSE = 1; - - /** - * Starting 8.2 $id is readonly to unlock subclassing it - */ - public string $id; - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Private constructor to deny instantiation - * @link https://php.net/manual/en/transliterator.construct.php - */ - final private function __construct() {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create a transliterator - * @link https://php.net/manual/en/transliterator.create.php - * @param string $id

- * The id. - *

- * @param int $direction [optional]

- * The direction, defaults to - * Transliterator::FORWARD. - * May also be set to - * Transliterator::REVERSE. - *

- * @return Transliterator|null a Transliterator object on success, - * or NULL on failure. - */ - public static function create( - string $id, - int $direction = 0 - ): ?Transliterator {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create transliterator from rules - * @link https://php.net/manual/en/transliterator.createfromrules.php - * @param string $rules

- * The rules. - *

- * @param int $direction [optional]

- * The direction, defaults to - * {@see Transliterator::FORWARD}. - * May also be set to - * {@see Transliterator::REVERSE}. - *

- * @return Transliterator|null a Transliterator object on success, - * or NULL on failure. - */ - public static function createFromRules( - string $rules, - int $direction = 0 - ): ?Transliterator {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create an inverse transliterator - * @link https://php.net/manual/en/transliterator.createinverse.php - * @return Transliterator|null a Transliterator object on success, - * or NULL on failure - */ - public function createInverse(): ?Transliterator {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get transliterator IDs - * @link https://php.net/manual/en/transliterator.listids.php - * @return array|false An array of registered transliterator IDs on success, - * or FALSE on failure. - */ - public static function listIDs(): array|false {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Transliterate a string - * @link https://php.net/manual/en/transliterator.transliterate.php - * @param string $string

- * The string to be transformed. - *

- * @param int $start [optional]

- * The start index (in UTF-16 code units) from which the string will start - * to be transformed, inclusive. Indexing starts at 0. The text before will - * be left as is. - *

- * @param int $end [optional]

- * The end index (in UTF-16 code units) until which the string will be - * transformed, exclusive. Indexing starts at 0. The text after will be - * left as is. - *

- * @return string|false The transfomed string on success, or FALSE on failure. - */ - public function transliterate( - string $string, - int $start = 0, - int $end = -1 - ): string|false {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get last error code - * @link https://php.net/manual/en/transliterator.geterrorcode.php - * @return int|false The error code on success, - * or FALSE if none exists, or on failure. - */ - public function getErrorCode(): int|false {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get last error message - * @link https://php.net/manual/en/transliterator.geterrormessage.php - * @return string|false The error code on success, - * or FALSE if none exists, or on failure. - */ - public function getErrorMessage(): string|false {} -} - -/** - * @link https://php.net/manual/en/class.spoofchecker.php - */ -class Spoofchecker -{ - public const SINGLE_SCRIPT_CONFUSABLE = 1; - public const MIXED_SCRIPT_CONFUSABLE = 2; - public const WHOLE_SCRIPT_CONFUSABLE = 4; - public const ANY_CASE = 8; - public const SINGLE_SCRIPT = 16; - public const INVISIBLE = 32; - public const CHAR_LIMIT = 64; - public const ASCII = 268435456; - public const HIGHLY_RESTRICTIVE = 805306368; - public const MODERATELY_RESTRICTIVE = 1073741824; - public const MINIMALLY_RESTRICTIVE = 1342177280; - public const UNRESTRICTIVE = 1610612736; - public const SINGLE_SCRIPT_RESTRICTIVE = 536870912; - public const MIXED_NUMBERS = 1; - public const HIDDEN_OVERLAY = 2; - - /** - * @since 8.4 - */ - public const IGNORE_SPACE = 1; - - /** - * @since 8.4 - */ - public const CASE_INSENSITIVE = 2; - - /** - * @since 8.4 - */ - public const ADD_CASE_MAPPINGS = 4; - - /** - * @since 8.4 - */ - public const SIMPLE_CASE_INSENSITIVE = 6; - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Constructor - * @link https://php.net/manual/en/spoofchecker.construct.php - */ - public function __construct() {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Checks if a given text contains any suspicious characters - * @link https://php.net/manual/en/spoofchecker.issuspicious.php - * @param string $string

- *

- * @param string &$errorCode [optional]

- *

- * @return bool - */ - public function isSuspicious(string $string, &$errorCode = null): bool {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Checks if a given text contains any confusable characters - * @link https://php.net/manual/en/spoofchecker.areconfusable.php - * @param string $string1

- *

- * @param string $string2

- *

- * @param int &$errorCode [optional]

- *

- * @return bool - */ - public function areConfusable( - string $string1, - string $string2, - &$errorCode = null - ): bool {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Locales to use when running checks - * @link https://php.net/manual/en/spoofchecker.setallowedlocales.php - * @param string $locales

- *

- * @return void - */ - public function setAllowedLocales(string $locales): void {} - - /** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Set the checks to run - * @link https://php.net/manual/en/spoofchecker.setchecks.php - * @param int $checks

- *

- * @return void - */ - public function setChecks(int $checks): void {} - - /** - * @param int $level - */ - public function setRestrictionLevel(int $level): void {} - - /** - * @since 8.4 - */ - public function setAllowedChars(string $pattern, int $patternOptions = 0): void {} -} - -/** - * @since 5.5 - */ -class IntlGregorianCalendar extends IntlCalendar -{ - /** - * @link https://www.php.net/manual/en/intlgregoriancalendar.construct - * @param int $timezoneOrYear [optional] - * @param int $localeOrMonth [optional] - * @param int $day [optional] - * @param int $hour [optional] - * @param int $minute [optional] - * @param int $second [optional] - */ - public function __construct($timezoneOrYear, $localeOrMonth, $day, $hour, $minute, $second) {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @param mixed $timeZone - * @param string $locale - * @return IntlGregorianCalendar - */ - public static function createInstance($timeZone = null, $locale = null) {} - - /** - * @param float $timestamp - */ - public function setGregorianChange(float $timestamp): bool {} - - /** - * @return float - */ - public function getGregorianChange(): float {} - - /** - * @param int $year - * @return bool - */ - public function isLeapYear(int $year): bool {} - - /** - * @since 8.3 - */ - public static function createFromDate(int $year, int $month, int $dayOfMonth): static {} - - /** - * @since 8.3 - */ - public static function createFromDateTime(int $year, int $month, int $dayOfMonth, int $hour, int $minute, ?int $second = null): static {} -} - -/** - * @since 5.5 - */ -class IntlCalendar -{ - /* Constants */ - public const FIELD_ERA = 0; - public const FIELD_YEAR = 1; - public const FIELD_MONTH = 2; - public const FIELD_WEEK_OF_YEAR = 3; - public const FIELD_WEEK_OF_MONTH = 4; - public const FIELD_DATE = 5; - public const FIELD_DAY_OF_YEAR = 6; - public const FIELD_DAY_OF_WEEK = 7; - public const FIELD_DAY_OF_WEEK_IN_MONTH = 8; - public const FIELD_AM_PM = 9; - public const FIELD_HOUR = 10; - public const FIELD_HOUR_OF_DAY = 11; - public const FIELD_MINUTE = 12; - public const FIELD_SECOND = 13; - public const FIELD_MILLISECOND = 14; - public const FIELD_ZONE_OFFSET = 15; - public const FIELD_DST_OFFSET = 16; - public const FIELD_YEAR_WOY = 17; - public const FIELD_DOW_LOCAL = 18; - public const FIELD_EXTENDED_YEAR = 19; - public const FIELD_JULIAN_DAY = 20; - public const FIELD_MILLISECONDS_IN_DAY = 21; - public const FIELD_IS_LEAP_MONTH = 22; - public const FIELD_FIELD_COUNT = 23; - public const FIELD_DAY_OF_MONTH = 5; - public const DOW_SUNDAY = 1; - public const DOW_MONDAY = 2; - public const DOW_TUESDAY = 3; - public const DOW_WEDNESDAY = 4; - public const DOW_THURSDAY = 5; - public const DOW_FRIDAY = 6; - public const DOW_SATURDAY = 7; - public const DOW_TYPE_WEEKDAY = 0; - public const DOW_TYPE_WEEKEND = 1; - public const DOW_TYPE_WEEKEND_OFFSET = 2; - public const DOW_TYPE_WEEKEND_CEASE = 3; - public const WALLTIME_FIRST = 1; - public const WALLTIME_LAST = 0; - public const WALLTIME_NEXT_VALID = 2; - - /* Methods */ - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Add a (signed) amount of time to a field - * @link https://secure.php.net/manual/en/intlcalendar.add.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param int $value

The signed amount to add to the current field. If the amount is positive, the instant will be moved forward; if it is negative, the instant wil be moved into the past. The unit is implicit to the field type. - * For instance, hours for IntlCalendar::FIELD_HOUR_OF_DAY.

- * @return bool Returns TRUE on success or FALSE on failure. - */ - public function add( - int $field, - int $value - ): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether this object's time is after that of the passed object - * https://secure.php.net/manual/en/intlcalendar.after.php - * @param IntlCalendar $other

The calendar whose time will be checked against this object's time.

- * @return bool - * Returns TRUE if this object's current time is after that of the - * calendar argument's time. Returns FALSE otherwise. - * Also returns FALSE on failure. You can use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to detect error conditions. - */ - public function after(IntlCalendar $other): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether this object's time is before that of the passed object - * @link https://secure.php.net/manual/en/intlcalendar.before.php - * @param IntlCalendar $other

The calendar whose time will be checked against this object's time.

- * @return bool - * Returns TRUE if this object's current time is before that of the - * calendar argument's time. Returns FALSE otherwise. - * Also returns FALSE on failure. You can use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to detect error conditions. - */ - public function before(IntlCalendar $other): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Clear a field or all fields - * @link https://secure.php.net/manual/en/intlcalendar.clear.php - * @param int $field [optional]

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return bool Returns TRUE on success or FALSE on failure. Failure can only occur is invalid arguments are provided. - */ - public function clear(int|null $field = null): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Private constructor for disallowing instantiation - * @link https://secure.php.net/manual/en/intlcalendar.construct.php - */ - private function __construct() {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a new IntlCalendar - * @link https://secure.php.net/manual/en/intlcalendar.createinstance.php - * @param mixed $timezone [optional]

- * The timezone to use. - *

- * - *
    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone date.timezone} or - * through the function {@link https://secure.php.net/manual/en/function.date-default-timezone-set.php date_default_timezone_set()} and as - * returned by {@link https://secure.php.net/manual/en/function.date-default-timezone-get.php date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/class.datetimezone.php DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/language.types.string.php string}, which should be a valid ICU timezone identifier. - * See IntlTimeZone::createTimeZoneIDEnumeration(). Raw - * offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- *

- * @param string|null $locale [optional]

- * A locale to use or NULL to use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.default-locale the default locale}. - *

- * @return IntlCalendar|null - * The created {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} instance or NULL on - * failure. - */ - public static function createInstance($timezone = null, string|null $locale = null): ?IntlCalendar {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Compare time of two IntlCalendar objects for equality - * @link https://secure.php.net/manual/en/intlcalendar.equals.php - * @param IntlCalendar $other - * @return bool

- * Returns TRUE if the current time of both this and the passed in - * {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} object are the same, or FALSE - * otherwise. The value FALSE can also be returned on failure. This can only - * happen if bad arguments are passed in. In any case, the two cases can be - * distinguished by calling {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()}. - *

- */ - public function equals(IntlCalendar $other): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Calculate difference between given time and this object's time - * @link https://secure.php.net/manual/en/intlcalendar.fielddifference.php - * @param float $timestamp

- * The time against which to compare the quantity represented by the - * field. For the result to be positive, the time - * given for this parameter must be ahead of the time of the object the - * method is being invoked on. - *

- * @param int $field

- * The field that represents the quantity being compared. - *

- * - *

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false Returns a (signed) difference of time in the unit associated with the - * specified field or FALSE on failure. - */ - public function fieldDifference( - float $timestamp, - int $field - ): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a2)
- * Create an IntlCalendar from a DateTime object or string - * @link https://secure.php.net/manual/en/intlcalendar.fromdatetime.php - * @param mixed $datetime

- * A {@link https://secure.php.net/manual/en/class.datetime.php DateTime} object or a {@link https://secure.php.net/manual/en/language.types.string.php string} that - * can be passed to {@link https://secure.php.net/manual/en/datetime.construct.php DateTime::__construct()}. - *

- * @param $locale [optional] - * @return IntlCalendar|null - * The created {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} object or NULL in case of - * failure. If a {@link https://secure.php.net/manual/en/language.types.string.php string} is passed, any exception that occurs - * inside the {@link https://secure.php.net/manual/en/class.datetime.php DateTime} constructor is propagated. - */ - public static function fromDateTime( - DateTime|string $datetime, - string|null $locale - ): ?IntlCalendar {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the value for a field - * @link https://secure.php.net/manual/en/intlcalendar.get.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false An integer with the value of the time field. - */ - public function get(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * The maximum value for a field, considering the object's current time - * @link https://secure.php.net/manual/en/intlcalendar.getactualmaximum.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing the maximum value in the units associated - * with the given field or FALSE on failure. - */ - public function getActualMaximum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * The minimum value for a field, considering the object's current time - * @link https://secure.php.net/manual/en/intlcalendar.getactualminimum.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing the minimum value in the field's - * unit or FALSE on failure. - */ - public function getActualMinimum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get array of locales for which there is data - * @link https://secure.php.net/manual/en/intlcalendar.getavailablelocales.php - * @return string[] An array of strings, one for which locale. - */ - public static function getAvailableLocales(): array {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Tell whether a day is a weekday, weekend or a day that has a transition between the two - * @param int $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return int|false - * Returns one of the constants - * IntlCalendar::DOW_TYPE_WEEKDAY, - * IntlCalendar::DOW_TYPE_WEEKEND, - * IntlCalendar::DOW_TYPE_WEEKEND_OFFSET or - * IntlCalendar::DOW_TYPE_WEEKEND_CEASE or FALSE on failure. - */ - public function getDayOfWeekType(int $dayOfWeek): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error code on the object - * @link https://secure.php.net/manual/en/intlcalendar.geterrorcode.php - * @return int|false An ICU error code indicating either success, failure or a warning. - */ - public function getErrorCode(): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error message on the object - * @link https://secure.php.net/manual/en/intlcalendar.geterrormessage.php - * @return string|false The error message associated with last error that occurred in a function call on this object, or a string indicating the non-existance of an error. - */ - public function getErrorMessage(): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the first day of the week for the calendar's locale - * @link https://secure.php.net/manual/en/intlcalendar.getfirstdayofweek.php - * @return int|false - * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY or FALSE on failure. - */ - public function getFirstDayOfWeek(): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the largest local minimum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getgreatestminimum.php - * @param int $field - * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - * @return int|false - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing a field value, in the field's - * unit, or FALSE on failure. - */ - public function getGreatestMinimum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get set of locale keyword values - * @param string $keyword

- * The locale keyword for which relevant values are to be queried. Only - * 'calendar' is supported. - *

- * @param string $locale

- * The locale onto which the keyword/value pair are to be appended. - *

- * @param bool $onlyCommon - *

- * Whether to show only the values commonly used for the specified locale. - *

- * @return Iterator|false An iterator that yields strings with the locale keyword values or FALSE on failure. - */ - public static function getKeywordValuesForLocale( - string $keyword, - string $locale, - bool $onlyCommon - ): IntlIterator|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the smallest local maximum for a field - * @link https://secure.php.net/manual/en/intlcalendar.getleastmaximum.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * An {@link https://secure.php.net/manual/en/language.types.integer.ph int} representing a field value in the field's - * unit or FALSE on failure. - */ - public function getLeastMaximum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the locale associated with the object - * @link https://secure.php.net/manual/en/intlcalendar.getlocale.php - * @param int $type

- * Whether to fetch the actual locale (the locale from which the calendar - * data originates, with Locale::ACTUAL_LOCALE) or the - * valid locale, i.e., the most specific locale supported by ICU relatively - * to the requested locale – see Locale::VALID_LOCALE. - * From the most general to the most specific, the locales are ordered in - * this fashion – actual locale, valid locale, requested locale. - *

- * @return string|false - */ - public function getLocale(int $type): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the global maximum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getmaximum.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - */ - public function getMaximum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get minimal number of days the first week in a year or month can have - * @link https://secure.php.net/manual/en/intlcalendar.getminimaldaysinfirstweek.php - * @return int|false - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing a number of days or FALSE on failure. - */ - public function getMinimalDaysInFirstWeek(): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the global minimum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getminimum.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * An int representing a value for the given field in the field's unit or FALSE on failure. - */ - public function getMinimum(int $field): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get number representing the current time - * @return float A float representing a number of milliseconds since the epoch, not counting leap seconds. - */ - public static function getNow(): float {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get behavior for handling repeating wall time - * @link https://secure.php.net/manual/en/intlcalendar.getrepeatedwalltimeoption.php - * @return int - * One of the constants IntlCalendar::WALLTIME_FIRST or - * IntlCalendar::WALLTIME_LAST. - */ - public function getRepeatedWallTimeOption(): int {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get behavior for handling skipped wall time - * @link https://secure.php.net/manual/en/intlcalendar.getskippedwalltimeoption.php - * @return int - * One of the constants IntlCalendar::WALLTIME_FIRST, - * IntlCalendar::WALLTIME_LAST or - * IntlCalendar::WALLTIME_NEXT_VALID. - */ - public function getSkippedWallTimeOption(): int {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get time currently represented by the object - * @return float|false - * A {@link https://secure.php.net/manual/en/language.types.float.php float} representing the number of milliseconds elapsed since the - * reference time (1 Jan 1970 00:00:00 UTC). - */ - public function getTime(): float|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the object's timezone - * @link https://secure.php.net/manual/en/intlcalendar.gettimezone.php - * @return IntlTimeZone|false - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone} object corresponding to the one used - * internally in this object. - */ - public function getTimeZone(): IntlTimeZone|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the calendar type - * @link https://secure.php.net/manual/en/intlcalendar.gettype.php - * @return string - * A {@link https://secure.php.net/manual/en/language.types.string.php string} representing the calendar type, such as - * 'gregorian', 'islamic', etc. - */ - public function getType(): string {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get time of the day at which weekend begins or ends - * @link https://secure.php.net/manual/en/intlcalendar.getweekendtransition.php - * @param string $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return int|false - * The number of milliseconds into the day at which the the weekend begins or - * ends or FALSE on failure. - */ - public function getWeekendTransition(int $dayOfWeek): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether the object's time is in Daylight Savings Time - * @link https://secure.php.net/manual/en/intlcalendar.indaylighttime.php - * @return bool - * Returns TRUE if the date is in Daylight Savings Time, FALSE otherwise. - * The value FALSE may also be returned on failure, for instance after - * specifying invalid field values on non-lenient mode; use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or query - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to disambiguate. - */ - public function inDaylightTime(): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether another calendar is equal but for a different time - * @link https://secure.php.net/manual/en/intlcalendar.isequivalentto.php - * @param IntlCalendar $other The other calendar against which the comparison is to be made. - * @return bool - * Assuming there are no argument errors, returns TRUE iif the calendars are equivalent except possibly for their set time. - */ - public function isEquivalentTo(IntlCalendar $other): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether date/time interpretation is in lenient mode - * @link https://secure.php.net/manual/en/intlcalendar.islenient.php - * @return bool - * A {@link https://secure.php.net/manual/en/language.types.boolean.php bool} representing whether the calendar is set to lenient mode. - */ - public function isLenient(): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether a certain date/time is in the weekend - * @link https://secure.php.net/manual/en/intlcalendar.isweekend.php - * @param float|null $timestamp [optional]

- * An optional timestamp representing the number of milliseconds since the - * epoch, excluding leap seconds. If NULL, this object's current time is - * used instead. - *

- * @return bool - *

A {@link https://secure.php.net/manual/en/language.types.boolean.php bool} indicating whether the given or this object's time occurs - * in a weekend. - *

- *

- * The value FALSE may also be returned on failure, for instance after giving - * a date out of bounds on non-lenient mode; use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or query - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to disambiguate.

- */ - public function isWeekend(float|null $timestamp = null): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Add value to field without carrying into more significant fields - * @link https://secure.php.net/manual/en/intlcalendar.roll.php - * @param int $field - *

One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time - * {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param mixed $value

- * The (signed) amount to add to the field, TRUE for rolling up (adding - * 1), or FALSE for rolling down (subtracting - * 1). - *

- * @return bool Returns TRUE on success or FALSE on failure. - */ - public function roll(int $field, $value): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether a field is set - * @link https://secure.php.net/manual/en/intlcalendar.isset.php - * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time - * {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return bool Assuming there are no argument errors, returns TRUE iif the field is set. - */ - public function PS_UNRESERVE_PREFIX_isSet(int $field): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set a time field or several common fields at once - * @link https://secure.php.net/manual/en/intlcalendar.set.php - * @param int $year

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param int $month

- * The new value for IntlCalendar::FIELD_MONTH. - *

- * @param int $dayOfMonth [optional]

- * The new value for IntlCalendar::FIELD_DAY_OF_MONTH. - * The month sequence is zero-based, i.e., January is represented by 0, - * February by 1, ..., December is 11 and Undecember (if the calendar has - * it) is 12. - *

- * @param int $hour [optional] - *

- * The new value for IntlCalendar::FIELD_HOUR_OF_DAY. - *

- * @param int $minute [optional] - *

- * The new value for IntlCalendar::FIELD_MINUTE. - *

- * @param int $second [optional]

- * The new value for IntlCalendar::FIELD_SECOND. - *

- * @return bool Returns TRUE on success and FALSE on failure. - */ - public function set($year, $month, $dayOfMonth = null, $hour = null, $minute = null, $second = null) {} - - /** - * (PHP 5 >= 5.5.0 PECL intl >= 3.0.0a1)
- * Set a time field or several common fields at once - * @link https://secure.php.net/manual/en/intlcalendar.set.php - * @param int $field One of the IntlCalendar date/time field constants. These are integer values between 0 and IntlCalendar::FIELD_COUNT. - * @param int $value The new value of the given field. - * @return bool Returns TRUE on success and FALSE on failure. - * @since 5.5 - */ - public function set($field, $value) {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the day on which the week is deemed to start - * @link https://secure.php.net/manual/en/intlcalendar.setfirstdayofweek.php - * @param int $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return bool Returns TRUE on success. Failure can only happen due to invalid parameters. - */ - public function setFirstDayOfWeek(int $dayOfWeek): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set whether date/time interpretation is to be lenient - * @link https://secure.php.net/manual/en/intlcalendar.setlenient.php - * @param bool $lenient

- * Use TRUE to activate the lenient mode; FALSE otherwise. - *

- * @return bool Returns TRUE on success. Failure can only happen due to invalid parameters. - */ - public function setLenient(bool $lenient): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set behavior for handling repeating wall times at negative timezone offset transitions - * @link https://secure.php.net/manual/en/intlcalendar.setrepeatedwalltimeoption.php - * @param int $option

- * One of the constants IntlCalendar::WALLTIME_FIRST or - * IntlCalendar::WALLTIME_LAST. - *

- * @return bool - * Returns TRUE on success. Failure can only happen due to invalid parameters. - */ - public function setRepeatedWallTimeOption(int $option): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set behavior for handling skipped wall times at positive timezone offset transitions - * @link https://secure.php.net/manual/en/intlcalendar.setskippedwalltimeoption.php - * @param int $option

- * One of the constants IntlCalendar::WALLTIME_FIRST, - * IntlCalendar::WALLTIME_LAST or - * IntlCalendar::WALLTIME_NEXT_VALID. - *

- * @return bool - *

- * Returns TRUE on success. Failure can only happen due to invalid parameters. - *

- */ - public function setSkippedWallTimeOption(int $option): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the calendar time in milliseconds since the epoch - * @link https://secure.php.net/manual/en/intlcalendar.settime.php - * @param float $timestamp

- * An instant represented by the number of number of milliseconds between - * such instant and the epoch, ignoring leap seconds. - *

- * @return bool - * Returns TRUE on success and FALSE on failure. - */ - public function setTime(float $timestamp): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the timezone used by this calendar - * @link https://secure.php.net/manual/en/intlcalendar.settimezone.php - * @param mixed $timezone

- * The new timezone to be used by this calendar. It can be specified in the - * following ways: - * - *

    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone date.timezone} or - * through the function {@link https://secure.php.net/manual/en/function.date-default-timezone-set.php date_default_timezone_set()} and as - * returned by {@link https://secure.php.net/manual/en/function.date-default-timezone-get.php date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/class.datetimezone.php DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/language.types.string.php string}, which should be a valid ICU timezone identifier. - * See b>IntlTimeZone::createTimeZoneIDEnumeration(). Raw - * offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- * @return bool Returns TRUE on success and FALSE on failure. - */ - public function setTimeZone($timezone): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a2)
- * Convert an IntlCalendar into a DateTime object - * @link https://secure.php.net/manual/en/intlcalendar.todatetime.php - * @return DateTime|false - * A {@link https://secure.php.net/manual/en/class.datetime.php DateTime} object with the same timezone as this - * object (though using PHP's database instead of ICU's) and the same time, - * except for the smaller precision (second precision instead of millisecond). - * Returns FALSE on failure. - */ - public function toDateTime(): DateTime|false {} - - /** - * @link https://www.php.net/manual/en/intlcalendar.setminimaldaysinfirstweek.php - * @param int $days - * @return bool - */ - public function setMinimalDaysInFirstWeek(int $days): bool {} - - /** - * @since 8.3 - */ - public function setDate(int $year, int $month, int $dayOfMonth): void {} - - /** - * @since 8.3 - */ - public function setDateTime(int $year, int $month, int $dayOfMonth, int $hour, int $minute, ?int $second = null): void {} -} - -/** - * @since 5.5 - */ -class IntlIterator implements Iterator -{ - public function current(): mixed {} - - public function key(): mixed {} - - public function next(): void {} - - public function rewind(): void {} - - public function valid(): bool {} -} - -/** - * @since 5.5 - */ -class IntlException extends Exception {} - -/** - * @since 5.5 - */ -class IntlTimeZone -{ - /* Constants */ - public const DISPLAY_SHORT = 1; - public const DISPLAY_LONG = 2; - public const DISPLAY_SHORT_GENERIC = 3; - public const DISPLAY_LONG_GENERIC = 4; - public const DISPLAY_SHORT_GMT = 5; - public const DISPLAY_LONG_GMT = 6; - public const DISPLAY_SHORT_COMMONLY_USED = 7; - public const DISPLAY_GENERIC_LOCATION = 8; - public const TYPE_ANY = 0; - public const TYPE_CANONICAL = 1; - public const TYPE_CANONICAL_LOCATION = 2; - - /* Methods */ - - private function __construct() {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the number of IDs in the equivalency group that includes the given ID - * @link https://secure.php.net/manual/en/intltimezone.countequivalentids.php - * @param string $timezoneId - * @return int|false number of IDs or FALSE on failure - */ - public static function countEquivalentIDs(string $timezoneId): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a new copy of the default timezone for this host - * @link https://secure.php.net/manual/en/intltimezone.createdefault.php - * @return IntlTimeZone - */ - public static function createDefault(): IntlTimeZone {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get an enumeration over time zone IDs associated with the given country or offset - * @link https://secure.php.net/manual/en/intltimezone.createenumeration.php - * @param mixed $countryOrRawOffset [optional] - * @return IntlIterator|false an iterator or FALSE on failure - */ - public static function createEnumeration($countryOrRawOffset): IntlIterator|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a timezone object for the given ID - * @link https://secure.php.net/manual/en/intltimezone.createtimezone.php - * @param string $timezoneId - * @return IntlTimeZone|null a timezone object or NULL on failure - */ - public static function createTimeZone(string $timezoneId): ?IntlTimeZone {} - - /** - * (PHP 5 >=5.5.0)
- * Get an enumeration over system time zone IDs with the given filter conditions - * @link https://secure.php.net/manual/en/intltimezone.createtimezoneidenumeration.php - * @param int $type - * @param string|null $region [optional] - * @param int $rawOffset [optional] - * @return IntlIterator|false an iterator or FALSE on failure - */ - public static function createTimeZoneIDEnumeration( - int $type, - string|null $region = null, - int|null $rawOffset = null - ): IntlIterator|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a timezone object from DateTimeZone - * @link https://secure.php.net/manual/en/intltimezone.fromdatetimezone.php - * @param DateTimeZone $timezone - * @return IntlTimeZone|null a timezone object or NULL on failure - */ - public static function fromDateTimeZone(DateTimeZone $timezone): ?IntlTimeZone {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the canonical system timezone ID or the normalized custom time zone ID for the given time zone ID - * @link https://secure.php.net/manual/en/intltimezone.getcanonicalid.php - * @param string $timezoneId - * @param bool &$isSystemId [optional] - * @return string|false the timezone ID or FALSE on failure - */ - public static function getCanonicalID(string $timezoneId, &$isSystemId): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get a name of this time zone suitable for presentation to the user - * @param bool $dst [optional] - * @param int $style [optional] - * @param string $locale [optional] - * @return string|false the timezone name or FALSE on failure - */ - public function getDisplayName( - bool $dst = false, - int $style = 2, - string|null $locale - ): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the amount of time to be added to local standard time to get local wall clock time - * @link https://secure.php.net/manual/en/intltimezone.getequivalentid.php - * @return int - */ - public function getDSTSavings(): int {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get an ID in the equivalency group that includes the given ID - * @link https://secure.php.net/manual/en/intltimezone.getequivalentid.php - * @param string $timezoneId - * @param int $offset - * @return string|false the time zone ID or FALSE on failure - */ - public static function getEquivalentID( - string $timezoneId, - int $offset - ): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error code on the object - * @link https://secure.php.net/manual/en/intltimezone.geterrorcode.php - * @return int|false - */ - public function getErrorCode(): int|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error message on the object - * @link https://secure.php.net/manual/en/intltimezone.geterrormessage.php - * @return string|false - */ - public function getErrorMessage(): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create GMT (UTC) timezone - * @link https://secure.php.net/manual/en/intltimezone.getgmt.php - * @return IntlTimeZone - */ - public static function getGMT(): IntlTimeZone {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get timezone ID - * @return string|false - */ - public function getID(): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the time zone raw and GMT offset for the given moment in time - * @link https://secure.php.net/manual/en/intltimezone.getoffset.php - * @param float $timestamp - * moment in time for which to return offsets, in units of milliseconds from - * January 1, 1970 0:00 GMT, either GMT time or local wall time, depending on - * `local'. - * @param bool $local - * if true, `date' is local wall time; otherwise it is in GMT time. - * @param int &$rawOffset - * output parameter to receive the raw offset, that is, the offset not - * including DST adjustments - * @param int &$dstOffset - * output parameter to receive the DST offset, that is, the offset to be added - * to `rawOffset' to obtain the total offset between local and GMT time. If - * DST is not in effect, this value is zero; otherwise it is a positive value, - * typically one hour. - * @return bool boolean indication of success - */ - public function getOffset( - float $timestamp, - bool $local, - &$rawOffset, - &$dstOffset - ): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the raw GMT offset (before taking daylight savings time into account - * @link https://secure.php.net/manual/en/intltimezone.getrawoffset.php - * @return int - */ - public function getRawOffset(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Get the region code associated with the given system time zone ID - * @link https://secure.php.net/manual/en/intltimezone.getregion.php - * @param string $timezoneId - * @return string|false region or FALSE on failure - */ - public static function getRegion(string $timezoneId): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the timezone data version currently used by ICU - * @link https://secure.php.net/manual/en/intltimezone.gettzdataversion.php - * @return string|false - */ - public static function getTZDataVersion(): string|false {} - - /** - * (PHP 5 >=5.5.0)
- * Get the "unknown" time zone - * @link https://secure.php.net/manual/en/intltimezone.getunknown.php - * @return IntlTimeZone - */ - public static function getUnknown(): IntlTimeZone {} - - /** - * (PHP 7 >=7.1.0)
- * Translates a system timezone (e.g. "America/Los_Angeles") into a Windows - * timezone (e.g. "Pacific Standard Time"). - * @link https://secure.php.net/manual/en/intltimezone.getwindowsid.php - * @param string $timezoneId - * @return string|false the Windows timezone or FALSE on failure - * @since 7.1 - */ - public static function getWindowsID(string $timezoneId): string|false {} - - /** - * @link https://www.php.net/manual/en/intltimezone.getidforwindowsid.php - * @param string $timezoneId - * @param string|null $region - * @return string|false the Windows timezone or FALSE on failure - * @since 7.1 - */ - public static function getIDForWindowsID(string $timezoneId, ?string $region = null): string|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Check if this zone has the same rules and offset as another zone - * @link https://secure.php.net/manual/en/intltimezone.hassamerules.php - * @param IntlTimeZone $other - * @return bool - */ - public function hasSameRules(IntlTimeZone $other): bool {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Convert to DateTimeZone object - * @link https://secure.php.net/manual/en/intltimezone.todatetimezone.php - * @return DateTimeZone|false the DateTimeZone object or FALSE on failure - */ - public function toDateTimeZone(): DateTimeZone|false {} - - /** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Check if this time zone uses daylight savings time - * @link https://secure.php.net/manual/en/intltimezone.usedaylighttime.php - * @return bool - */ - public function useDaylightTime(): bool {} - - /** - * @since 8.4 - */ - public static function getIanaID(string $timezoneId): string|false {} -} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a collator - * @link https://php.net/manual/en/collator.create.php - * @param string $locale

- * The locale containing the required collation rules. Special values for - * locales can be passed in - if null is passed for the locale, the - * default locale collation rules will be used. If empty string ("") or - * "root" are passed, UCA rules will be used. - *

- * @return Collator|null Return new instance of Collator object, or NULL - * on error. - */ -function collator_create(string $locale): ?Collator {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Compare two Unicode strings - * @link https://php.net/manual/en/collator.compare.php - * @param Collator $object - * @param string $string1

- * The first string to compare. - *

- * @param string $string2

- * The second string to compare. - *

- * @return int|false Return comparison result:

- *

- *

- * 1 if string1 is greater than - * string2 ; - *

- *

- * 0 if string1 is equal to - * string2; - *

- *

- * -1 if string1 is less than - * string2 . - *

- * On error - * boolean - * FALSE - * is returned. - */ -function collator_compare(Collator $object, string $string1, string $string2): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get collation attribute value - * @link https://php.net/manual/en/collator.getattribute.php - * @param Collator $object - * @param int $attribute

- * Attribute to get value for. - *

- * @return int|false Attribute value, or boolean FALSE on error. - */ -function collator_get_attribute(Collator $object, int $attribute): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set collation attribute - * @link https://php.net/manual/en/collator.setattribute.php - * @param Collator $object - * @param int $attribute

Attribute.

- * @param int $value

- * Attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function collator_set_attribute(Collator $object, int $attribute, int $value): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get current collation strength - * @link https://php.net/manual/en/collator.getstrength.php - * @param Collator $object - * @return int current collation strength - */ -function collator_get_strength(Collator $object): int {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set collation strength - * @link https://php.net/manual/en/collator.setstrength.php - * @param Collator $object - * @param int $strength

Strength to set.

- *

- * Possible values are: - * Collator::PRIMARY - *

- * @return bool TRUE on success or FALSE on failure. - */ -function collator_set_strength(Collator $object, int $strength): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array using specified collator - * @link https://php.net/manual/en/collator.sort.php - * @param Collator $object - * @param string[] &$array

- * Array of strings to sort. - *

- * @param int $flags

- * Optional sorting type, one of the following: - *

- *

- * Collator::SORT_REGULAR - * - compare items normally (don't change types) - *

- * @return bool TRUE on success or FALSE on failure. - */ -function collator_sort(Collator $object, array &$array, int $flags = 0): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array using specified collator and sort keys - * @link https://php.net/manual/en/collator.sortwithsortkeys.php - * @param Collator $object - * @param string[] &$array

Array of strings to sort

- * @return bool TRUE on success or FALSE on failure. - */ -function collator_sort_with_sort_keys( - Collator $object, - array &$array, -): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Sort array maintaining index association - * @link https://php.net/manual/en/collator.asort.php - * @param Collator $object - * @param string[] &$array

Array of strings to sort.

- * @param int $flags

- * Optional sorting type, one of the following: - * Collator::SORT_REGULAR - * - compare items normally (don't change types) - *

- * @return bool TRUE on success or FALSE on failure. - */ -function collator_asort(Collator $object, array &$array, int $flags = 0): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale name of the collator - * @link https://php.net/manual/en/collator.getlocale.php - * @param Collator $object - * @param int $type

- * You can choose between valid and actual locale ( - * Locale::VALID_LOCALE and - * Locale::ACTUAL_LOCALE, - * respectively). The default is the actual locale. - *

- * @return string|false Real locale name from which the collation data comes. If the collator was - * instantiated from rules or an error occurred, returns - * boolean FALSE. - */ -function collator_get_locale(Collator $object, int $type): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get collator's last error code - * @link https://php.net/manual/en/collator.geterrorcode.php - * @param Collator $object - * @return int|false Error code returned by the last Collator API function call. - */ -function collator_get_error_code(Collator $object): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get text for collator's last error code - * @link https://php.net/manual/en/collator.geterrormessage.php - * @param Collator $object - * @return string|false Description of an error occurred in the last Collator API function call. - */ -function collator_get_error_message(Collator $object): string|false {} - -/** - * (PHP 5 >= 5.3.2, PHP 7, PECL intl >= 1.0.3)
- * Get sorting key for a string - * @link https://php.net/manual/en/collator.getsortkey.php - * @param Collator $object - * @param string $string

- * The string to produce the key from. - *

- * @return string|false the collation key for the string. Collation keys can be compared directly instead of strings. - */ -function collator_get_sort_key( - Collator $object, - string $string, -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a number formatter - * @link https://php.net/manual/en/numberformatter.create.php - * @param string $locale

- * Locale in which the number would be formatted (locale name, e.g. en_CA). - *

- * @param int $style

- * Style of the formatting, one of the - * format style constants. If - * NumberFormatter::PATTERN_DECIMAL - * or NumberFormatter::PATTERN_RULEBASED - * is passed then the number format is opened using the given pattern, - * which must conform to the syntax described in - * ICU DecimalFormat - * documentation or - * ICU RuleBasedNumberFormat - * documentation, respectively. - *

- * @param string|null $pattern [optional]

- * Pattern string if the chosen style requires a pattern. - *

- * @return NumberFormatter|null NumberFormatter object or NULL on error. - */ -function numfmt_create(string $locale, int $style, ?string $pattern = null): ?NumberFormatter {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format a number - * @link https://php.net/manual/en/numberformatter.format.php - * @param NumberFormatter $formatter - * @param int|float $num

- * The value to format. Can be integer or float, - * other values will be converted to a numeric value. - *

- * @param int $type

- * The - * formatting type to use. - *

- * @return string|false the string containing formatted value, or FALSE on error. - */ -function numfmt_format(NumberFormatter $formatter, int|float $num, int $type = 0): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse a number - * @link https://php.net/manual/en/numberformatter.parse.php - * @param NumberFormatter $formatter - * @param string $string - * @param int $type [optional]

- * The - * formatting type to use. By default, - * NumberFormatter::TYPE_DOUBLE is used. - *

- * @param int &$offset [optional]

- * Offset in the string at which to begin parsing. On return, this value - * will hold the offset at which parsing ended. - *

- * @return int|float|false The value of the parsed number or FALSE on error. - */ -function numfmt_parse(NumberFormatter $formatter, string $string, int $type = NumberFormatter::TYPE_DOUBLE, &$offset = null): int|float|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format a currency value - * @link https://php.net/manual/en/numberformatter.formatcurrency.php - * @param NumberFormatter $formatter - * @param float $amount

- * The numeric currency value. - *

- * @param string $currency

- * The 3-letter ISO 4217 currency code indicating the currency to use. - *

- * @return string|false String representing the formatted currency value. - */ -function numfmt_format_currency(NumberFormatter $formatter, float $amount, string $currency): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse a currency number - * @link https://php.net/manual/en/numberformatter.parsecurrency.php - * @param NumberFormatter $formatter - * @param string $string - * @param string &$currency

- * Parameter to receive the currency name (3-letter ISO 4217 currency - * code). - *

- * @param int &$offset [optional]

- * Offset in the string at which to begin parsing. On return, this value - * will hold the offset at which parsing ended. - *

- * @return float|false The parsed numeric value or FALSE on error. - */ -function numfmt_parse_currency(NumberFormatter $formatter, string $string, &$currency, &$offset = null): float|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set an attribute - * @link https://php.net/manual/en/numberformatter.setattribute.php - * @param NumberFormatter $formatter - * @param int $attribute

- * Attribute specifier - one of the - * numeric attribute constants. - *

- * @param int|float $value

- * The attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function numfmt_set_attribute(NumberFormatter $formatter, int $attribute, int|float $value): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get an attribute - * @link https://php.net/manual/en/numberformatter.getattribute.php - * @param NumberFormatter $formatter - * @param int $attribute

- * Attribute specifier - one of the - * numeric attribute constants. - *

- * @return int|float|false Return attribute value on success, or FALSE on error. - */ -function numfmt_get_attribute(NumberFormatter $formatter, int $attribute): int|float|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set a text attribute - * @link https://php.net/manual/en/numberformatter.settextattribute.php - * @param NumberFormatter $formatter - * @param int $attribute

- * Attribute specifier - one of the - * text attribute - * constants. - *

- * @param string $value

- * Text for the attribute value. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function numfmt_set_text_attribute(NumberFormatter $formatter, int $attribute, string $value): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get a text attribute - * @link https://php.net/manual/en/numberformatter.gettextattribute.php - * @param NumberFormatter $formatter - * @param int $attribute

- * Attribute specifier - one of the - * text attribute constants. - *

- * @return string|false Return attribute value on success, or FALSE on error. - */ -function numfmt_get_text_attribute(NumberFormatter $formatter, int $attribute): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set a symbol value - * @link https://php.net/manual/en/numberformatter.setsymbol.php - * @param NumberFormatter $formatter - * @param int $symbol

- * Symbol specifier, one of the - * format symbol constants. - *

- * @param string $value

- * Text for the symbol. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function numfmt_set_symbol(NumberFormatter $formatter, int $symbol, string $value): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get a symbol value - * @link https://php.net/manual/en/numberformatter.getsymbol.php - * @param NumberFormatter $formatter - * @param int $symbol

- * Symbol specifier, one of the - * format symbol constants. - *

- * @return string|false The symbol string or FALSE on error. - */ -function numfmt_get_symbol(NumberFormatter $formatter, int $symbol): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set formatter pattern - * @link https://php.net/manual/en/numberformatter.setpattern.php - * @param NumberFormatter $formatter - * @param string $pattern

- * Pattern in syntax described in - * ICU DecimalFormat - * documentation. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function numfmt_set_pattern(NumberFormatter $formatter, string $pattern): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter pattern - * @link https://php.net/manual/en/numberformatter.getpattern.php - * @param NumberFormatter $formatter - * @return string|false Pattern string that is used by the formatter, or FALSE if an error happens. - */ -function numfmt_get_pattern(NumberFormatter $formatter): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter locale - * @link https://php.net/manual/en/numberformatter.getlocale.php - * @param NumberFormatter $formatter - * @param int $type

- * You can choose between valid and actual locale ( - * Locale::VALID_LOCALE, - * Locale::ACTUAL_LOCALE, - * respectively). The default is the actual locale. - *

- * @return string|false The locale name used to create the formatter. - */ -function numfmt_get_locale(NumberFormatter $formatter, int $type = 0): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter's last error code. - * @link https://php.net/manual/en/numberformatter.geterrorcode.php - * @param NumberFormatter $formatter - * @return int error code from last formatter call. - */ -function numfmt_get_error_code(NumberFormatter $formatter): int {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get formatter's last error message. - * @link https://php.net/manual/en/numberformatter.geterrormessage.php - * @param NumberFormatter $formatter - * @return string error message from last formatter call. - */ -function numfmt_get_error_message(NumberFormatter $formatter): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Normalizes the input provided and returns the normalized string - * @link https://php.net/manual/en/normalizer.normalize.php - * @param string $string

The input string to normalize

- * @param int $form [optional]

One of the normalization forms.

- * @return string|false The normalized string or FALSE if an error occurred. - */ -function normalizer_normalize(string $string, int $form = Normalizer::FORM_C): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Checks if the provided string is already in the specified normalization - * form. - * @link https://php.net/manual/en/normalizer.isnormalized.php - * @param string $string

The input string to normalize

- * @param int $form [optional]

- * One of the normalization forms. - *

- * @return bool TRUE if normalized, FALSE otherwise or if there an error - */ -function normalizer_is_normalized(string $string, int $form = Normalizer::FORM_C): bool {} - -/** - * Gets the default locale value from the intl global 'default_locale' - * @link https://php.net/manual/en/function.locale-get-default.php - * @return string a string with the current Locale. - */ -function locale_get_default(): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the default runtime Locale - * @link https://php.net/manual/en/function.locale-set-default.php - * @param string $locale

- * The new Locale name. A comprehensive list of the supported locales is - * available at . - *

- * @return bool TRUE on success or FALSE on failure. - */ -function locale_set_default(string $locale): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the primary language for the input locale - * @link https://php.net/manual/en/locale.getprimarylanguage.php - * @param string $locale

- * The locale to extract the primary language code from - *

- * @return string|null The language code associated with the language or NULL in case of error. - */ -function locale_get_primary_language(string $locale): ?string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the script for the input locale - * @link https://php.net/manual/en/locale.getscript.php - * @param string $locale

- * The locale to extract the script code from - *

- * @return string|null The script subtag for the locale or NULL if not present - */ -function locale_get_script(string $locale): ?string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the region for the input locale - * @link https://php.net/manual/en/locale.getregion.php - * @param string $locale

- * The locale to extract the region code from - *

- * @return string|null The region subtag for the locale or NULL if not present - */ -function locale_get_region(string $locale): ?string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the keywords for the input locale - * @link https://php.net/manual/en/locale.getkeywords.php - * @param string $locale

- * The locale to extract the keywords from - *

- * @return array|false|null Associative array containing the keyword-value pairs for this locale - */ -function locale_get_keywords(string $locale): array|false|null {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for script of the input locale - * @link https://php.net/manual/en/locale.getdisplayscript.php - * @param string $locale

- * The locale to return a display script for - *

- * @param string|null $displayLocale

- * Optional format locale to use to display the script name - *

- * @return string|false Display name of the script for the $locale in the format appropriate for - * $in_locale. - */ -function locale_get_display_script( - string $locale, - ?string $displayLocale = null -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for region of the input locale - * @link https://php.net/manual/en/locale.getdisplayregion.php - * @param string $locale

- * The locale to return a display region for. - *

- * @param string|null $displayLocale

- * Optional format locale to use to display the region name - *

- * @return string|false display name of the region for the $locale in the format appropriate for - * $in_locale. - */ -function locale_get_display_region( - string $locale, - ?string $displayLocale = null -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for the input locale - * @link https://php.net/manual/en/locale.getdisplayname.php - * @param string $locale

- * The locale to return a display name for. - *

- * @param string|null $displayLocale

optional format locale

- * @return string|false Display name of the locale in the format appropriate for $in_locale. - */ -function locale_get_display_name( - string $locale, - ?string $displayLocale = null -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for language of the inputlocale - * @link https://php.net/manual/en/locale.getdisplaylanguage.php - * @param string $locale

- * The locale to return a display language for - *

- * @param string|null $displayLocale

- * Optional format locale to use to display the language name - *

- * @return string|false display name of the language for the $locale in the format appropriate for - * $in_locale. - */ -function locale_get_display_language( - string $locale, - ?string $displayLocale = null -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns an appropriately localized display name for variants of the input locale - * @link https://php.net/manual/en/locale.getdisplayvariant.php - * @param string $locale

- * The locale to return a display variant for - *

- * @param string|null $displayLocale

- * Optional format locale to use to display the variant name - *

- * @return string|false Display name of the variant for the $locale in the format appropriate for - * $in_locale. - */ -function locale_get_display_variant( - string $locale, - ?string $displayLocale = null -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns a correctly ordered and delimited locale ID - * @link https://php.net/manual/en/locale.composelocale.php - * @param string[] $subtags

- * an array containing a list of key-value pairs, where the keys identify - * the particular locale ID subtags, and the values are the associated - * subtag values. - *

- * The 'variant' and 'private' subtags can take maximum 15 values - * whereas 'extlang' can take maximum 3 values.e.g. Variants are allowed - * with the suffix ranging from 0-14. Hence the keys for the input array - * can be variant0, variant1, ...,variant14. In the returned locale id, - * the subtag is ordered by suffix resulting in variant0 followed by - * variant1 followed by variant2 and so on. - *

- *

- * The 'variant', 'private' and 'extlang' multiple values can be specified both - * as array under specific key (e.g. 'variant') and as multiple numbered keys - * (e.g. 'variant0', 'variant1', etc.). - *

- *

- * @return string|false The corresponding locale identifier. - */ -function locale_compose(array $subtags): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns a key-value array of locale ID subtag elements. - * @link https://php.net/manual/en/locale.parselocale.php - * @param string $locale

- * The locale to extract the subtag array from. Note: The 'variant' and - * 'private' subtags can take maximum 15 values whereas 'extlang' can take - * maximum 3 values. - *

- * @return string[]|null an array containing a list of key-value pairs, where the keys - * identify the particular locale ID subtags, and the values are the - * associated subtag values. The array will be ordered as the locale id - * subtags e.g. in the locale id if variants are '-varX-varY-varZ' then the - * returned array will have variant0=>varX , variant1=>varY , - * variant2=>varZ - */ -function locale_parse(string $locale): ?array {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Gets the variants for the input locale - * @link https://php.net/manual/en/locale.getallvariants.php - * @param string $locale

- * The locale to extract the variants from - *

- * @return array|null The array containing the list of all variants subtag for the locale - * or NULL if not present - */ -function locale_get_all_variants(string $locale): ?array {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Checks if a language tag filter matches with locale - * @link https://php.net/manual/en/locale.filtermatches.php - * @param string $languageTag

- * The language tag to check - *

- * @param string $locale

- * The language range to check against - *

- * @param bool $canonicalize

- * If true, the arguments will be converted to canonical form before - * matching. - *

- * @return bool|null TRUE if $locale matches $langtag FALSE otherwise. - */ -function locale_filter_matches( - string $languageTag, - string $locale, - bool $canonicalize = false -): ?bool {} - -/** - * Canonicalize the locale string - * @param string $locale - * - * @return null|string - */ -function locale_canonicalize(string $locale): ?string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Searches the language tag list for the best match to the language - * @link https://php.net/manual/en/locale.lookup.php - * @param string[] $languageTag

- * An array containing a list of language tags to compare to - * locale. Maximum 100 items allowed. - *

- * @param string $locale

- * The locale to use as the language range when matching. - *

- * @param bool $canonicalize

- * If true, the arguments will be converted to canonical form before - * matching. - *

- * @param string|null $defaultLocale

- * The locale to use if no match is found. - *

- * @return string|null The closest matching language tag or default value. - */ -function locale_lookup( - array $languageTag, - string $locale, - bool $canonicalize = false, - ?string $defaultLocale = null, -): ?string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Tries to find out best available locale based on HTTP "Accept-Language" header - * @link https://php.net/manual/en/locale.acceptfromhttp.php - * @param string $header

- * The string containing the "Accept-Language" header according to format in RFC 2616. - *

- * @return string|false The corresponding locale identifier. - */ -function locale_accept_from_http(string $header): string|false {} - -/** - * Constructs a new message formatter - * @param string $locale - * @param string $pattern - * @return MessageFormatter|null - */ -function msgfmt_create(string $locale, string $pattern): ?MessageFormatter {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format the message - * @link https://php.net/manual/en/messageformatter.format.php - * @param MessageFormatter $formatter - * @param array $values

- * Arguments to insert into the format string - *

- * @return string|false The formatted string, or FALSE if an error occurred - */ -function msgfmt_format(MessageFormatter $formatter, array $values): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Quick format message - * @link https://php.net/manual/en/messageformatter.formatmessage.php - * @param string $locale

- * The locale to use for formatting locale-dependent parts - *

- * @param string $pattern

- * The pattern string to insert things into. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @param array $values

- * The array of values to insert into the format string - *

- * @return string|false The formatted pattern string or FALSE if an error occurred - */ -function msgfmt_format_message(string $locale, string $pattern, array $values): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse input string according to pattern - * @link https://php.net/manual/en/messageformatter.parse.php - * @param MessageFormatter $formatter - * @param string $string

- * The string to parse - *

- * @return array|false An array containing the items extracted, or FALSE on error - */ -function msgfmt_parse(MessageFormatter $formatter, string $string): array|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Quick parse input string - * @link https://php.net/manual/en/messageformatter.parsemessage.php - * @param string $locale

- * The locale to use for parsing locale-dependent parts - *

- * @param string $pattern

- * The pattern with which to parse the value. - *

- * @param string $message

- * The string to parse, conforming to the pattern. - *

- * @return array|false An array containing items extracted, or FALSE on error - */ -function msgfmt_parse_message(string $locale, string $pattern, string $message): array|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the pattern used by the formatter - * @link https://php.net/manual/en/messageformatter.setpattern.php - * @param MessageFormatter $formatter - * @param string $pattern

- * The pattern string to use in this message formatter. - * The pattern uses an 'apostrophe-friendly' syntax; it is run through - * umsg_autoQuoteApostrophe - * before being interpreted. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function msgfmt_set_pattern(MessageFormatter $formatter, string $pattern): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the pattern used by the formatter - * @link https://php.net/manual/en/messageformatter.getpattern.php - * @param MessageFormatter $formatter - * @return string|false The pattern string for this message formatter - */ -function msgfmt_get_pattern(MessageFormatter $formatter): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale for which the formatter was created. - * @link https://php.net/manual/en/messageformatter.getlocale.php - * @param MessageFormatter $formatter - * @return string The locale name - */ -function msgfmt_get_locale(MessageFormatter $formatter): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error code from last operation - * @link https://php.net/manual/en/messageformatter.geterrorcode.php - * @param MessageFormatter $formatter - * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR. - */ -function msgfmt_get_error_code(MessageFormatter $formatter): int {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error text from the last operation - * @link https://php.net/manual/en/messageformatter.geterrormessage.php - * @param MessageFormatter $formatter - * @return string Description of the last error. - */ -function msgfmt_get_error_message(MessageFormatter $formatter): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Create a date formatter - * @link https://php.net/manual/en/intldateformatter.create.php - * @param string|null $locale

- * Locale to use when formatting or parsing. - *

- * @param int $dateType

- * Date type to use (none, - * short, medium, - * long, full). - * This is one of the - * IntlDateFormatter constants. - *

- * @param int $timeType

- * Time type to use (none, - * short, medium, - * long, full). - * This is one of the - * IntlDateFormatter constants. - *

- * @param string|null $timezone [optional]

- * Time zone ID, default is system default. - *

- * @param IntlCalendar|int|null $calendar [optional]

- * Calendar to use for formatting or parsing; default is Gregorian. - * This is one of the - * IntlDateFormatter calendar constants. - *

- * @param string|null $pattern [optional]

- * Optional pattern to use when formatting or parsing. - * Possible patterns are documented at http://userguide.icu-project.org/formatparse/datetime. - *

- * @return IntlDateFormatter|null - */ -function datefmt_create( - ?string $locale, - int $dateType = 0, - int $timeType = 0, - $timezone = null, - IntlCalendar|int|null $calendar = null, - string|null $pattern = null -): ?IntlDateFormatter {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the datetype used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getdatetype.php - * @param IntlDateFormatter $formatter - * @return int|false The current date type value of the formatter. - */ -function datefmt_get_datetype(IntlDateFormatter $formatter): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the timetype used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.gettimetype.php - * @param IntlDateFormatter $formatter - * @return int|false The current date type value of the formatter. - */ -function datefmt_get_timetype(IntlDateFormatter $formatter): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the calendar type used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getcalendar.php - * @param IntlDateFormatter $formatter - * @return int|false The calendar being used by the formatter. - */ -function datefmt_get_calendar(IntlDateFormatter $formatter): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * sets the calendar used to the appropriate calendar, which must be - * @link https://php.net/manual/en/intldateformatter.setcalendar.php - * @param IntlDateFormatter $formatter $mf - * @param IntlCalendar|int|null $calendar

- * The calendar to use. - * Default is IntlDateFormatter::GREGORIAN. - *

- * @return bool TRUE on success or FALSE on failure. - */ -function datefmt_set_calendar(IntlDateFormatter $formatter, IntlCalendar|int|null $calendar): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the locale used by formatter - * @link https://php.net/manual/en/intldateformatter.getlocale.php - * @param IntlDateFormatter $formatter - * @param int $type [optional] - * @return string|false the locale of this formatter or 'false' if error - */ -function datefmt_get_locale( - IntlDateFormatter $formatter, - int $type = ULOC_ACTUAL_LOCALE -): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the timezone-id used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.gettimezoneid.php - * @param IntlDateFormatter $formatter - * @return string|false ID string for the time zone used by this formatter. - */ -function datefmt_get_timezone_id(IntlDateFormatter $formatter): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 3.0.0)
- * Get copy of formatter's calendar object - * @link https://secure.php.net/manual/en/intldateformatter.getcalendarobject.php - * @param IntlDateFormatter $formatter - * @return IntlCalendar|false|null A copy of the internal calendar object used by this formatter. - */ -function datefmt_get_calendar_object(IntlDateFormatter $formatter): IntlCalendar|false|null {} - -/** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Get formatter's timezone - * @link https://secure.php.net/manual/en/intldateformatter.gettimezone.php - * @param IntlDateFormatter $formatter - * @return IntlTimeZone|false The associated IntlTimeZone object or FALSE on failure. - */ -function datefmt_get_timezone(IntlDateFormatter $formatter): IntlTimeZone|false {} -/** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Sets formatter's timezone - * @link https://php.net/manual/en/intldateformatter.settimezone.php - * @param IntlDateFormatter $formatter - * @param IntlTimeZone|DateTimeZone|string|null $timezone

- * The timezone to use for this formatter. This can be specified in the - * following forms: - *

    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link "https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone" date.timezone} or - * through the function {@link "https://secure.php.net/manual/en/function.date-default-timezone-set.php" date_default_timezone_set()} and as - * returned by {@link "https://secure.php.net/manual/en/function.date-default-timezone-get.php" date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link "https://secure.php.net/manual/en/class.intltimezone.php" IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link "https://secure.php.net/manual/en/class.datetimezone.php" DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link "https://secure.php.net/manual/en/language.types.string.php" string}, which should be a valid ICU timezone identifier. - * See IntlTimeZone::createTimeZoneIDEnumeration(). Raw offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- *

- * @return bool|null TRUE on success or FALSE on failure. - */ -function datefmt_set_timezone(IntlDateFormatter $formatter, $timezone): bool|null {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the pattern used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.getpattern.php - * @param IntlDateFormatter $formatter - * @return string|false The pattern string being used to format/parse. - */ -function datefmt_get_pattern(IntlDateFormatter $formatter): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the pattern used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.setpattern.php - * @param IntlDateFormatter $formatter - * @param string $pattern

- * New pattern string to use. - * Possible patterns are documented at http://userguide.icu-project.org/formatparse/datetime. - *

- * @return bool TRUE on success or FALSE on failure. - * Bad formatstrings are usually the cause of the failure. - */ -function datefmt_set_pattern(IntlDateFormatter $formatter, string $pattern): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the lenient used for the IntlDateFormatter - * @link https://php.net/manual/en/intldateformatter.islenient.php - * @param IntlDateFormatter $formatter - * @return bool TRUE if parser is lenient, FALSE if parser is strict. By default the parser is lenient. - */ -function datefmt_is_lenient(IntlDateFormatter $formatter): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Set the leniency of the parser - * @link https://php.net/manual/en/intldateformatter.setlenient.php - * @param IntlDateFormatter $formatter - * @param bool $lenient

- * Sets whether the parser is lenient or not, default is TRUE (lenient). - *

- * @return void - */ -function datefmt_set_lenient( - IntlDateFormatter $formatter, - bool $lenient -): void {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Format the date/time value as a string - * @link https://php.net/manual/en/intldateformatter.format.php - * @param IntlDateFormatter $formatter - * @param object|array|string|int|float $datetime

- * Value to format. This may be a DateTime object, - * an integer representing a Unix timestamp value (seconds - * since epoch, UTC) or an array in the format output by - * localtime. - *

- * @return string|false The formatted string or, if an error occurred, FALSE. - */ -function datefmt_format( - IntlDateFormatter $formatter, - $datetime -): string|false {} - -/** - * (PHP 5 >= 5.5.0, PECL intl >= 3.0.0)
- * Formats an object - * @link https://secure.php.net/manual/en/intldateformatter.formatobject.php - * @param IntlCalendar|DateTimeInterface $datetime

- * An object of type IntlCalendar or DateTime. The timezone information in the object will be used. - *

- * @param array|int|string|null $format [optional]

- * How to format the date/time. This can either be an {https://secure.php.net/manual/en/language.types.array.php array} with - * two elements (first the date style, then the time style, these being one - * of the constants IntlDateFormatter::NONE, - * IntlDateFormatter::SHORT, - * IntlDateFormatter::MEDIUM, - * IntlDateFormatter::LONG, - * IntlDateFormatter::FULL), a long with - * the value of one of these constants (in which case it will be used both - * for the time and the date) or a {@link https://secure.php.net/manual/en/language.types.string.php} with the format - * described in {@link http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details the ICU documentation} - * documentation. If NULL, the default style will be used. - *

- * @param string|null $locale [optional]

- * The locale to use, or NULL to use the default one.

- * @return string|false The formatted string or, if an error occurred, FALSE. - */ -function datefmt_format_object($datetime, $format = null, ?string $locale = null): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse string to a timestamp value - * @link https://php.net/manual/en/intldateformatter.parse.php - * @param IntlDateFormatter $formatter - * @param string $string

- * string to convert to a time - *

- * @param int &$offset [optional]

- * Position at which to start the parsing in $value (zero-based). - * If no error occurs before $value is consumed, $parse_pos will contain -1 - * otherwise it will contain the position at which parsing ended (and the error occurred). - * This variable will contain the end position if the parse fails. - * If $parse_pos > strlen($value), the parse fails immediately. - *

- * @return int|float|false timestamp parsed value - */ -function datefmt_parse(IntlDateFormatter $formatter, string $string, &$offset = null): int|float|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Parse string to a field-based time value - * @link https://php.net/manual/en/intldateformatter.localtime.php - * @param IntlDateFormatter $formatter - * @param string $string

- * string to convert to a time - *

- * @param int &$offset [optional]

- * Position at which to start the parsing in $value (zero-based). - * If no error occurs before $value is consumed, $parse_pos will contain -1 - * otherwise it will contain the position at which parsing ended . - * If $parse_pos > strlen($value), the parse fails immediately. - *

- * @return array|false Localtime compatible array of integers : contains 24 hour clock value in tm_hour field - */ -function datefmt_localtime(IntlDateFormatter $formatter, string $string, &$offset = null): array|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error code from last operation - * @link https://php.net/manual/en/intldateformatter.geterrorcode.php - * @param IntlDateFormatter $formatter - * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR. - */ -function datefmt_get_error_code(IntlDateFormatter $formatter): int {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the error text from the last operation. - * @link https://php.net/manual/en/intldateformatter.geterrormessage.php - * @param IntlDateFormatter $formatter - * @return string Description of the last error. - */ -function datefmt_get_error_message(IntlDateFormatter $formatter): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get string length in grapheme units - * @link https://php.net/manual/en/function.grapheme-strlen.php - * @param string $string

- * The string being measured for length. It must be a valid UTF-8 string. - *

- * @return int|false|null The length of the string on success, and 0 if the string is empty. - */ -function grapheme_strlen(string $string): int|false|null {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Find position (in grapheme units) of first occurrence of a string - * @link https://php.net/manual/en/function.grapheme-strpos.php - * @param string $haystack

- * The string to look in. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param int $offset [optional]

- * The optional $offset parameter allows you to specify where in $haystack to - * start searching as an offset in grapheme units (not bytes or characters). - * The position returned is still relative to the beginning of haystack - * regardless of the value of $offset. - *

- * @return int|false the position as an integer. If needle is not found, strpos() will return boolean FALSE. - */ -function grapheme_strpos(string $haystack, string $needle, int $offset = 0): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Find position (in grapheme units) of first occurrence of a case-insensitive string - * @link https://php.net/manual/en/function.grapheme-stripos.php - * @param string $haystack

- * The string to look in. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param int $offset [optional]

- * The optional $offset parameter allows you to specify where in haystack to - * start searching as an offset in grapheme units (not bytes or characters). - * The position returned is still relative to the beginning of haystack - * regardless of the value of $offset. - *

- * @return int|false the position as an integer. If needle is not found, grapheme_stripos() will return boolean FALSE. - */ -function grapheme_stripos(string $haystack, string $needle, int $offset = 0): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Find position (in grapheme units) of last occurrence of a string - * @link https://php.net/manual/en/function.grapheme-strrpos.php - * @param string $haystack

- * The string to look in. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param int $offset [optional]

- * The optional $offset parameter allows you to specify where in $haystack to - * start searching as an offset in grapheme units (not bytes or characters). - * The position returned is still relative to the beginning of haystack - * regardless of the value of $offset. - *

- * @return int|false the position as an integer. If needle is not found, grapheme_strrpos() will return boolean FALSE. - */ -function grapheme_strrpos(string $haystack, string $needle, int $offset = 0): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Find position (in grapheme units) of last occurrence of a case-insensitive string - * @link https://php.net/manual/en/function.grapheme-strripos.php - * @param string $haystack

- * The string to look in. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param int $offset [optional]

- * The optional $offset parameter allows you to specify where in $haystack to - * start searching as an offset in grapheme units (not bytes or characters). - * The position returned is still relative to the beginning of haystack - * regardless of the value of $offset. - *

- * @return int|false the position as an integer. If needle is not found, grapheme_strripos() will return boolean FALSE. - */ -function grapheme_strripos(string $haystack, string $needle, int $offset = 0): int|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Return part of a string - * @link https://php.net/manual/en/function.grapheme-substr.php - * @param string $string

- * The input string. Must be valid UTF-8. - *

- * @param int $offset

- * Start position in default grapheme units. - * If $start is non-negative, the returned string will start at the - * $start'th position in $string, counting from zero. If $start is negative, - * the returned string will start at the $start'th grapheme unit from the - * end of string. - *

- * @param int|null $length [optional]

- * Length in grapheme units. - * If $length is given and is positive, the string returned will contain - * at most $length grapheme units beginning from $start (depending on the - * length of string). If $length is given and is negative, then - * that many grapheme units will be omitted from the end of string (after the - * start position has been calculated when a start is negative). If $start - * denotes a position beyond this truncation, FALSE will be returned. - *

- * @return string|false

the extracted part of $string,
- * or FALSE if $length is negative and $start denotes a position beyond truncation $length,
- * or also FALSE if $start denotes a position beyond $string length

- */ -function grapheme_substr(string $string, int $offset, ?int $length = null): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns part of haystack string from the first occurrence of needle to the end of haystack. - * @link https://php.net/manual/en/function.grapheme-strstr.php - * @param string $haystack

- * The input string. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param bool $beforeNeedle [optional]

- * If TRUE, grapheme_strstr() returns the part of the - * haystack before the first occurrence of the needle (excluding the needle). - *

- * @return string|false the portion of string, or FALSE if needle is not found. - */ -function grapheme_strstr(string $haystack, string $needle, bool $beforeNeedle = false): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack. - * @link https://php.net/manual/en/function.grapheme-stristr.php - * @param string $haystack

- * The input string. Must be valid UTF-8. - *

- * @param string $needle

- * The string to look for. Must be valid UTF-8. - *

- * @param bool $beforeNeedle [optional]

- * If TRUE, grapheme_strstr() returns the part of the - * haystack before the first occurrence of the needle (excluding needle). - *

- * @return string|false the portion of $haystack, or FALSE if $needle is not found. - */ -function grapheme_stristr(string $haystack, string $needle, bool $beforeNeedle = false): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Function to extract a sequence of default grapheme clusters from a text buffer, which must be encoded in UTF-8. - * @link https://php.net/manual/en/function.grapheme-extract.php - * @param string $haystack

- * String to search. - *

- * @param int $size

- * Maximum number items - based on the $extract_type - to return. - *

- * @param int $type

- * Defines the type of units referred to by the $size parameter: - *

- *

- * GRAPHEME_EXTR_COUNT (default) - $size is the number of default - * grapheme clusters to extract. - * GRAPHEME_EXTR_MAXBYTES - $size is the maximum number of bytes - * returned. - * GRAPHEME_EXTR_MAXCHARS - $size is the maximum number of UTF-8 - * characters returned. - *

- * @param int $offset [optional]

- * Starting position in $haystack in bytes - if given, it must be zero or a - * positive value that is less than or equal to the length of $haystack in - * bytes. If $start does not point to the first byte of a UTF-8 - * character, the start position is moved to the next character boundary. - *

- * @param int &$next [optional]

- * Reference to a value that will be set to the next starting position. - * When the call returns, this may point to the first byte position past the end of the string. - *

- * @return string|false A string starting at offset $start and ending on a default grapheme cluster - * boundary that conforms to the $size and $extract_type specified. - */ -function grapheme_extract(string $haystack, int $size, int $type = 0, int $offset = 0, &$next = null): string|false {} - -/** - * (PHP 5 >= 5.3.0, PHP 7, PECL intl >= 1.0.2, PHP 7, PECL idn >= 0.1)
- * Convert domain name to IDNA ASCII form. - * @link https://php.net/manual/en/function.idn-to-ascii.php - * @param string $domain

- * Domain to convert. In PHP 5 must be UTF-8 encoded. - * If e.g. an ISO-8859-1 (aka Western Europe latin1) encoded string is - * passed it will be converted into an ACE encoded "xn--" string. - * It will not be the one you expected though! - *

- * @param int $flags [optional]

- * Conversion options - combination of IDNA_* constants (except IDNA_ERROR_* constants). - *

- * @param int $variant [optional]

- * Either INTL_IDNA_VARIANT_2003 for IDNA 2003 or INTL_IDNA_VARIANT_UTS46 for UTS #46. - *

- * @param array &$idna_info [optional]

- * This parameter can be used only if INTL_IDNA_VARIANT_UTS46 was used for variant. - * In that case, it will be filled with an array with the keys 'result', - * the possibly illegal result of the transformation, 'isTransitionalDifferent', - * a boolean indicating whether the usage of the transitional mechanisms of UTS #46 - * either has or would have changed the result and 'errors', - * which is an int representing a bitset of the error constants IDNA_ERROR_*. - *

- * @return string|false The ACE encoded version of the domain name or FALSE on failure. - */ -function idn_to_ascii(string $domain, int $flags = 0, int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info): string|false {} - -/** - * (PHP 5 >= 5.3.0, PHP 7, PECL intl >= 1.0.2, PHP 7, PECL idn >= 0.1)
- * Convert domain name from IDNA ASCII to Unicode. - * @link https://php.net/manual/en/function.idn-to-utf8.php - * @param string $domain

- * Domain to convert in IDNA ASCII-compatible format. - * The ASCII encoded domain name. Looks like "xn--..." if the it originally contained non-ASCII characters. - *

- * @param int $flags [optional]

- * Conversion options - combination of IDNA_* constants (except IDNA_ERROR_* constants). - *

- * @param int $variant [optional]

- * Either INTL_IDNA_VARIANT_2003 for IDNA 2003 or INTL_IDNA_VARIANT_UTS46 for UTS #46. - *

- * @param array &$idna_info [optional]

- * This parameter can be used only if INTL_IDNA_VARIANT_UTS46 was used for variant. - * In that case, it will be filled with an array with the keys 'result', - * the possibly illegal result of the transformation, 'isTransitionalDifferent', - * a boolean indicating whether the usage of the transitional mechanisms of UTS #46 - * either has or would have changed the result and 'errors', - * which is an int representing a bitset of the error constants IDNA_ERROR_*. - *

- * @return string|false The UTF-8 encoded version of the domain name or FALSE on failure. - * RFC 3490 4.2 states though "ToUnicode never fails. If any step fails, then the original input - * sequence is returned immediately in that step." - */ -function idn_to_utf8( - string $domain, - int $flags = 0, - int $variant = INTL_IDNA_VARIANT_UTS46, - &$idna_info = null, -): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a new IntlCalendar - * @link https://secure.php.net/manual/en/intlcalendar.createinstance.php - * @param IntlTimeZone|DateTimeZone|string|null $timezone [optional]

- * The timezone to use. - *

- * - *
    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone date.timezone} or - * through the function {@link https://secure.php.net/manual/en/function.date-default-timezone-set.php date_default_timezone_set()} and as - * returned by {@link https://secure.php.net/manual/en/function.date-default-timezone-get.php date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/class.datetimezone.php DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/language.types.string.php string}, which should be a valid ICU timezone identifier. - * See IntlTimeZone::createTimeZoneIDEnumeration(). Raw - * offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- *

- * @param string|null $locale [optional]

- * A locale to use or NULL to use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.default-locale the default locale}. - *

- * @return IntlCalendar|null - * The created {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} instance or NULL on - * failure. - * @since 5.5 - */ -function intlcal_create_instance($timezone = null, ?string $locale = null): ?IntlCalendar {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get set of locale keyword values - * @param string $keyword

- * The locale keyword for which relevant values are to be queried. Only - * 'calendar' is supported. - *

- * @param string $locale

- * The locale onto which the keyword/value pair are to be appended. - *

- * @param bool $onlyCommon - *

- * Whether to show only the values commonly used for the specified locale. - *

- * @return IntlIterator|false An iterator that yields strings with the locale keyword values or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_keyword_values_for_locale(string $keyword, string $locale, bool $onlyCommon): IntlIterator|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get number representing the current time - * @link https://secure.php.net/manual/en/intlcalendar.getnow.php - * @return float A float representing a number of milliseconds since the epoch, not counting leap seconds. - * @since 5.5 - */ -function intlcal_get_now(): float {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get array of locales for which there is data - * @link https://secure.php.net/manual/en/intlcalendar.getavailablelocales.php - * @return string[] An array of strings, one for which locale. - * @since 5.5 - */ -function intlcal_get_available_locales(): array {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the value for a field - * @link https://secure.php.net/manual/en/intlcalendar.get.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int An integer with the value of the time field. - * @since 5.5 - */ -function intl_get($calendar, $field) {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get time currently represented by the object - * @param IntlCalendar $calendar

The calendar whose time will be checked against this object's time.

- * @return float - * A {@link https://secure.php.net/manual/en/language.types.float.php float} representing the number of milliseconds elapsed since the - * reference time (1 Jan 1970 00:00:00 UTC). - * @since 5.5 - */ -function intlcal_get_time(IntlCalendar $calendar): float|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the calendar time in milliseconds since the epoch - * @link https://secure.php.net/manual/en/intlcalendar.settime.php - * @param IntlCalendar $calendar

- * The IntlCalendar resource. - *

- * @param float $timestamp

- * An instant represented by the number of number of milliseconds between - * such instant and the epoch, ignoring leap seconds. - *

- * @return bool - * Returns TRUE on success and FALSE on failure. - * @since 5.5 - */ -function intlcal_set_time(IntlCalendar $calendar, float $timestamp): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Add a (signed) amount of time to a field - * @link https://secure.php.net/manual/en/intlcalendar.add.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param int $value

The signed amount to add to the current field. If the amount is positive, the instant will be moved forward; if it is negative, the instant wil be moved into the past. The unit is implicit to the field type. - * For instance, hours for IntlCalendar::FIELD_HOUR_OF_DAY.

- * @return bool Returns TRUE on success or FALSE on failure. - * @since 5.5 - */ -function intlcal_add(IntlCalendar $calendar, int $field, int $value): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the timezone used by this calendar - * @link https://secure.php.net/manual/en/intlcalendar.settimezone.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param IntlTimeZone|DateTimeZone|string|null $timezone

- * The new timezone to be used by this calendar. It can be specified in the - * following ways: - * - *

    - *
  • - *

    - * NULL, in which case the default timezone will be used, as specified in - * the ini setting {@link https://secure.php.net/manual/en/datetime.configuration.php#ini.date.timezone date.timezone} or - * through the function {@link https://secure.php.net/manual/en/function.date-default-timezone-set.php date_default_timezone_set()} and as - * returned by {@link https://secure.php.net/manual/en/function.date-default-timezone-get.php date_default_timezone_get()}. - *

    - *
  • - *
  • - *

    - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone}, which will be used directly. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/class.datetimezone.php DateTimeZone}. Its identifier will be extracted - * and an ICU timezone object will be created; the timezone will be backed - * by ICU's database, not PHP's. - *

    - *
  • - *
  • - *

    - * A {@link https://secure.php.net/manual/en/language.types.string.php string}, which should be a valid ICU timezone identifier. - * See IntlTimeZone::createTimeZoneIDEnumeration(). Raw - * offsets such as "GMT+08:30" are also accepted. - *

    - *
  • - *
- * @return bool Returns TRUE on success and FALSE on failure. - * @since 5.5 - */ -function intlcal_set_time_zone(IntlCalendar $calendar, $timezone): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether this object's time is after that of the passed object - * https://secure.php.net/manual/en/intlcalendar.after.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param IntlCalendar $other

The calendar whose time will be checked against this object's time.

- * @return bool - * Returns TRUE if this object's current time is after that of the - * calendar argument's time. Returns FALSE otherwise. - * Also returns FALSE on failure. You can use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to detect error conditions. - * @since 5.5 - */ -function intlcal_after(IntlCalendar $calendar, IntlCalendar $other): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether this object's time is before that of the passed object - * @link https://secure.php.net/manual/en/intlcalendar.before.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param IntlCalendar $other

The calendar whose time will be checked against this object's time.

- * @return bool - *

- * Returns TRUE if this object's current time is before that of the - * calendar argument's time. Returns FALSE otherwise. - * Also returns FALSE on failure. You can use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to detect error conditions. - *

- * @since 5.5 - */ -function intlcal_before(IntlCalendar $calendar, IntlCalendar $other): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set a time field or several common fields at once - * @link https://secure.php.net/manual/en/intlcalendar.set.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $year

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param int $month

- * The new value for IntlCalendar::FIELD_MONTH. - *

- * @param int $dayOfMonth [optional]

- * The new value for IntlCalendar::FIELD_DAY_OF_MONTH. - * The month sequence is zero-based, i.e., January is represented by 0, - * February by 1, ..., December is 11 and Undecember (if the calendar has - * it) is 12. - *

- * @param int $hour [optional] - *

- * The new value for IntlCalendar::FIELD_HOUR_OF_DAY. - *

- * @param int $minute [optional] - *

- * The new value for IntlCalendar::FIELD_MINUTE. - *

- * @param int $second [optional]

- * The new value for IntlCalendar::FIELD_SECOND. - *

- * @return bool Returns TRUE on success and FALSE on failure. - * @since 5.5 - */ -function intlcal_set(IntlCalendar $calendar, int $year, int $month, int $dayOfMonth, int $hour, int $minute, int $second): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Add value to field without carrying into more significant fields - * @link https://secure.php.net/manual/en/intlcalendar.roll.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

One of the - * {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time - * {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @param int|bool $value

- * The (signed) amount to add to the field, TRUE for rolling up (adding - * 1), or FALSE for rolling down (subtracting - * 1). - *

- * @return bool Returns TRUE on success or FALSE on failure. - * @since 5.5 - */ -function intlcal_roll( - IntlCalendar $calendar, - int $field, - $value -): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Clear a field or all fields - * @link https://secure.php.net/manual/en/intlcalendar.clear.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int|null $field [optional]

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return bool Returns TRUE on success or FALSE on failure. Failure can only occur is invalid arguments are provided. - * @since 5.5 - */ -function intlcal_clear(IntlCalendar $calendar, ?int $field = null): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Calculate difference between given time and this object's time - * @link https://secure.php.net/manual/en/intlcalendar.fielddifference.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param float $timestamp

- * The time against which to compare the quantity represented by the - * field. For the result to be positive, the time - * given for this parameter must be ahead of the time of the object the - * method is being invoked on. - *

- * @param int $field

- * The field that represents the quantity being compared. - *

- * - *

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int Returns a (signed) difference of time in the unit associated with the - * specified field or FALSE on failure. - * @since 5.5 - */ -function intlcal_field_difference(IntlCalendar $calendar, float $timestamp, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * The maximum value for a field, considering the object's current time - * @link https://secure.php.net/manual/en/intlcalendar.getactualmaximum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing the maximum value in the units associated - * with the given field or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_actual_maximum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * The minimum value for a field, considering the object's current time - * @link https://secure.php.net/manual/en/intlcalendar.getactualminimum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. - * These are integer values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing the minimum value in the field's - * unit or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_actual_minimum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @link https://secure.php.net/manual/en/intlcalendar.getdayofweektype.php - * Tell whether a day is a weekday, weekend or a day that has a transition between the two - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return int - * Returns one of the constants - * IntlCalendar::DOW_TYPE_WEEKDAY, - * IntlCalendar::DOW_TYPE_WEEKEND, - * IntlCalendar::DOW_TYPE_WEEKEND_OFFSET or - * IntlCalendar::DOW_TYPE_WEEKEND_CEASE or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_day_of_week_type(IntlCalendar $calendar, int $dayOfWeek): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the first day of the week for the calendar's locale - * @link https://secure.php.net/manual/en/intlcalendar.getfirstdayofweek.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return int - * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_first_day_of_week(IntlCalendar $calendar): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the largest local minimum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getgreatestminimum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT.

- * @return int - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing a field value, in the field's - * unit, or FALSE on failure. - * @since 5.5 - */ -function intlcal_greates_minimum($calendar, $field) {} - -/** - * (PHP >= 5.5.0, PECL intl >= 3.0.0a1)
- * Gets the value for a specific field. - * @link https://www.php.net/manual/en/intlcalendar.get.php - * @param IntlCalendar $calendar

- * The IntlCalendar resource. - *

- * @param int $field

- * One of the IntlCalendar date/time field constants. These are integer values between 0 and IntlCalendar::FIELD_COUNT. - *

- * @return int An integer with the value of the time field. - */ -function intlcal_get(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the smallest local maximum for a field - * @link https://secure.php.net/manual/en/intlcalendar.getleastmaximum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int - *

An {@link https://secure.php.net/manual/en/language.types.integer.ph int} representing a field value in the field's - * unit or FALSE on failure. - *

- * @since 5.5 - */ -function intlcal_get_least_maximum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the largest local minimum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getgreatestminimum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT.

- * @return int - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing a field value, in the field's - * unit, or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_greatest_minimum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the locale associated with the object - * @link https://secure.php.net/manual/en/intlcalendar.getlocale.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $type

- * Whether to fetch the actual locale (the locale from which the calendar - * data originates, with Locale::ACTUAL_LOCALE) or the - * valid locale, i.e., the most specific locale supported by ICU relatively - * to the requested locale – see Locale::VALID_LOCALE. - * From the most general to the most specific, the locales are ordered in - * this fashion – actual locale, valid locale, requested locale. - *

- * @return string - * A locale string or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_locale(IntlCalendar $calendar, int $type): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the global maximum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getmaximum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * @since 5.5 - */ -function intcal_get_maximum($calendar, $field) {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @link https://secure.php.net/manual/en/intlcalendar.getminimaldaysinfirstweek.php - * Get minimal number of days the first week in a year or month can have - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return int - * An {@link https://secure.php.net/manual/en/language.types.integer.php int} representing a number of days or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_minimal_days_in_first_week(IntlCalendar $calendar): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the global minimum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getminimum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int - * An int representing a value for the given field in the field's unit or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_minimum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the object's timezone - * @link https://secure.php.net/manual/en/intlcalendar.gettimezone.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return IntlTimeZone|false - * An {@link https://secure.php.net/manual/en/class.intltimezone.php IntlTimeZone} object corresponding to the one used - * internally in this object. - * @since 5.5 - */ -function intlcal_get_time_zone(IntlCalendar $calendar): IntlTimeZone|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the calendar type - * @link https://secure.php.net/manual/en/intlcalendar.gettype.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return string - * A {@link https://secure.php.net/manual/en/language.types.string.php string} representing the calendar type, such as - * 'gregorian', 'islamic', etc. - * @since 5.5 - */ -function intlcal_get_type(IntlCalendar $calendar): string {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get time of the day at which weekend begins or ends - * @link https://secure.php.net/manual/en/intlcalendar.getweekendtransition.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return int - * The number of milliseconds into the day at which the the weekend begins or - * ends or FALSE on failure. - * @since 5.5 - */ -function intlcal_get_weekend_transition(IntlCalendar $calendar, int $dayOfWeek): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether the object's time is in Daylight Savings Time - * @link https://secure.php.net/manual/en/intlcalendar.indaylighttime.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return bool - * Returns TRUE if the date is in Daylight Savings Time, FALSE otherwise. - * The value FALSE may also be returned on failure, for instance after - * specifying invalid field values on non-lenient mode; use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or query - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to disambiguate. - * @since 5.5 - */ -function intlcal_in_daylight_time(IntlCalendar $calendar): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether date/time interpretation is in lenient mode - * @link https://secure.php.net/manual/en/intlcalendar.islenient.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return bool - * A {@link https://secure.php.net/manual/en/language.types.boolean.php bool} representing whether the calendar is set to lenient mode. - * @since 5.5 - */ -function intlcal_is_lenient(IntlCalendar $calendar): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether a field is set - * @link https://secure.php.net/manual/en/intlcalendar.isset.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return bool Assuming there are no argument errors, returns TRUE iif the field is set. - * @since 5.5 - */ -function intlcal_is_set(IntlCalendar $calendar, int $field): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the global maximum value for a field - * @link https://secure.php.net/manual/en/intlcalendar.getmaximum.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $field

- * One of the {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} date/time {@link https://secure.php.net/manual/en/class.intlcalendar.php#intlcalendar.constants field constants}. These are integer - * values between 0 and - * IntlCalendar::FIELD_COUNT. - *

- * @return int|false - * @since 5.5 - */ -function intlcal_get_maximum(IntlCalendar $calendar, int $field): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether another calendar is equal but for a different time - * @link https://secure.php.net/manual/en/intlcalendar.isequivalentto.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param IntlCalendar $other The other calendar against which the comparison is to be made. - * @return bool - * Assuming there are no argument errors, returns TRUE iif the calendars are equivalent except possibly for their set time. - * @since 5.5 - */ -function intlcal_is_equivalent_to(IntlCalendar $calendar, IntlCalendar $other): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Whether a certain date/time is in the weekend - * @link https://secure.php.net/manual/en/intlcalendar.isweekend.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param float|null $timestamp [optional]

- * An optional timestamp representing the number of milliseconds since the - * epoch, excluding leap seconds. If NULL, this object's current time is - * used instead. - *

- * @return bool - *

A {@link https://secure.php.net/manual/en/language.types.boolean.php bool} indicating whether the given or this object's time occurs - * in a weekend. - *

- *

- * The value FALSE may also be returned on failure, for instance after giving - * a date out of bounds on non-lenient mode; use {@link https://secure.php.net/manual/en/intl.configuration.php#ini.intl.use-exceptions exceptions} or query - * {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()} to disambiguate.

- * @since 5.5 - */ -function intlcal_is_weekend(IntlCalendar $calendar, ?float $timestamp = null): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set the day on which the week is deemed to start - * @link https://secure.php.net/manual/en/intlcalendar.setfirstdayofweek.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $dayOfWeek

- * One of the constants IntlCalendar::DOW_SUNDAY, - * IntlCalendar::DOW_MONDAY, ..., - * IntlCalendar::DOW_SATURDAY. - *

- * @return bool Returns TRUE on success. Failure can only happen due to invalid parameters. - * @since 5.5 - */ -function intlcal_set_first_day_of_week(IntlCalendar $calendar, int $dayOfWeek): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set whether date/time interpretation is to be lenient - * @link https://secure.php.net/manual/en/intlcalendar.setlenient.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param bool $lenient

- * Use TRUE to activate the lenient mode; FALSE otherwise. - *

- * @return bool Returns TRUE on success. Failure can only happen due to invalid parameters. - * @since 5.5 - */ -function intlcal_set_lenient(IntlCalendar $calendar, bool $lenient): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get behavior for handling repeating wall time - * @link https://secure.php.net/manual/en/intlcalendar.getrepeatedwalltimeoption.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return int - * One of the constants IntlCalendar::WALLTIME_FIRST or - * IntlCalendar::WALLTIME_LAST. - * @since 5.5 - */ -function intlcal_get_repeated_wall_time_option(IntlCalendar $calendar): int {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Compare time of two IntlCalendar objects for equality - * @link https://secure.php.net/manual/en/intlcalendar.equals.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param IntlCalendar $other - * @return bool

- * Returns TRUE if the current time of both this and the passed in - * {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} object are the same, or FALSE - * otherwise. The value FALSE can also be returned on failure. This can only - * happen if bad arguments are passed in. In any case, the two cases can be - * distinguished by calling {@link https://secure.php.net/manual/en/function.intl-get-error-code.php intl_get_error_code()}. - *

- * @since 5.5 - */ -function intlcal_equals(IntlCalendar $calendar, IntlCalendar $other): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get behavior for handling skipped wall time - * @link https://secure.php.net/manual/en/intlcalendar.getskippedwalltimeoption.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return int - * One of the constants IntlCalendar::WALLTIME_FIRST, - * IntlCalendar::WALLTIME_LAST or - * IntlCalendar::WALLTIME_NEXT_VALID. - * @since 5.5 - */ -function intlcal_get_skipped_wall_time_option(IntlCalendar $calendar): int {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set behavior for handling repeating wall times at negative timezone offset transitions - * @link https://secure.php.net/manual/en/intlcalendar.setrepeatedwalltimeoption.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $option

- * One of the constants IntlCalendar::WALLTIME_FIRST or - * IntlCalendar::WALLTIME_LAST. - *

- * @return bool - * Returns TRUE on success. Failure can only happen due to invalid parameters. - * @since 5.5 - */ -function intlcal_set_repeated_wall_time_option(IntlCalendar $calendar, int $option): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Set behavior for handling skipped wall times at positive timezone offset transitions - * @link https://secure.php.net/manual/en/intlcalendar.setskippedwalltimeoption.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @param int $option

- * One of the constants IntlCalendar::WALLTIME_FIRST, - * IntlCalendar::WALLTIME_LAST or - * IntlCalendar::WALLTIME_NEXT_VALID. - *

- * @return bool - *

- * Returns TRUE on success. Failure can only happen due to invalid parameters. - *

- * @since 5.5 - */ -function intlcal_set_skipped_wall_time_option(IntlCalendar $calendar, int $option): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a2)
- * Create an IntlCalendar from a DateTime object or string - * @link https://secure.php.net/manual/en/intlcalendar.fromdatetime.php - * @param DateTime|string $datetime

- * A {@link https://secure.php.net/manual/en/class.datetime.php DateTime} object or a {@link https://secure.php.net/manual/en/language.types.string.php string} that - * can be passed to {@link https://secure.php.net/manual/en/datetime.construct.php DateTime::__construct()}. - *

- * @param null|string $locale - * @return IntlCalendar|null - * The created {@link https://secure.php.net/manual/en/class.intlcalendar.php IntlCalendar} object or NULL in case of - * failure. If a {@link https://secure.php.net/manual/en/language.types.string.php string} is passed, any exception that occurs - * inside the {@link https://secure.php.net/manual/en/class.datetime.php DateTime} constructor is propagated. - * @since 5.5 - */ -function intlcal_from_date_time( - DateTime|string $datetime, - ?string $locale = null -): ?IntlCalendar {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a2)
- * Convert an IntlCalendar into a DateTime object - * @link https://secure.php.net/manual/en/intlcalendar.todatetime.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return DateTime|false - * A {@link https://secure.php.net/manual/en/class.datetime.php DateTime} object with the same timezone as this - * object (though using PHP's database instead of ICU's) and the same time, - * except for the smaller precision (second precision instead of millisecond). - * Returns FALSE on failure. - * @since 5.5 - */ -function intlcal_to_date_time(IntlCalendar $calendar): DateTime|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error code on the object - * @link https://secure.php.net/manual/en/intlcalendar.geterrorcode.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return int|false An ICU error code indicating either success, failure or a warning. - * @since 5.5 - */ -function intlcal_get_error_code(IntlCalendar $calendar): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error message on the object - * @link https://secure.php.net/manual/en/intlcalendar.geterrormessage.php - * @param IntlCalendar $calendar

- * The calendar object, on the procedural style interface. - *

- * @return string|false The error message associated with last error that occurred in a function call on this object, or a string indicating the non-existance of an error. - * @since 5.5 - */ -function intlcal_get_error_message(IntlCalendar $calendar): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the number of IDs in the equivalency group that includes the given ID - * @link https://secure.php.net/manual/en/intltimezone.countequivalentids.php - * @param string $timezoneId - * @return int|false - * @since 5.5 - */ -function intltz_count_equivalent_ids(string $timezoneId): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create a new copy of the default timezone for this host - * @link https://secure.php.net/manual/en/intltimezone.createdefault.php - * @return IntlTimeZone - * @since 5.5 - */ -function intlz_create_default() {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @link https://secure.php.net/manual/en/intltimezone.createenumeration.php - * @param IntlTimeZone|string|int|float|null $countryOrRawOffset [optional] - * @return IntlIterator|false - * @since 5.5 - */ -function intltz_create_enumeration($countryOrRawOffset): IntlIterator|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @link https://secure.php.net/manual/en/intltimezone.createtimezone.php - * @param string $timezoneId - * @return IntlTimeZone|null - * @since 5.5 - */ -function intltz_create_time_zone(string $timezoneId): ?IntlTimeZone {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @link https://secure.php.net/manual/en/intltimezone.fromdatetimezone.php - * @param DateTimeZone $timezone - * @return IntlTimeZone|null - * @since 5.5 - */ -function intltz_from_date_time_zone(DateTimeZone $timezone): ?IntlTimeZone {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the canonical system timezone ID or the normalized custom time zone ID for the given time zone ID - * @link https://secure.php.net/manual/en/intltimezone.getcanonicalid.php - * @param string $timezoneId - * @param bool &$isSystemId [optional] - * @return string|false - * @since 5.5 - */ -function intltz_get_canonical_id(string $timezoneId, &$isSystemId): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get a name of this time zone suitable for presentation to the user - * @param IntlTimeZone $timezone -

- * The time zone object, on the procedural style interface. - *

- * @param bool $dst [optional] - * @param int $style [optional] - * @param string|null $locale [optional] - * @return string|false - * @since 5.5 - */ -function intltz_get_display_name(IntlTimeZone $timezone, bool $dst = false, int $style = 2, ?string $locale): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the amount of time to be added to local standard time to get local wall clock time - * @param IntlTimeZone $timezone -

- * The time zone object, on the procedural style interface. - *

- * @return int - * @link https://secure.php.net/manual/en/intltimezone.getequivalentid.php - * @since 5.5 - */ -function intltz_get_dst_savings(IntlTimeZone $timezone): int {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get an ID in the equivalency group that includes the given ID - * @link https://secure.php.net/manual/en/intltimezone.getequivalentid.php - * @param string $timezoneId - * @param int $offset - * @return string|false - * @since 5.5 - */ -function intltz_get_equivalent_id(string $timezoneId, int $offset): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error code on the object - * @link https://secure.php.net/manual/en/intltimezone.geterrorcode.php - * @param IntlTimeZone $timezone -

- * The time zone object, on the procedural style interface. - *

- * @return int|false - * @since 5.5 - */ -function intltz_get_error_code(IntlTimeZone $timezone): int|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get last error message on the object - * @link https://secure.php.net/manual/en/intltimezone.geterrormessage.php - * @param IntlTimeZone $timezone -

- * The time zone object, on the procedural style interface. - *

- * @return string|false - * @since 5.5 - */ -function intltz_get_error_message(IntlTimeZone $timezone): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Create GMT (UTC) timezone - * @link https://secure.php.net/manual/en/intltimezone.getgmt.php - * @return IntlTimeZone - * @since 5.5 - */ -function intltz_getGMT(): IntlTimeZone {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get timezone ID - * @link https://secure.php.net/manual/en/intltimezone.getid.php - * @param IntlTimeZone $timezone - * @return string|false - * @since 5.5 - */ -function intltz_get_id(IntlTimeZone $timezone): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the time zone raw and GMT offset for the given moment in time - * @link https://secure.php.net/manual/en/intltimezone.getoffset.php - * @param IntlTimeZone $timezone - * @param float $timestamp - * @param bool $local - * @param int &$rawOffset - * @param int &$dstOffset - * @return bool - * @since 5.5 - */ -function intltz_get_offset(IntlTimeZone $timezone, float $timestamp, bool $local, &$rawOffset, &$dstOffset): bool {} - -/** - * Get the raw GMT offset (before taking daylight savings time into account - * @link https://secure.php.net/manual/en/intltimezone.getrawoffset.php - * @param IntlTimeZone $timezone - * @return int - */ -function intltz_get_raw_offset(IntlTimeZone $timezone): int {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Get the timezone data version currently used by ICU - * @link https://secure.php.net/manual/en/intltimezone.gettzdataversion.php - * @return string|false - * @since 5.5 - */ -function intltz_get_tz_data_version(): string|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Check if this zone has the same rules and offset as another zone - * @link https://secure.php.net/manual/en/intltimezone.hassamerules.php - * @param IntlTimeZone $timezone - * @param IntlTimeZone $other - * @return bool - * @since 5.5 - */ -function intltz_has_same_rules( - IntlTimeZone $timezone, - IntlTimeZone $other -): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Convert to DateTimeZone object - * @link https://secure.php.net/manual/en/intltimezone.todatetimezone.php - * @param IntlTimeZone $timezone - * @return DateTimeZone|false - * @since 5.5 - */ -function intltz_to_date_time_zone(IntlTimeZone $timezone): DateTimeZone|false {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * Check if this time zone uses daylight savings time - * @link https://secure.php.net/manual/en/intltimezone.usedaylighttime.php - * @param IntlTimeZone $timezone - * @return bool - * @since 5.5 - */ -function intltz_use_daylight_time(IntlTimeZone $timezone): bool {} - -/** - * (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)
- * @param DateTimeZone|IntlTimeZone|string|int|null $timezoneOrYear [optional] - * @param string|null $localeOrMonth [optional] - * @param int $day [optional] - * @param int $hour [optional] - * @param int $minute [optional] - * @param int $second [optional] - * @return IntlGregorianCalendar|null - * @since 5.5 - */ -function intlgregcal_create_instance($timezoneOrYear, $localeOrMonth, $day, $hour, $minute, $second): ?IntlGregorianCalendar {} - -/** - * @param IntlGregorianCalendar $calendar - * @param float $timestamp - * @return bool - */ -function intlgregcal_set_gregorian_change(IntlGregorianCalendar $calendar, float $timestamp): bool {} - -/** - * @param IntlGregorianCalendar $calendar - * @return float - */ -function intlgregcal_get_gregorian_change(IntlGregorianCalendar $calendar): float {} - -/** - * @param IntlGregorianCalendar $calendar - * @param int $year - * @return bool - */ -function intlgregcal_is_leap_year(IntlGregorianCalendar $calendar, int $year): bool {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Create a resource bundle - * @link https://php.net/manual/en/resourcebundle.create.php - * @param string|null $locale

- * Locale for which the resources should be loaded (locale name, e.g. en_CA). - *

- * @param string|null $bundle

- * The directory where the data is stored or the name of the .dat file. - *

- * @param bool $fallback [optional]

- * Whether locale should match exactly or fallback to parent locale is allowed. - *

- * @return ResourceBundle|null ResourceBundle object or NULL on error. - */ -function resourcebundle_create(?string $locale, ?string $bundle, bool $fallback = true): ?ResourceBundle {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get data from the bundle - * @link https://php.net/manual/en/resourcebundle.get.php - * @param ResourceBundle $bundle - * @param string|int $index

- * Data index, must be string or integer. - *

- * @param bool $fallback - * @return mixed the data located at the index or NULL on error. Strings, integers and binary data strings - * are returned as corresponding PHP types, integer array is returned as PHP array. Complex types are - * returned as ResourceBundle object. - */ -function resourcebundle_get(ResourceBundle $bundle, string|int $index, bool $fallback = true) {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get number of elements in the bundle - * @link https://php.net/manual/en/resourcebundle.count.php - * @param ResourceBundle $bundle - * @return int number of elements in the bundle. - */ -function resourcebundle_count(ResourceBundle $bundle): int {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get supported locales - * @link https://php.net/manual/en/resourcebundle.locales.php - * @param string $bundle

- * Path of ResourceBundle for which to get available locales, or - * empty string for default locales list. - *

- * @return array|false the list of locales supported by the bundle. - */ -function resourcebundle_locales(string $bundle): array|false {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get bundle's last error code. - * @link https://php.net/manual/en/resourcebundle.geterrorcode.php - * @param ResourceBundle $bundle - * @return int error code from last bundle object call. - */ -function resourcebundle_get_error_code(ResourceBundle $bundle): int {} - -/** - * (PHP >= 5.3.2, PECL intl >= 2.0.0)
- * Get bundle's last error message. - * @link https://php.net/manual/en/resourcebundle.geterrormessage.php - * @param ResourceBundle $bundle - * @return string error message from last bundle object's call. - */ -function resourcebundle_get_error_message(ResourceBundle $bundle): string {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create a transliterator - * @link https://php.net/manual/en/transliterator.create.php - * @param string $id

- * The id. - *

- * @param int $direction

- * The direction, defaults to - * Transliterator::FORWARD. - * May also be set to - * Transliterator::REVERSE. - *

- * @return Transliterator|null a Transliterator object on success, - * or NULL on failure. - * @since 5.4 - */ -function transliterator_create(string $id, int $direction = 0): ?Transliterator {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create transliterator from rules - * @link https://php.net/manual/en/transliterator.createfromrules.php - * @param string $rules

- * The rules. - *

- * @param int $direction

- * The direction, defaults to - * Transliterator::FORWARD. - * May also be set to - * Transliterator::REVERSE. - *

- * @return Transliterator|null a Transliterator object on success, - * or NULL on failure. - * @since 5.4 - */ -function transliterator_create_from_rules(string $rules, int $direction = 0): ?Transliterator {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get transliterator IDs - * @link https://php.net/manual/en/transliterator.listids.php - * @return string[]|false An array of registered transliterator IDs on success, - * or FALSE on failure. - * @since 5.4 - */ -function transliterator_list_ids(): array|false {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Create an inverse transliterator - * @link https://php.net/manual/en/transliterator.createinverse.php - * @param Transliterator $transliterator - * @return Transliterator|null a Transliterator object on success, - * or NULL on failure - * @since 5.4 - */ -function transliterator_create_inverse(Transliterator $transliterator): ?Transliterator {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Transliterate a string - * @link https://php.net/manual/en/transliterator.transliterate.php - * @param Transliterator|string $transliterator - * @param string $string

- * The string to be transformed. - *

- * @param int $start

- * The start index (in UTF-16 code units) from which the string will start - * to be transformed, inclusive. Indexing starts at 0. The text before will - * be left as is. - *

- * @param int $end

- * The end index (in UTF-16 code units) until which the string will be - * transformed, exclusive. Indexing starts at 0. The text after will be - * left as is. - *

- * @return string|false The transfomed string on success, or FALSE on failure. - * @since 5.4 - */ -function transliterator_transliterate(Transliterator|string $transliterator, string $string, int $start = 0, int $end = -1): string|false {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get last error code - * @link https://php.net/manual/en/transliterator.geterrorcode.php - * @param Transliterator $transliterator - * @return int|false The error code on success, - * or FALSE if none exists, or on failure. - * @since 5.4 - */ -function transliterator_get_error_code(Transliterator $transliterator): int|false {} - -/** - * (PHP >= 5.4.0, PECL intl >= 2.0.0)
- * Get last error message - * @link https://php.net/manual/en/transliterator.geterrormessage.php - * @param Transliterator $transliterator - * @return string|false The error code on success, - * or FALSE if none exists, or on failure. - * @since 5.4 - */ -function transliterator_get_error_message(Transliterator $transliterator): string|false {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get the last error code - * @link https://php.net/manual/en/function.intl-get-error-code.php - * @return int Error code returned by the last API function call. - */ -function intl_get_error_code(): int {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get description of the last error - * @link https://php.net/manual/en/function.intl-get-error-message.php - * @return string Description of an error occurred in the last API function call. - */ -function intl_get_error_message(): string {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Check whether the given error code indicates failure - * @link https://php.net/manual/en/function.intl-is-failure.php - * @param int $errorCode

- * is a value that returned by functions: - * intl_get_error_code, - * collator_get_error_code . - *

- * @return bool TRUE if it the code indicates some failure, and FALSE - * in case of success or a warning. - */ -function intl_is_failure(int $errorCode): bool {} - -/** - * (PHP 5 >= 5.3.0, PECL intl >= 1.0.0)
- * Get symbolic name for a given error code - * @link https://php.net/manual/en/function.intl-error-name.php - * @param int $errorCode

- * ICU error code. - *

- * @return string The returned string will be the same as the name of the error code - * constant. - */ -function intl_error_name(int $errorCode): string {} - -/** - * Gets the Decomposition_Mapping property for the given UTF-8 encoded code point - * - * @link https://www.php.net/manual/en/normalizer.getrawdecomposition.php - * - * @param string $string - * @param int $form - * @return string|null - * - * @since 7.3 - */ -function normalizer_get_raw_decomposition(string $string, int $form = Normalizer::FORM_C): ?string {} - -/** - * @return IntlTimeZone - * @since 5.5 - */ -function intltz_create_default(): IntlTimeZone {} - -/** - * @return IntlTimeZone - * @since 5.5 - */ -function intltz_get_gmt(): IntlTimeZone {} - -/** - * @return IntlTimeZone - * @since 5.5 - */ -function intltz_get_unknown(): IntlTimeZone {} - -/** - * @param int $type - * @param null|string $region - * @param null|int $rawOffset - * @return IntlIterator|false - * @since 5.5 - */ -function intltz_create_time_zone_id_enumeration(int $type, ?string $region = null, ?int $rawOffset = null): IntlIterator|false {} - -/** - * @param string $timezoneId - * @return string|false - * @since 5.5 - */ -function intltz_get_region(string $timezoneId): string|false {} - -/** - * Set minimal number of days the first week in a year or month can have - * - * @link https://www.php.net/manual/en/intlcalendar.setminimaldaysinfirstweek.php - * - * @param IntlCalendar $calendar - * @param int $days - * @return bool - * - * @since 5.5.1 - */ -function intlcal_set_minimal_days_in_first_week(IntlCalendar $calendar, int $days): bool {} - -function intltz_get_windows_id(string $timezoneId): string|false {} - -function intltz_get_id_for_windows_id(string $timezoneId, ?string $region = null): string|false {} - -/** - * @since 8.4 - */ -function grapheme_str_split(string $string, int $length = 1): array|false {} - -/** - * @since 8.4 - */ -function intltz_get_iana_id(string $timezoneId): string|false {} - -/** - * Limit on locale length, set to 80 in PHP code. Locale names longer - * than this limit will not be accepted. - * @link https://php.net/manual/en/intl.constants.php - */ -define('INTL_MAX_LOCALE_LEN', 156); -define('INTL_ICU_VERSION', "74.1"); -define('INTL_ICU_DATA_VERSION', "74.1"); -define('ULOC_ACTUAL_LOCALE', 0); -define('ULOC_VALID_LOCALE', 1); -define('GRAPHEME_EXTR_COUNT', 0); -define('GRAPHEME_EXTR_MAXBYTES', 1); -define('GRAPHEME_EXTR_MAXCHARS', 2); -define('U_USING_FALLBACK_WARNING', -128); -define('U_ERROR_WARNING_START', -128); -define('U_USING_DEFAULT_WARNING', -127); -define('U_SAFECLONE_ALLOCATED_WARNING', -126); -define('U_STATE_OLD_WARNING', -125); -define('U_STRING_NOT_TERMINATED_WARNING', -124); -define('U_SORT_KEY_TOO_SHORT_WARNING', -123); -define('U_AMBIGUOUS_ALIAS_WARNING', -122); -define('U_DIFFERENT_UCA_VERSION', -121); -define('U_ERROR_WARNING_LIMIT', -119); -define('U_ZERO_ERROR', 0); -define('U_ILLEGAL_ARGUMENT_ERROR', 1); -define('U_MISSING_RESOURCE_ERROR', 2); -define('U_INVALID_FORMAT_ERROR', 3); -define('U_FILE_ACCESS_ERROR', 4); -define('U_INTERNAL_PROGRAM_ERROR', 5); -define('U_MESSAGE_PARSE_ERROR', 6); -define('U_MEMORY_ALLOCATION_ERROR', 7); -define('U_INDEX_OUTOFBOUNDS_ERROR', 8); -define('U_PARSE_ERROR', 9); -define('U_INVALID_CHAR_FOUND', 10); -define('U_TRUNCATED_CHAR_FOUND', 11); -define('U_ILLEGAL_CHAR_FOUND', 12); -define('U_INVALID_TABLE_FORMAT', 13); -define('U_INVALID_TABLE_FILE', 14); -define('U_BUFFER_OVERFLOW_ERROR', 15); -define('U_UNSUPPORTED_ERROR', 16); -define('U_RESOURCE_TYPE_MISMATCH', 17); -define('U_ILLEGAL_ESCAPE_SEQUENCE', 18); -define('U_UNSUPPORTED_ESCAPE_SEQUENCE', 19); -define('U_NO_SPACE_AVAILABLE', 20); -define('U_CE_NOT_FOUND_ERROR', 21); -define('U_PRIMARY_TOO_LONG_ERROR', 22); -define('U_STATE_TOO_OLD_ERROR', 23); -define('U_TOO_MANY_ALIASES_ERROR', 24); -define('U_ENUM_OUT_OF_SYNC_ERROR', 25); -define('U_INVARIANT_CONVERSION_ERROR', 26); -define('U_INVALID_STATE_ERROR', 27); -define('U_COLLATOR_VERSION_MISMATCH', 28); -define('U_USELESS_COLLATOR_ERROR', 29); -define('U_NO_WRITE_PERMISSION', 30); -define('U_STANDARD_ERROR_LIMIT', 32); -define('U_BAD_VARIABLE_DEFINITION', 65536); -define('U_PARSE_ERROR_START', 65536); -define('U_MALFORMED_RULE', 65537); -define('U_MALFORMED_SET', 65538); -define('U_MALFORMED_SYMBOL_REFERENCE', 65539); -define('U_MALFORMED_UNICODE_ESCAPE', 65540); -define('U_MALFORMED_VARIABLE_DEFINITION', 65541); -define('U_MALFORMED_VARIABLE_REFERENCE', 65542); -define('U_MISMATCHED_SEGMENT_DELIMITERS', 65543); -define('U_MISPLACED_ANCHOR_START', 65544); -define('U_MISPLACED_CURSOR_OFFSET', 65545); -define('U_MISPLACED_QUANTIFIER', 65546); -define('U_MISSING_OPERATOR', 65547); -define('U_MISSING_SEGMENT_CLOSE', 65548); -define('U_MULTIPLE_ANTE_CONTEXTS', 65549); -define('U_MULTIPLE_CURSORS', 65550); -define('U_MULTIPLE_POST_CONTEXTS', 65551); -define('U_TRAILING_BACKSLASH', 65552); -define('U_UNDEFINED_SEGMENT_REFERENCE', 65553); -define('U_UNDEFINED_VARIABLE', 65554); -define('U_UNQUOTED_SPECIAL', 65555); -define('U_UNTERMINATED_QUOTE', 65556); -define('U_RULE_MASK_ERROR', 65557); -define('U_MISPLACED_COMPOUND_FILTER', 65558); -define('U_MULTIPLE_COMPOUND_FILTERS', 65559); -define('U_INVALID_RBT_SYNTAX', 65560); -define('U_INVALID_PROPERTY_PATTERN', 65561); -define('U_MALFORMED_PRAGMA', 65562); -define('U_UNCLOSED_SEGMENT', 65563); -define('U_ILLEGAL_CHAR_IN_SEGMENT', 65564); -define('U_VARIABLE_RANGE_EXHAUSTED', 65565); -define('U_VARIABLE_RANGE_OVERLAP', 65566); -define('U_ILLEGAL_CHARACTER', 65567); -define('U_INTERNAL_TRANSLITERATOR_ERROR', 65568); -define('U_INVALID_ID', 65569); -define('U_INVALID_FUNCTION', 65570); -define('U_PARSE_ERROR_LIMIT', 65571); -define('U_UNEXPECTED_TOKEN', 65792); -define('U_FMT_PARSE_ERROR_START', 65792); -define('U_MULTIPLE_DECIMAL_SEPARATORS', 65793); -define('U_MULTIPLE_DECIMAL_SEPERATORS', 65793); -define('U_MULTIPLE_EXPONENTIAL_SYMBOLS', 65794); -define('U_MALFORMED_EXPONENTIAL_PATTERN', 65795); -define('U_MULTIPLE_PERCENT_SYMBOLS', 65796); -define('U_MULTIPLE_PERMILL_SYMBOLS', 65797); -define('U_MULTIPLE_PAD_SPECIFIERS', 65798); -define('U_PATTERN_SYNTAX_ERROR', 65799); -define('U_ILLEGAL_PAD_POSITION', 65800); -define('U_UNMATCHED_BRACES', 65801); -define('U_UNSUPPORTED_PROPERTY', 65802); -define('U_UNSUPPORTED_ATTRIBUTE', 65803); -define('U_FMT_PARSE_ERROR_LIMIT', 65812); -define('U_BRK_INTERNAL_ERROR', 66048); -define('U_BRK_ERROR_START', 66048); -define('U_BRK_HEX_DIGITS_EXPECTED', 66049); -define('U_BRK_SEMICOLON_EXPECTED', 66050); -define('U_BRK_RULE_SYNTAX', 66051); -define('U_BRK_UNCLOSED_SET', 66052); -define('U_BRK_ASSIGN_ERROR', 66053); -define('U_BRK_VARIABLE_REDFINITION', 66054); -define('U_BRK_MISMATCHED_PAREN', 66055); -define('U_BRK_NEW_LINE_IN_QUOTED_STRING', 66056); -define('U_BRK_UNDEFINED_VARIABLE', 66057); -define('U_BRK_INIT_ERROR', 66058); -define('U_BRK_RULE_EMPTY_SET', 66059); -define('U_BRK_UNRECOGNIZED_OPTION', 66060); -define('U_BRK_MALFORMED_RULE_TAG', 66061); -define('U_BRK_ERROR_LIMIT', 66062); -define('U_REGEX_INTERNAL_ERROR', 66304); -define('U_REGEX_ERROR_START', 66304); -define('U_REGEX_RULE_SYNTAX', 66305); -define('U_REGEX_INVALID_STATE', 66306); -define('U_REGEX_BAD_ESCAPE_SEQUENCE', 66307); -define('U_REGEX_PROPERTY_SYNTAX', 66308); -define('U_REGEX_UNIMPLEMENTED', 66309); -define('U_REGEX_MISMATCHED_PAREN', 66310); -define('U_REGEX_NUMBER_TOO_BIG', 66311); -define('U_REGEX_BAD_INTERVAL', 66312); -define('U_REGEX_MAX_LT_MIN', 66313); -define('U_REGEX_INVALID_BACK_REF', 66314); -define('U_REGEX_INVALID_FLAG', 66315); -define('U_REGEX_LOOK_BEHIND_LIMIT', 66316); -define('U_REGEX_SET_CONTAINS_STRING', 66317); -define('U_REGEX_ERROR_LIMIT', 66326); -define('U_IDNA_PROHIBITED_ERROR', 66560); -define('U_IDNA_ERROR_START', 66560); -define('U_IDNA_UNASSIGNED_ERROR', 66561); -define('U_IDNA_CHECK_BIDI_ERROR', 66562); -define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); -define('U_IDNA_ACE_PREFIX_ERROR', 66564); -define('U_IDNA_VERIFICATION_ERROR', 66565); -define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); -define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); -define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); -define('U_IDNA_ERROR_LIMIT', 66569); -define('U_STRINGPREP_PROHIBITED_ERROR', 66560); -define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); -define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); -define('U_ERROR_LIMIT', 66818); - -/** - * Prohibit processing of unassigned codepoints in the input for IDN - * functions and do not check if the input conforms to domain name ASCII rules. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_DEFAULT', 0); - -/** - * Allow processing of unassigned codepoints in the input for IDN functions. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_ALLOW_UNASSIGNED', 1); - -/** - * Check if the input for IDN functions conforms to domain name ASCII rules. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_USE_STD3_RULES', 2); - -/** - * Check whether the input conforms to the BiDi rules. - * Ignored by the IDNA2003 implementation, which always performs this check. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_CHECK_BIDI', 4); - -/** - * Check whether the input conforms to the CONTEXTJ rules. - * Ignored by the IDNA2003 implementation, as this check is new in IDNA2008. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_CHECK_CONTEXTJ', 8); - -/** - * Option for nontransitional processing in - * idn_to_ascii. Transitional processing is activated - * by default. This option is ignored by the IDNA2003 implementation. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); - -/** - * Option for nontransitional processing in - * idn_to_utf8. Transitional processing is activated - * by default. This option is ignored by the IDNA2003 implementation. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); - -/** - * Use IDNA 2003 algorithm in {@see idn_to_utf8} and - * {@see idn_to_ascii}. This is the default. - * @link https://php.net/manual/en/intl.constants.php - * @deprecated 7.2 Use {@see INTL_IDNA_VARIANT_UTS46} instead. - */ -define('INTL_IDNA_VARIANT_2003', 0); - -/** - * Use UTS #46 algorithm in idn_to_utf8 and - * idn_to_ascii. - * @link https://php.net/manual/en/intl.constants.php - */ -define('INTL_IDNA_VARIANT_UTS46', 1); - -/** - * Errors reported in a bitset returned by the UTS #46 algorithm in - * idn_to_utf8 and - * idn_to_ascii. - * @link https://php.net/manual/en/intl.constants.php - */ -define('IDNA_ERROR_EMPTY_LABEL', 1); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_LABEL_TOO_LONG', 2); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_LEADING_HYPHEN', 8); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_TRAILING_HYPHEN', 16); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_HYPHEN_3_4', 32); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_DISALLOWED', 128); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_PUNYCODE', 256); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_LABEL_HAS_DOT', 512); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_BIDI', 2048); -/** - * @link https://secure.php.net/manual/en/migration54.global-constants.php - * @since 5.4 - */ -define('IDNA_ERROR_CONTEXTJ', 4096); - -/** - * @since 5.5 - */ -class IntlBreakIterator implements IteratorAggregate -{ - /* Constants */ - public const DONE = -1; - public const WORD_NONE = 0; - public const WORD_NONE_LIMIT = 100; - public const WORD_NUMBER = 100; - public const WORD_NUMBER_LIMIT = 200; - public const WORD_LETTER = 200; - public const WORD_LETTER_LIMIT = 300; - public const WORD_KANA = 300; - public const WORD_KANA_LIMIT = 400; - public const WORD_IDEO = 400; - public const WORD_IDEO_LIMIT = 500; - public const LINE_SOFT = 0; - public const LINE_SOFT_LIMIT = 100; - public const LINE_HARD = 100; - public const LINE_HARD_LIMIT = 200; - public const SENTENCE_TERM = 0; - public const SENTENCE_TERM_LIMIT = 100; - public const SENTENCE_SEP = 100; - public const SENTENCE_SEP_LIMIT = 200; - - /* Methods */ - /** - * (PHP 5 >=5.5.0)
- * Private constructor for disallowing instantiation - */ - private function __construct() {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for boundaries of combining character sequences - * @link https://secure.php.net/manual/en/intlbreakiterator.createcharacterinstance.php - * @param string $locale - * @return IntlBreakIterator|null - */ - public static function createCharacterInstance(string|null $locale = null): ?IntlBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for boundaries of code points - * @link https://secure.php.net/manual/en/intlbreakiterator.createcodepointinstance.php - * @return IntlCodePointBreakIterator - */ - public static function createCodePointInstance(): IntlCodePointBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for logically possible line breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createlineinstance.php - * @param string $locale [optional] - * @return IntlBreakIterator|null - */ - public static function createLineInstance(string|null $locale): ?IntlBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for sentence breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createsentenceinstance.php - * @param string $locale [optional] - * @return IntlBreakIterator|null - */ - public static function createSentenceInstance(string|null $locale): ?IntlBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for title-casing breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createtitleinstance.php - * @param string $locale [optional] - * @return IntlBreakIterator|null - */ - public static function createTitleInstance(string|null $locale): ?IntlBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for word breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createwordinstance.php - * @param string $locale [optional] - * @return IntlBreakIterator|null - */ - public static function createWordInstance(string|null $locale): ?IntlBreakIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Get index of current position - * @link https://secure.php.net/manual/en/intlbreakiterator.current.php - * @return int - */ - public function current(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Set position to the first character in the text - * @link https://secure.php.net/manual/en/intlbreakiterator.first.php - */ - public function first(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Advance the iterator to the first boundary following specified offset - * @link https://secure.php.net/manual/en/intlbreakiterator.following.php - * @param int $offset - */ - public function following(int $offset): int {} - - /** - * (PHP 5 >=5.5.0)
- * Get last error code on the object - * @link https://secure.php.net/manual/en/intlbreakiterator.geterrorcode.php - * @return int - */ - public function getErrorCode(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Get last error message on the object - * @link https://secure.php.net/manual/en/intlbreakiterator.geterrormessage.php - * @return string - */ - public function getErrorMessage(): string {} - - /** - * (PHP 5 >=5.5.0)
- * Get the locale associated with the object - * @link https://secure.php.net/manual/en/intlbreakiterator.getlocale.php - * @param string $type - */ - public function getLocale(int $type): string|false {} - - /** - * (PHP 5 >=5.5.0)
- * Create iterator for navigating fragments between boundaries - * @link https://secure.php.net/manual/en/intlbreakiterator.getpartsiterator.php - * @param int $type [optional] - *

- * Optional key type. Possible values are: - *

    - *
  • - * {@see IntlPartsIterator::KEY_SEQUENTIAL} - * - The default. Sequentially increasing integers used as key. - *
  • - *
  • - * {@see IntlPartsIterator::KEY_LEFT} - * - Byte offset left of current part used as key. - *
  • - *
  • - * {@see IntlPartsIterator::KEY_RIGHT} - * - Byte offset right of current part used as key. - *
  • - *
- */ - public function getPartsIterator( - int $type = IntlPartsIterator::KEY_SEQUENTIAL - ): IntlPartsIterator {} - - /** - * (PHP 5 >=5.5.0)
- * Get the text being scanned - * @link https://secure.php.net/manual/en/intlbreakiterator.gettext.php - */ - public function getText(): ?string {} - - /** - * (PHP 5 >=5.5.0)
- * Tell whether an offset is a boundary's offset - * @link https://secure.php.net/manual/en/intlbreakiterator.isboundary.php - * @param int $offset - */ - public function isBoundary(int $offset): bool {} - - /** - * (PHP 5 >=5.5.0)
- * Set the iterator position to index beyond the last character - * @link https://secure.php.net/manual/en/intlbreakiterator.last.php - * @return int - */ - public function last(): int {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlbreakiterator.next.php - * @param int $offset [optional] - * @return int - */ - public function next(int|null $offset = null): int {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlbreakiterator.preceding.php - * @param int $offset - */ - public function preceding(int $offset): int {} - - /** - * (PHP 5 >=5.5.0)
- * Set the iterator position to the boundary immediately before the current - * @link https://secure.php.net/manual/en/intlbreakiterator.previous.php - * @return int - */ - public function previous(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Set the text being scanned - * @link https://secure.php.net/manual/en/intlbreakiterator.settext.php - * @param string $text - */ - public function setText(string $text): bool {} - - /** - * @since 8.0 - * @return Iterator - */ - public function getIterator(): Iterator {} -} - -class IntlRuleBasedBreakIterator extends IntlBreakIterator implements Traversable -{ - /* Methods */ - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlbreakiterator.construct.php - * @param string $rules - * @param string $compiled [optional] - */ - public function __construct( - string $rules, - bool $compiled = false - ) {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for boundaries of combining character sequences - * @link https://secure.php.net/manual/en/intlbreakiterator.createcharacterinstance.php - * @param string $locale - * @return IntlRuleBasedBreakIterator - */ - public static function createCharacterInstance($locale) {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for boundaries of code points - * @link https://secure.php.net/manual/en/intlbreakiterator.createcodepointinstance.php - * @return IntlRuleBasedBreakIterator - */ - public static function createCodePointInstance() {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for logically possible line breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createlineinstance.php - * @param string $locale [optional] - * @return IntlRuleBasedBreakIterator - */ - public static function createLineInstance($locale) {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for sentence breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createsentenceinstance.php - * @param string $locale [optional] - * @return IntlRuleBasedBreakIterator - */ - public static function createSentenceInstance($locale) {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for title-casing breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createtitleinstance.php - * @param string $locale [optional] - * @return IntlRuleBasedBreakIterator - */ - public static function createTitleInstance($locale) {} - - /** - * (PHP 5 >=5.5.0)
- * Create break iterator for word breaks - * @link https://secure.php.net/manual/en/intlbreakiterator.createwordinstance.php - * @param string $locale [optional] - * @return IntlRuleBasedBreakIterator - */ - public static function createWordInstance($locale) {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlrulebasedbreakiterator.getbinaryrules.php - * Get the binary form of compiled rules - * @return string|false - */ - public function getBinaryRules(): string|false {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlrulebasedbreakiterator.getrules.php - * Get the rule set used to create this object - * @return string|false - */ - public function getRules(): string|false {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlrulebasedbreakiterator.getrulesstatus.php - * Get the largest status value from the break rules that determined the current break position - * @return int - */ - public function getRuleStatus(): int {} - - /** - * (PHP 5 >=5.5.0)
- * @link https://secure.php.net/manual/en/intlrulebasedbreakiterator.getrulestatusvec.php - * Get the status values from the break rules that determined the current break position - * @return array|false - */ - public function getRuleStatusVec(): array|false {} -} - -/** - * @link https://www.php.net/manual/en/class.intlpartsiterator.php - * @since 5.5 - */ -class IntlPartsIterator extends IntlIterator implements Iterator -{ - public const KEY_SEQUENTIAL = 0; - public const KEY_LEFT = 1; - public const KEY_RIGHT = 2; - - /** - * @return IntlBreakIterator - */ - public function getBreakIterator(): IntlBreakIterator {} - - /** - * @since 8.1 - */ - public function getRuleStatus(): int {} -} - -class IntlCodePointBreakIterator extends IntlBreakIterator implements Traversable -{ - /** - * (PHP 5 >=5.5.0)
- * Get last code point passed over after advancing or receding the iterator - * @link https://secure.php.net/manual/en/intlcodepointbreakiterator.getlastcodepoint.php - * @return int - */ - public function getLastCodePoint(): int {} -} - -class UConverter -{ - /* Constants */ - public const REASON_UNASSIGNED = 0; - public const REASON_ILLEGAL = 1; - public const REASON_IRREGULAR = 2; - public const REASON_RESET = 3; - public const REASON_CLOSE = 4; - public const REASON_CLONE = 5; - public const UNSUPPORTED_CONVERTER = -1; - public const SBCS = 0; - public const DBCS = 1; - public const MBCS = 2; - public const LATIN_1 = 3; - public const UTF8 = 4; - public const UTF16_BigEndian = 5; - public const UTF16_LittleEndian = 6; - public const UTF32_BigEndian = 7; - public const UTF32_LittleEndian = 8; - public const EBCDIC_STATEFUL = 9; - public const ISO_2022 = 10; - public const LMBCS_1 = 11; - public const LMBCS_2 = 12; - public const LMBCS_3 = 13; - public const LMBCS_4 = 14; - public const LMBCS_5 = 15; - public const LMBCS_6 = 16; - public const LMBCS_8 = 17; - public const LMBCS_11 = 18; - public const LMBCS_16 = 19; - public const LMBCS_17 = 20; - public const LMBCS_18 = 21; - public const LMBCS_19 = 22; - public const LMBCS_LAST = 22; - public const HZ = 23; - public const SCSU = 24; - public const ISCII = 25; - public const US_ASCII = 26; - public const UTF7 = 27; - public const BOCU1 = 28; - public const UTF16 = 29; - public const UTF32 = 30; - public const CESU8 = 31; - public const IMAP_MAILBOX = 32; - - /* Methods */ - /** - * (PHP 5 >=5.5.0)
- * Create UConverter object - * @link https://php.net/manual/en/uconverter.construct.php - * @param string $destination_encoding - * @param string $source_encoding - */ - public function __construct( - string|null $destination_encoding = null, - string|null $source_encoding = null - ) {} - - /** - * (PHP 5 >=5.5.0)
- * Convert string from one charset to anothe - * @link https://php.net/manual/en/uconverter.convert.php - * @param string $str - * @param bool $reverse [optional] - * @return string|false - */ - public function convert( - string $str, - bool $reverse = false - ): string|false {} - - /** - * (PHP 5 >=5.5.0)
- * Default "from" callback function - * @link https://php.net/manual/en/uconverter.fromucallback.php - * @param int $reason - * @param string $source - * @param string $codePoint - * @param int &$error - * @return array|string|int|null - */ - public function fromUCallback( - int $reason, - array $source, - int $codePoint, - &$error - ): array|string|int|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get the aliases of the given name - * @link https://php.net/manual/en/uconverter.getaliases.php - * @param string $name - * @return array|false|null - */ - public static function getAliases( - string $name - ): array|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get the available canonical converter names - * @link https://php.net/manual/en/uconverter.getavailable.php - * @return array - */ - public static function getAvailable(): array {} - - /** - * (PHP 5 >=5.5.0)
- * Get the destination encoding - * @link https://php.net/manual/en/uconverter.getdestinationencoding.php - * @return string|false|null - */ - public function getDestinationEncoding(): string|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get the destination converter type - * @link https://php.net/manual/en/uconverter.getdestinationtype.php - * @return int|false|null - */ - public function getDestinationType(): int|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get last error code on the object - * @link https://php.net/manual/en/uconverter.geterrorcode.php - * @return int - */ - public function getErrorCode(): int {} - - /** - * (PHP 5 >=5.5.0)
- * Get last error message on the object - * @link https://php.net/manual/en/uconverter.geterrormessage.php - * @return string|null - */ - public function getErrorMessage(): ?string {} - - /** - * (PHP 5 >=5.5.0)
- * Get the source encoding - * @link https://php.net/manual/en/uconverter.getsourceencoding.php - * @return string|false|null - */ - public function getSourceEncoding(): string|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get the source convertor type - * @link https://php.net/manual/en/uconverter.getsourcetype.php - * @return int|false|null - */ - public function getSourceType(): int|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get standards associated to converter names - * @link https://php.net/manual/en/uconverter.getstandards.php - * @return array|null - */ - public static function getStandards(): ?array {} - - /** - * (PHP 5 >=5.5.0)
- * Get substitution chars - * @link https://php.net/manual/en/uconverter.getsubstchars.php - * @return string|false|null - */ - public function getSubstChars(): string|false|null {} - - /** - * (PHP 5 >=5.5.0)
- * Get string representation of the callback reason - * @link https://php.net/manual/en/uconverter.reasontext.php - * @param int $reason - * @return string - */ - public static function reasonText( - int $reason - ): string {} - - /** - * (PHP 5 >=5.5.0)
- * Set the destination encoding - * @link https://php.net/manual/en/uconverter.setdestinationencoding.php - * @param string $encoding - * @return bool - */ - public function setDestinationEncoding(string $encoding): bool {} - - /** - * (PHP 5 >=5.5.0)
- * Set the source encoding - * @link https://php.net/manual/en/uconverter.setsourceencoding.php - * @param string $encoding - * @return bool - */ - public function setSourceEncoding(string $encoding): bool {} - - /** - * (PHP 5 >=5.5.0)
- * Set the substitution chars - * @link https://php.net/manual/en/uconverter.setsubstchars.php - * @param string $chars - * @return bool - */ - public function setSubstChars(string $chars): bool {} - - /** - * (PHP 5 >=5.5.0)
- * Default "to" callback function - * @link https://php.net/manual/en/uconverter.toucallback.php - * @param int $reason - * @param string $source - * @param string $codeUnits - * @param int &$error - * @return array|string|int|null - */ - public function toUCallback( - int $reason, - string $source, - string $codeUnits, - &$error - ): array|string|int|null {} - - /** - * (PHP 5 >=5.5.0)
- * Convert string from one charset to another - * @link https://php.net/manual/en/uconverter.transcode.php - * @param string $str - * @param string $toEncoding - * @param string $fromEncoding - * @param array|null $options - * @return string|false - */ - public static function transcode( - string $str, - string $toEncoding, - string $fromEncoding, - array|null $options = null - ): string|false {} -} -// End of intl v.1.1.0 --- a/build/stubs/ldap.php +++ /dev/null @@ -1,226 +0,0 @@ -|false - * @refcount 1 - */ - function ldap_get_entries(LDAP\Connection $ldap, LDAP\Result $result): array|false {} - - function ldap_first_attribute(LDAP\Connection $ldap, LDAP\ResultEntry $entry): string|false {} - - function ldap_next_attribute(LDAP\Connection $ldap, LDAP\ResultEntry $entry): string|false {} - - /** - * @return array - * @refcount 1 - */ - function ldap_get_attributes(LDAP\Connection $ldap, LDAP\ResultEntry $entry): array {} - - /** - * @return array|false - * @refcount 1 - */ - function ldap_get_values_len(LDAP\Connection $ldap, LDAP\ResultEntry $entry, string $attribute): array|false {} - - /** - * @return array|false - * @refcount 1 - * @alias ldap_get_values_len - */ - function ldap_get_values(LDAP\Connection $ldap, LDAP\ResultEntry $entry, string $attribute): array|false {} - - function ldap_get_dn(LDAP\Connection $ldap, LDAP\ResultEntry $entry): string|false {} - - /** - * @return array|false - * @refcount 1 - */ - function ldap_explode_dn(string $dn, int $with_attrib): array|false {} - - function ldap_dn2ufn(string $dn): string|false {} - - function ldap_add(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): bool {} - - function ldap_add_ext(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): LDAP\Result|false {} - - function ldap_delete(LDAP\Connection $ldap, string $dn, ?array $controls = null): bool {} - - function ldap_delete_ext(LDAP\Connection $ldap, string $dn, ?array $controls = null): LDAP\Result|false {} - - function ldap_modify_batch(LDAP\Connection $ldap, string $dn, array $modifications_info, ?array $controls = null): bool {} - - function ldap_mod_add(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): bool {} - - function ldap_mod_add_ext(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): LDAP\Result|false {} - - function ldap_mod_replace(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): bool {} - - /** @alias ldap_mod_replace */ - function ldap_modify(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): bool {} - - function ldap_mod_replace_ext(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): LDAP\Result|false {} - - function ldap_mod_del(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): bool {} - - function ldap_mod_del_ext(LDAP\Connection $ldap, string $dn, array $entry, ?array $controls = null): LDAP\Result|false {} - - function ldap_errno(LDAP\Connection $ldap): int {} - - function ldap_error(LDAP\Connection $ldap): string {} - - function ldap_err2str(int $errno): string {} - - function ldap_compare(LDAP\Connection $ldap, string $dn, string $attribute, string $value, ?array $controls = null): bool|int {} - - #if (LDAP_API_VERSION > 2000) || defined(HAVE_ORALDAP) - function ldap_rename(LDAP\Connection $ldap, string $dn, string $new_rdn, string $new_parent, bool $delete_old_rdn, ?array $controls = null): bool {} - - function ldap_rename_ext(LDAP\Connection $ldap, string $dn, string $new_rdn, string $new_parent, bool $delete_old_rdn, ?array $controls = null): LDAP\Result|false {} - - /** @param array|string|int $value */ - function ldap_get_option(LDAP\Connection $ldap, int $option, &$value = null): bool {} - - /** @param array|string|int|bool $value */ - function ldap_set_option(?LDAP\Connection $ldap, int $option, $value): bool {} - - function ldap_count_references(LDAP\Connection $ldap, LDAP\Result $result): int {} - - function ldap_first_reference(LDAP\Connection $ldap, LDAP\Result $result): LDAP\ResultEntry|false {} - - function ldap_next_reference(LDAP\Connection $ldap, LDAP\ResultEntry $entry): LDAP\ResultEntry|false {} - - #ifdef HAVE_LDAP_PARSE_REFERENCE - /** @param array $referrals */ - function ldap_parse_reference(LDAP\Connection $ldap, LDAP\ResultEntry $entry, &$referrals): bool {} - #endif - - #ifdef HAVE_LDAP_PARSE_RESULT - /** - * @param int $error_code - * @param string $matched_dn - * @param string $error_message - * @param array $referrals - * @param array $controls - */ - function ldap_parse_result(LDAP\Connection $ldap, LDAP\Result $result, &$error_code, &$matched_dn = null, &$error_message = null, &$referrals = null, &$controls = null): bool {} - #endif - #endif - - #if defined(LDAP_API_FEATURE_X_OPENLDAP) && defined(HAVE_3ARG_SETREBINDPROC) - function ldap_set_rebind_proc(LDAP\Connection $ldap, ?callable $callback): bool {} - #endif - - #ifdef HAVE_LDAP_START_TLS_S - function ldap_start_tls(LDAP\Connection $ldap): bool {} - #endif - - function ldap_escape(string $value, string $ignore = "", int $flags = 0): string {} - - #ifdef STR_TRANSLATION - function ldap_t61_to_8859(string $value): string|false {} - - function ldap_8859_to_t61(string $value): string|false {} - #endif - - - #ifdef HAVE_LDAP_EXTENDED_OPERATION_S - /** - * @param string $response_data - * @param string $response_oid - */ - function ldap_exop(LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = NULL, &$response_data = UNKNOWN, &$response_oid = null): LDAP\Result|bool {} - #endif - - #ifdef HAVE_LDAP_PASSWD - /** - * @param array $controls - */ - function ldap_exop_passwd(LDAP\Connection $ldap, string $user = "", string $old_password = "", string $new_password = "", &$controls = null): string|bool {} - #endif - - - #ifdef HAVE_LDAP_WHOAMI_S - function ldap_exop_whoami(LDAP\Connection $ldap): string|false {} - #endif - - #ifdef HAVE_LDAP_REFRESH_S - function ldap_exop_refresh(LDAP\Connection $ldap, string $dn, int $ttl): int|false {} - #endif - - #ifdef HAVE_LDAP_PARSE_EXTENDED_RESULT - /** - * @param string $response_data - * @param string $response_oid - */ - function ldap_parse_exop(LDAP\Connection $ldap, LDAP\Result $result, &$response_data = null, &$response_oid = null): bool {} - #endif - -} --- a/build/stubs/memcached.php +++ /dev/null @@ -1,1528 +0,0 @@ -Enables or disables payload compression. When enabled, - * item values longer than a certain threshold (currently 100 bytes) will be - * compressed during storage and decompressed during retrieval - * transparently.

- *

Type: boolean, default: TRUE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_COMPRESSION = -1001; - const OPT_COMPRESSION_TYPE = -1004; - - /** - *

This can be used to create a "domain" for your item keys. The value - * specified here will be prefixed to each of the keys. It cannot be - * longer than 128 characters and will reduce the - * maximum available key size. The prefix is applied only to the item keys, - * not to the server keys.

- *

Type: string, default: "".

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_PREFIX_KEY = -1002; - - /** - *

- * Specifies the serializer to use for serializing non-scalar values. - * The valid serializers are Memcached::SERIALIZER_PHP - * or Memcached::SERIALIZER_IGBINARY. The latter is - * supported only when memcached is configured with - * --enable-memcached-igbinary option and the - * igbinary extension is loaded. - *

- *

Type: integer, default: Memcached::SERIALIZER_PHP.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_SERIALIZER = -1003; - - /** - *

Indicates whether igbinary serializer support is available.

- *

Type: boolean.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HAVE_IGBINARY = 0; - - /** - *

Indicates whether JSON serializer support is available.

- *

Type: boolean.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HAVE_JSON = 0; - - /** - *

Indicates whether msgpack serializer support is available.

- *

Type: boolean.

- * Available as of Memcached 3.0.0. - * @since 3.0.0 - * @link https://php.net/manual/en/memcached.constants.php - */ - const HAVE_MSGPACK = 0; - - /** - *

Indicate whether set_encoding_key is available

- *

Type: boolean.

- * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/memcached-api.php, https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c#L4387 - */ - const HAVE_ENCODING = 0; - - /** - * Feature support - */ - const HAVE_SESSION = 1; - const HAVE_SASL = 0; - - /** - *

Specifies the hashing algorithm used for the item keys. The valid - * values are supplied via Memcached::HASH_* constants. - * Each hash algorithm has its advantages and its disadvantages. Go with the - * default if you don't know or don't care.

- *

Type: integer, default: Memcached::HASH_DEFAULT

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_HASH = 2; - - /** - *

The default (Jenkins one-at-a-time) item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_DEFAULT = 0; - - /** - *

MD5 item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_MD5 = 1; - - /** - *

CRC item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_CRC = 2; - - /** - *

FNV1_64 item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_FNV1_64 = 3; - - /** - *

FNV1_64A item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_FNV1A_64 = 4; - - /** - *

FNV1_32 item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_FNV1_32 = 5; - - /** - *

FNV1_32A item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_FNV1A_32 = 6; - - /** - *

Hsieh item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_HSIEH = 7; - - /** - *

Murmur item key hashing algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const HASH_MURMUR = 8; - - /** - *

Specifies the method of distributing item keys to the servers. - * Currently supported methods are modulo and consistent hashing. Consistent - * hashing delivers better distribution and allows servers to be added to - * the cluster with minimal cache losses.

- *

Type: integer, default: Memcached::DISTRIBUTION_MODULA.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_DISTRIBUTION = 9; - - /** - *

Modulo-based key distribution algorithm.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const DISTRIBUTION_MODULA = 0; - - /** - *

Consistent hashing key distribution algorithm (based on libketama).

- * @link https://php.net/manual/en/memcached.constants.php - */ - const DISTRIBUTION_CONSISTENT = 1; - const DISTRIBUTION_VIRTUAL_BUCKET = 6; - - /** - *

Enables or disables compatibility with libketama-like behavior. When - * enabled, the item key hashing algorithm is set to MD5 and distribution is - * set to be weighted consistent hashing distribution. This is useful - * because other libketama-based clients (Python, Ruby, etc.) with the same - * server configuration will be able to access the keys transparently. - *

- *

- * It is highly recommended to enable this option if you want to use - * consistent hashing, and it may be enabled by default in future - * releases. - *

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_LIBKETAMA_COMPATIBLE = 16; - const OPT_LIBKETAMA_HASH = 17; - const OPT_TCP_KEEPALIVE = 32; - - /** - *

Enables or disables buffered I/O. Enabling buffered I/O causes - * storage commands to "buffer" instead of being sent. Any action that - * retrieves data causes this buffer to be sent to the remote connection. - * Quitting the connection or closing down the connection will also cause - * the buffered data to be pushed to the remote connection.

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_BUFFER_WRITES = 10; - - /** - *

Enable the use of the binary protocol. Please note that you cannot - * toggle this option on an open connection.

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_BINARY_PROTOCOL = 18; - - /** - *

Enables or disables asynchronous I/O. This is the fastest transport - * available for storage functions.

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_NO_BLOCK = 0; - - /** - *

Enables or disables the no-delay feature for connecting sockets (may - * be faster in some environments).

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_TCP_NODELAY = 1; - - /** - *

The maximum socket send buffer in bytes.

- *

Type: integer, default: varies by platform/kernel - * configuration.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_SOCKET_SEND_SIZE = 4; - - /** - *

The maximum socket receive buffer in bytes.

- *

Type: integer, default: varies by platform/kernel - * configuration.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_SOCKET_RECV_SIZE = 5; - - /** - *

In non-blocking mode this set the value of the timeout during socket - * connection, in milliseconds.

- *

Type: integer, default: 1000.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_CONNECT_TIMEOUT = 14; - - /** - *

The amount of time, in seconds, to wait until retrying a failed - * connection attempt.

- *

Type: integer, default: 0.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_RETRY_TIMEOUT = 15; - - /** - *

Socket sending timeout, in microseconds. In cases where you cannot - * use non-blocking I/O this will allow you to still have timeouts on the - * sending of data.

- *

Type: integer, default: 0.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_SEND_TIMEOUT = 19; - - /** - *

Socket reading timeout, in microseconds. In cases where you cannot - * use non-blocking I/O this will allow you to still have timeouts on the - * reading of data.

- *

Type: integer, default: 0.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_RECV_TIMEOUT = 20; - - /** - *

Timeout for connection polling, in milliseconds.

- *

Type: integer, default: 1000.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_POLL_TIMEOUT = 8; - - /** - *

Enables or disables caching of DNS lookups.

- *

Type: boolean, default: FALSE.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_CACHE_LOOKUPS = 6; - - /** - *

Specifies the failure limit for server connection attempts. The - * server will be removed after this many continuous connection - * failures.

- *

Type: integer, default: 0.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const OPT_SERVER_FAILURE_LIMIT = 21; - const OPT_AUTO_EJECT_HOSTS = 28; - const OPT_HASH_WITH_PREFIX_KEY = 25; - const OPT_NOREPLY = 26; - const OPT_SORT_HOSTS = 12; - const OPT_VERIFY_KEY = 13; - const OPT_USE_UDP = 27; - const OPT_NUMBER_OF_REPLICAS = 29; - const OPT_RANDOMIZE_REPLICA_READ = 30; - const OPT_CORK = 31; - const OPT_REMOVE_FAILED_SERVERS = 35; - const OPT_DEAD_TIMEOUT = 36; - const OPT_SERVER_TIMEOUT_LIMIT = 37; - const OPT_MAX = 38; - const OPT_IO_BYTES_WATERMARK = 23; - const OPT_IO_KEY_PREFETCH = 24; - const OPT_IO_MSG_WATERMARK = 22; - const OPT_LOAD_FROM_FILE = 34; - const OPT_SUPPORT_CAS = 7; - const OPT_TCP_KEEPIDLE = 33; - const OPT_USER_DATA = 11; - - - /** - * libmemcached result codes - */ - /** - *

The operation was successful.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_SUCCESS = 0; - - /** - *

The operation failed in some fashion.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_FAILURE = 1; - - /** - *

DNS lookup failed.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_HOST_LOOKUP_FAILURE = 2; - - /** - *

Failed to read network data.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_UNKNOWN_READ_FAILURE = 7; - - /** - *

Bad command in memcached protocol.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_PROTOCOL_ERROR = 8; - - /** - *

Error on the client side.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_CLIENT_ERROR = 9; - - /** - *

Error on the server side.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_SERVER_ERROR = 10; - - /** - *

Failed to write network data.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_WRITE_FAILURE = 5; - - /** - *

Failed to do compare-and-swap: item you are trying to store has been - * modified since you last fetched it.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_DATA_EXISTS = 12; - - /** - *

Item was not stored: but not because of an error. This normally - * means that either the condition for an "add" or a "replace" command - * wasn't met, or that the item is in a delete queue.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_NOTSTORED = 14; - - /** - *

Item with this key was not found (with "get" operation or "cas" - * operations).

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_NOTFOUND = 16; - - /** - *

Partial network data read error.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_PARTIAL_READ = 18; - - /** - *

Some errors occurred during multi-get.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_SOME_ERRORS = 19; - - /** - *

Server list is empty.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_NO_SERVERS = 20; - - /** - *

End of result set.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_END = 21; - - /** - *

System error.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_ERRNO = 26; - - /** - *

The operation was buffered.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_BUFFERED = 32; - - /** - *

The operation timed out.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_TIMEOUT = 31; - - /** - *

Bad key.

- * @link https://php.net/manual/en/memcached.constants.php, http://docs.libmemcached.org/index.html - */ - /** - *

MEMCACHED_BAD_KEY_PROVIDED: The key provided is not a valid key.

- */ - const RES_BAD_KEY_PROVIDED = 33; - /** - *

MEMCACHED_STORED: The requested object has been successfully stored on the server.

- */ - const RES_STORED = 15; - /** - *

MEMCACHED_DELETED: The object requested by the key has been deleted.

- */ - const RES_DELETED = 22; - /** - *

MEMCACHED_STAT: A “stat” command has been returned in the protocol.

- */ - const RES_STAT = 24; - /** - *

MEMCACHED_ITEM: An item has been fetched (this is an internal error only).

- */ - const RES_ITEM = 25; - /** - *

MEMCACHED_NOT_SUPPORTED: The given method is not supported in the server.

- */ - const RES_NOT_SUPPORTED = 28; - /** - *

MEMCACHED_FETCH_NOTFINISHED: A request has been made, but the server has not finished the fetch of the last request.

- */ - const RES_FETCH_NOTFINISHED = 30; - /** - *

MEMCACHED_SERVER_MARKED_DEAD: The requested server has been marked dead.

- */ - const RES_SERVER_MARKED_DEAD = 35; - /** - *

MEMCACHED_UNKNOWN_STAT_KEY: The server you are communicating with has a stat key which has not be defined in the protocol.

- */ - const RES_UNKNOWN_STAT_KEY = 36; - /** - *

MEMCACHED_INVALID_HOST_PROTOCOL: The server you are connecting too has an invalid protocol. Most likely you are connecting to an older server that does not speak the binary protocol.

- */ - const RES_INVALID_HOST_PROTOCOL = 34; - /** - *

MEMCACHED_MEMORY_ALLOCATION_FAILURE: An error has occurred while trying to allocate memory.

- */ - const RES_MEMORY_ALLOCATION_FAILURE = 17; - /** - *

MEMCACHED_E2BIG: Item is too large for the server to store.

- */ - const RES_E2BIG = 37; - /** - *

MEMCACHED_KEY_TOO_BIG: The key that has been provided is too large for the given server.

- */ - const RES_KEY_TOO_BIG = 39; - /** - *

MEMCACHED_SERVER_TEMPORARILY_DISABLED

- */ - const RES_SERVER_TEMPORARILY_DISABLED = 47; - /** - *

MEMORY_ALLOCATION_FAILURE: An error has occurred while trying to allocate memory. - * - * #if defined(LIBMEMCACHED_VERSION_HEX) && LIBMEMCACHED_VERSION_HEX >= 0x01000008

- */ - const RES_SERVER_MEMORY_ALLOCATION_FAILURE = 48; - /** - *

MEMCACHED_AUTH_PROBLEM: An unknown issue has occured during authentication.

- */ - const RES_AUTH_PROBLEM = 40; - /** - *

MEMCACHED_AUTH_FAILURE: The credentials provided are not valid for this server.

- */ - const RES_AUTH_FAILURE = 41; - /** - *

MEMCACHED_AUTH_CONTINUE: Authentication has been paused.

- */ - const RES_AUTH_CONTINUE = 42; - /** - *

MEMCACHED_CONNECTION_FAILURE: A unknown error has occured while trying to connect to a server.

- */ - const RES_CONNECTION_FAILURE = 3; - /** - *

MEMCACHED_CONNECTION_BIND_FAILURE: Deprecated since version <0.30(libmemcached). - * We were not able to bind() to the socket.

- */ - const RES_CONNECTION_BIND_FAILURE = 4; - /** - *

MEMCACHED_READ_FAILURE: A read failure has occurred.

- */ - const RES_READ_FAILURE = 6; - /** - *

MEMCACHED_DATA_DOES_NOT_EXIST: The data requested with the key given was not found.

- */ - const RES_DATA_DOES_NOT_EXIST = 13; - /** - *

MEMCACHED_VALUE: A value has been returned from the server (this is an internal condition only).

- */ - const RES_VALUE = 23; - /** - *

MEMCACHED_FAIL_UNIX_SOCKET: A connection was not established with the server via a unix domain socket.

- */ - const RES_FAIL_UNIX_SOCKET = 27; - /** - *

MEMCACHED_NO_KEY_PROVIDED: Deprecated since version <0.30(libmemcached): Use MEMCACHED_BAD_KEY_PROVIDED instead. - * No key was provided.

- */ - const RES_NO_KEY_PROVIDED = 29; - /** - *

MEMCACHED_INVALID_ARGUMENTS: The arguments supplied to the given function were not valid.

- */ - const RES_INVALID_ARGUMENTS = 38; - /** - *

MEMCACHED_PARSE_ERROR: An error has occurred while trying to parse the configuration string. You should use memparse to determine what the error was.

- */ - const RES_PARSE_ERROR = 43; - /** - *

MEMCACHED_PARSE_USER_ERROR: An error has occurred in parsing the configuration string.

- */ - const RES_PARSE_USER_ERROR = 44; - /** - *

MEMCACHED_DEPRECATED: The method that was requested has been deprecated.

- */ - const RES_DEPRECATED = 45; - //unknow - const RES_IN_PROGRESS = 46; - /** - *

MEMCACHED_MAXIMUM_RETURN: This in an internal only state.

- */ - const RES_MAXIMUM_RETURN = 49; - - /** - * Server callbacks, if compiled with --memcached-protocol - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/memcached-api.php - */ - const ON_CONNECT = 0; - const ON_ADD = 1; - const ON_APPEND = 2; - const ON_DECREMENT = 3; - const ON_DELETE = 4; - const ON_FLUSH = 5; - const ON_GET = 6; - const ON_INCREMENT = 7; - const ON_NOOP = 8; - const ON_PREPEND = 9; - const ON_QUIT = 10; - const ON_REPLACE = 11; - const ON_SET = 12; - const ON_STAT = 13; - const ON_VERSION = 14; - /** - * Constants used when compiled with --memcached-protocol - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/memcached-api.php - */ - const RESPONSE_SUCCESS = 0; - const RESPONSE_KEY_ENOENT = 1; - const RESPONSE_KEY_EEXISTS = 2; - const RESPONSE_E2BIG = 3; - const RESPONSE_EINVAL = 4; - const RESPONSE_NOT_STORED = 5; - const RESPONSE_DELTA_BADVAL = 6; - const RESPONSE_NOT_MY_VBUCKET = 7; - const RESPONSE_AUTH_ERROR = 32; - const RESPONSE_AUTH_CONTINUE = 33; - const RESPONSE_UNKNOWN_COMMAND = 129; - const RESPONSE_ENOMEM = 130; - const RESPONSE_NOT_SUPPORTED = 131; - const RESPONSE_EINTERNAL = 132; - const RESPONSE_EBUSY = 133; - const RESPONSE_ETMPFAIL = 134; - - - /** - *

Failed to create network socket.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_CONNECTION_SOCKET_CREATE_FAILURE = 11; - - /** - *

Payload failure: could not compress/decompress or serialize/unserialize the value.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const RES_PAYLOAD_FAILURE = -1001; - - /** - *

The default PHP serializer.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const SERIALIZER_PHP = 1; - - /** - *

The igbinary serializer. - * Instead of textual representation it stores PHP data structures in a - * compact binary form, resulting in space and time gains.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const SERIALIZER_IGBINARY = 2; - - /** - *

The JSON serializer. Requires PHP 5.2.10+.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const SERIALIZER_JSON = 3; - const SERIALIZER_JSON_ARRAY = 4; - /** - *

The msgpack serializer.

- * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/memcached-api.php - */ - const SERIALIZER_MSGPACK = 5; - - const COMPRESSION_FASTLZ = 2; - const COMPRESSION_ZLIB = 1; - - /** - *

A flag for Memcached::getMulti and - * Memcached::getMultiByKey to ensure that the keys are - * returned in the same order as they were requested in. Non-existing keys - * get a default value of NULL.

- * @link https://php.net/manual/en/memcached.constants.php - */ - const GET_PRESERVE_ORDER = 1; - - /** - * A flag for Memcached::get(), Memcached::getMulti() and - * Memcached::getMultiByKey() to ensure that the CAS token values are returned as well. - * @link https://php.net/manual/en/memcached.constants.php - */ - const GET_EXTENDED = 2; - - const GET_ERROR_RETURN_VALUE = false; - - /** - * (PECL memcached >= 0.1.0)
- * Create a Memcached instance - * @link https://php.net/manual/en/memcached.construct.php, https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @param string $persistent_id [optional] - * @param callable $on_new_object_cb [optional] - * @param string $connection_str [optional] - */ - public function __construct ($persistent_id = '', $on_new_object_cb = null, $connection_str = '') {} - - /** - * (PECL memcached >= 0.1.0)
- * Return the result code of the last operation - * @link https://php.net/manual/en/memcached.getresultcode.php - * @return int Result code of the last Memcached operation. - */ - public function getResultCode () {} - - /** - * (PECL memcached >= 1.0.0)
- * Return the message describing the result of the last operation - * @link https://php.net/manual/en/memcached.getresultmessage.php - * @return string Message describing the result of the last Memcached operation. - */ - public function getResultMessage () {} - - /** - * (PECL memcached >= 0.1.0)
- * Retrieve an item - * @link https://php.net/manual/en/memcached.get.php - * @param string $key

- * The key of the item to retrieve. - *

- * @param callable $cache_cb [optional]

- * Read-through caching callback or NULL. - *

- * @param int $flags [optional]

- * The flags for the get operation. - *

- * @return mixed the value stored in the cache or FALSE otherwise. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function get ($key, callable $cache_cb = null, $flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Retrieve an item from a specific server - * @link https://php.net/manual/en/memcached.getbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key of the item to fetch. - *

- * @param callable $cache_cb [optional]

- * Read-through caching callback or NULL - *

- * @param int $flags [optional]

- * The flags for the get operation. - *

- * @return mixed the value stored in the cache or FALSE otherwise. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function getByKey ($server_key, $key, callable $cache_cb = null, $flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Retrieve multiple items - * @link https://php.net/manual/en/memcached.getmulti.php - * @param array $keys

- * Array of keys to retrieve. - *

- * @param int $flags [optional]

- * The flags for the get operation. - *

- * @return mixed the array of found items or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function getMulti (array $keys, $flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Retrieve multiple items from a specific server - * @link https://php.net/manual/en/memcached.getmultibykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param array $keys

- * Array of keys to retrieve. - *

- * @param int $flags [optional]

- * The flags for the get operation. - *

- * @return array|false the array of found items or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function getMultiByKey ($server_key, array $keys, $flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Request multiple items - * @link https://php.net/manual/en/memcached.getdelayed.php - * @param array $keys

- * Array of keys to request. - *

- * @param bool $with_cas [optional]

- * Whether to request CAS token values also. - *

- * @param callable $value_cb [optional]

- * The result callback or NULL. - *

- * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function getDelayed (array $keys, $with_cas = null, callable $value_cb = null) {} - - /** - * (PECL memcached >= 0.1.0)
- * Request multiple items from a specific server - * @link https://php.net/manual/en/memcached.getdelayedbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param array $keys

- * Array of keys to request. - *

- * @param bool $with_cas [optional]

- * Whether to request CAS token values also. - *

- * @param callable $value_cb [optional]

- * The result callback or NULL. - *

- * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function getDelayedByKey ($server_key, array $keys, $with_cas = null, callable $value_cb = null) {} - - /** - * (PECL memcached >= 0.1.0)
- * Fetch the next result - * @link https://php.net/manual/en/memcached.fetch.php - * @return array|false the next result or FALSE otherwise. - * The Memcached::getResultCode will return - * Memcached::RES_END if result set is exhausted. - */ - public function fetch () {} - - /** - * (PECL memcached >= 0.1.0)
- * Fetch all the remaining results - * @link https://php.net/manual/en/memcached.fetchall.php - * @return array|false the results or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function fetchAll () {} - - /** - * (PECL memcached >= 0.1.0)
- * Store an item - * @link https://php.net/manual/en/memcached.set.php - * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function set ($key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Store an item on a specific server - * @link https://php.net/manual/en/memcached.setbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function setByKey ($server_key, $key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Set a new expiration on an item - * @link https://php.net/manual/en/memcached.touch.php - * @param string $key

- * The key under which to store the value. - *

- * @param int $expiration

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function touch ($key, $expiration = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Set a new expiration on an item on a specific server - * @link https://php.net/manual/en/memcached.touchbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param int $expiration

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function touchByKey ($server_key, $key, $expiration) {} - - /** - * (PECL memcached >= 0.1.0)
- * Store multiple items - * @link https://php.net/manual/en/memcached.setmulti.php - * @param array $items

- * An array of key/value pairs to store on the server. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function setMulti (array $items, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Store multiple items on a specific server - * @link https://php.net/manual/en/memcached.setmultibykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param array $items

- * An array of key/value pairs to store on the server. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function setMultiByKey ($server_key, array $items, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Compare and swap an item - * @link https://php.net/manual/en/memcached.cas.php - * @param float $cas_token

- * Unique value associated with the existing item. Generated by memcache. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_DATA_EXISTS if the item you are trying - * to store has been modified since you last fetched it. - */ - public function cas ($cas_token, $key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Compare and swap an item on a specific server - * @link https://php.net/manual/en/memcached.casbykey.php - * @param float $cas_token

- * Unique value associated with the existing item. Generated by memcache. - *

- * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_DATA_EXISTS if the item you are trying - * to store has been modified since you last fetched it. - */ - public function casByKey ($cas_token, $server_key, $key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Add an item under a new key - * @link https://php.net/manual/en/memcached.add.php - * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key already exists. - */ - public function add ($key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Add an item under a new key on a specific server - * @link https://php.net/manual/en/memcached.addbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key already exists. - */ - public function addByKey ($server_key, $key, $value, $expiration = 0, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Append data to an existing item - * @link https://php.net/manual/en/memcached.append.php - * @param string $key

- * The key under which to store the value. - *

- * @param string $value

- * The string to append. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function append ($key, $value) {} - - /** - * (PECL memcached >= 0.1.0)
- * Append data to an existing item on a specific server - * @link https://php.net/manual/en/memcached.appendbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param string $value

- * The string to append. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function appendByKey ($server_key, $key, $value) {} - - /** - * (PECL memcached >= 0.1.0)
- * Prepend data to an existing item - * @link https://php.net/manual/en/memcached.prepend.php - * @param string $key

- * The key of the item to prepend the data to. - *

- * @param string $value

- * The string to prepend. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function prepend ($key, $value) {} - - /** - * (PECL memcached >= 0.1.0)
- * Prepend data to an existing item on a specific server - * @link https://php.net/manual/en/memcached.prependbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key of the item to prepend the data to. - *

- * @param string $value

- * The string to prepend. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function prependByKey ($server_key, $key, $value) {} - - /** - * (PECL memcached >= 0.1.0)
- * Replace the item under an existing key - * @link https://php.net/manual/en/memcached.replace.php - * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function replace ($key, $value, $expiration = null, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Replace the item under an existing key on a specific server - * @link https://php.net/manual/en/memcached.replacebykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key under which to store the value. - *

- * @param mixed $value

- * The value to store. - *

- * @param int $expiration [optional]

- * The expiration time, defaults to 0. See Expiration Times for more info. - *

- * @param int $udf_flags [optional] - * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTSTORED if the key does not exist. - */ - public function replaceByKey ($server_key, $key, $value, $expiration = null, $udf_flags = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Delete an item - * @link https://php.net/manual/en/memcached.delete.php - * @param string $key

- * The key to be deleted. - *

- * @param int $time [optional]

- * The amount of time the server will wait to delete the item. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function delete ($key, $time = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Delete multiple items - * @link https://php.net/manual/en/memcached.deletemulti.php - * @param array $keys

- * The keys to be deleted. - *

- * @param int $time [optional]

- * The amount of time the server will wait to delete the items. - *

- * @return array Returns array indexed by keys and where values are indicating whether operation succeeded or not. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function deleteMulti (array $keys, $time = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Delete an item from a specific server - * @link https://php.net/manual/en/memcached.deletebykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key to be deleted. - *

- * @param int $time [optional]

- * The amount of time the server will wait to delete the item. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function deleteByKey ($server_key, $key, $time = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Delete multiple items from a specific server - * @link https://php.net/manual/en/memcached.deletemultibykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param array $keys

- * The keys to be deleted. - *

- * @param int $time [optional]

- * The amount of time the server will wait to delete the items. - *

- * @return bool TRUE on success or FALSE on failure. - * The Memcached::getResultCode will return - * Memcached::RES_NOTFOUND if the key does not exist. - */ - public function deleteMultiByKey ($server_key, array $keys, $time = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Increment numeric item's value - * @link https://php.net/manual/en/memcached.increment.php - * @param string $key

- * The key of the item to increment. - *

- * @param int $offset [optional]

- * The amount by which to increment the item's value. - *

- * @param int $initial_value [optional]

- * The value to set the item to if it doesn't currently exist. - *

- * @param int $expiry [optional]

- * The expiry time to set on the item. - *

- * @return int|false new item's value on success or FALSE on failure. - */ - public function increment ($key, $offset = 1, $initial_value = 0, $expiry = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Decrement numeric item's value - * @link https://php.net/manual/en/memcached.decrement.php - * @param string $key

- * The key of the item to decrement. - *

- * @param int $offset [optional]

- * The amount by which to decrement the item's value. - *

- * @param int $initial_value [optional]

- * The value to set the item to if it doesn't currently exist. - *

- * @param int $expiry [optional]

- * The expiry time to set on the item. - *

- * @return int|false item's new value on success or FALSE on failure. - */ - public function decrement ($key, $offset = 1, $initial_value = 0, $expiry = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Increment numeric item's value, stored on a specific server - * @link https://php.net/manual/en/memcached.incrementbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key of the item to increment. - *

- * @param int $offset [optional]

- * The amount by which to increment the item's value. - *

- * @param int $initial_value [optional]

- * The value to set the item to if it doesn't currently exist. - *

- * @param int $expiry [optional]

- * The expiry time to set on the item. - *

- * @return int|false new item's value on success or FALSE on failure. - */ - public function incrementByKey ($server_key, $key, $offset = 1, $initial_value = 0, $expiry = 0) {} - - /** - * (PECL memcached >= 2.0.0)
- * Decrement numeric item's value, stored on a specific server - * @link https://php.net/manual/en/memcached.decrementbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @param string $key

- * The key of the item to decrement. - *

- * @param int $offset [optional]

- * The amount by which to decrement the item's value. - *

- * @param int $initial_value [optional]

- * The value to set the item to if it doesn't currently exist. - *

- * @param int $expiry [optional]

- * The expiry time to set on the item. - *

- * @return int|false item's new value on success or FALSE on failure. - */ - public function decrementByKey ($server_key, $key, $offset = 1, $initial_value = 0, $expiry = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Add a server to the server pool - * @link https://php.net/manual/en/memcached.addserver.php - * @param string $host

- * The hostname of the memcache server. If the hostname is invalid, data-related - * operations will set - * Memcached::RES_HOST_LOOKUP_FAILURE result code. - *

- * @param int $port

- * The port on which memcache is running. Usually, this is - * 11211. - *

- * @param int $weight [optional]

- * The weight of the server relative to the total weight of all the - * servers in the pool. This controls the probability of the server being - * selected for operations. This is used only with consistent distribution - * option and usually corresponds to the amount of memory available to - * memcache on that server. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function addServer ($host, $port, $weight = 0) {} - - /** - * (PECL memcached >= 0.1.1)
- * Add multiple servers to the server pool - * @link https://php.net/manual/en/memcached.addservers.php - * @param array $servers - * @return bool TRUE on success or FALSE on failure. - */ - public function addServers (array $servers) {} - - /** - * (PECL memcached >= 0.1.0)
- * Get the list of the servers in the pool - * @link https://php.net/manual/en/memcached.getserverlist.php - * @return array The list of all servers in the server pool. - */ - public function getServerList () {} - - /** - * (PECL memcached >= 0.1.0)
- * Map a key to a server - * @link https://php.net/manual/en/memcached.getserverbykey.php - * @param string $server_key

- * The key identifying the server to store the value on or retrieve it from. Instead of hashing on the actual key for the item, we hash on the server key when deciding which memcached server to talk to. This allows related items to be grouped together on a single server for efficiency with multi operations. - *

- * @return array an array containing three keys of host, - * port, and weight on success or FALSE - * on failure. - * Use Memcached::getResultCode if necessary. - */ - public function getServerByKey ($server_key) {} - - /** - * (PECL memcached >= 2.0.0)
- * Clears all servers from the server list - * @link https://php.net/manual/en/memcached.resetserverlist.php - * @return bool TRUE on success or FALSE on failure. - */ - public function resetServerList () {} - - /** - * (PECL memcached >= 2.0.0)
- * Close any open connections - * @link https://php.net/manual/en/memcached.quit.php - * @return bool TRUE on success or FALSE on failure. - */ - public function quit () {} - - /** - * (PECL memcached >= 0.1.0)
- * Get server pool statistics - * @link https://php.net/manual/en/memcached.getstats.php - * @param string $type

items, slabs, sizes ...

- * @return array Array of server statistics, one entry per server. - */ - public function getStats ($type = null) {} - - /** - * (PECL memcached >= 0.1.5)
- * Get server pool version info - * @link https://php.net/manual/en/memcached.getversion.php - * @return array Array of server versions, one entry per server. - */ - public function getVersion () {} - - /** - * (PECL memcached >= 2.0.0)
- * Gets the keys stored on all the servers - * @link https://php.net/manual/en/memcached.getallkeys.php - * @return array|false the keys stored on all the servers on success or FALSE on failure. - */ - public function getAllKeys () {} - - /** - * (PECL memcached >= 0.1.0)
- * Invalidate all items in the cache - * @link https://php.net/manual/en/memcached.flush.php - * @param int $delay [optional]

- * Numer of seconds to wait before invalidating the items. - *

- * @return bool TRUE on success or FALSE on failure. - * Use Memcached::getResultCode if necessary. - */ - public function flush ($delay = 0) {} - - /** - * (PECL memcached >= 0.1.0)
- * Retrieve a Memcached option value - * @link https://php.net/manual/en/memcached.getoption.php - * @param int $option

- * One of the Memcached::OPT_* constants. - *

- * @return mixed the value of the requested option, or FALSE on - * error. - */ - public function getOption ($option) {} - - /** - * (PECL memcached >= 0.1.0)
- * Set a Memcached option - * @link https://php.net/manual/en/memcached.setoption.php - * @param int $option - * @param mixed $value - * @return bool TRUE on success or FALSE on failure. - */ - public function setOption ($option, $value) {} - - /** - * (PECL memcached >= 2.0.0)
- * Set Memcached options - * @link https://php.net/manual/en/memcached.setoptions.php - * @param array $options

- * An associative array of options where the key is the option to set and - * the value is the new value for the option. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setOptions (array $options) {} - - /** - * (PECL memcached >= 2.0.0)
- * Set the credentials to use for authentication - * @link https://www.php.net/manual/en/memcached.setsaslauthdata.php - * @param string $username

- * The username to use for authentication. - *

- * @param string $password

- * The password to use for authentication. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setSaslAuthData (string $username , string $password) {} - - /** - * (PECL memcached >= 2.0.0)
- * Check if a persitent connection to memcache is being used - * @link https://php.net/manual/en/memcached.ispersistent.php - * @return bool true if Memcache instance uses a persistent connection, false otherwise. - */ - public function isPersistent () {} - - /** - * (PECL memcached >= 2.0.0)
- * Check if the instance was recently created - * @link https://php.net/manual/en/memcached.ispristine.php - * @return bool the true if instance is recently created, false otherwise. - */ - public function isPristine () {} - - /** - * Flush and send buffered commands - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @return bool - */ - public function flushBuffers () {} - - /** - * Sets AES encryption key (libmemcached 1.0.6 and higher) - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @param string $key - * @return bool - */ - public function setEncodingKey ( $key ) {} - - /** - * Returns the last disconnected server. Was added in 0.34 according to libmemcached's Changelog - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @return array|false - */ - public function getLastDisconnectedServer () {} - - /** - * Returns the last error errno that occurred - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @return int - */ - public function getLastErrorErrno () {} - - /** - * Returns the last error code that occurred - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @return int - */ - public function getLastErrorCode () {} - - /** - * Returns the last error message that occurred - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @return string - */ - public function getLastErrorMessage () {} - - /** - * Sets the memcached virtual buckets - * @link https://github.com/php-memcached-dev/php-memcached/blob/v3.1.5/php_memcached.c - * @param array $host_map - * @param array $forward_map - * @param int $replicas - * @return bool - */ - public function setBucket (array $host_map, array $forward_map, $replicas) {} - -} - -/** - * @link https://php.net/manual/en/class.memcachedexception.php - */ -class MemcachedException extends RuntimeException { - function __construct( $errmsg = "", $errcode = 0 ) {} -} -// End of memcached v.3.1.5 -?> --- a/build/stubs/oca_talk_share_helper_shareapicontroller.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @link https://github.com/ukko/phpredis-phpdoc - */ -class Redis -{ - const AFTER = 'after'; - const BEFORE = 'before'; - - /** - * Options - */ - const OPT_SERIALIZER = 1; - const OPT_PREFIX = 2; - const OPT_READ_TIMEOUT = 3; - const OPT_SCAN = 4; - const OPT_FAILOVER = 5; - const OPT_TCP_KEEPALIVE = 6; - const OPT_COMPRESSION = 7; - const OPT_REPLY_LITERAL = 8; - const OPT_COMPRESSION_LEVEL = 9; - - /** - * Cluster options - */ - const FAILOVER_NONE = 0; - const FAILOVER_ERROR = 1; - const FAILOVER_DISTRIBUTE = 2; - const FAILOVER_DISTRIBUTE_SLAVES = 3; - - /** - * SCAN options - */ - const SCAN_NORETRY = 0; - const SCAN_RETRY = 1; - - /** - * @since 5.3.0 - */ - const SCAN_PREFIX = 2; - - /** - * @since 5.3.0 - */ - const SCAN_NOPREFIX = 3; - - /** - * Serializers - */ - const SERIALIZER_NONE = 0; - const SERIALIZER_PHP = 1; - const SERIALIZER_IGBINARY = 2; - const SERIALIZER_MSGPACK = 3; - const SERIALIZER_JSON = 4; - - /** - * Compressions - */ - const COMPRESSION_NONE = 0; - const COMPRESSION_LZF = 1; - const COMPRESSION_ZSTD = 2; - const COMPRESSION_LZ4 = 3; - - /** - * Compression ZSTD levels - */ - const COMPRESSION_ZSTD_MIN = 1; - const COMPRESSION_ZSTD_DEFAULT = 3; - const COMPRESSION_ZSTD_MAX = 22; - - /** - * Multi - */ - const ATOMIC = 0; - const MULTI = 1; - const PIPELINE = 2; - - /** - * Type - */ - const REDIS_NOT_FOUND = 0; - const REDIS_STRING = 1; - const REDIS_SET = 2; - const REDIS_LIST = 3; - const REDIS_ZSET = 4; - const REDIS_HASH = 5; - const REDIS_STREAM = 6; - - /** - * Creates a Redis client - * - * @example $redis = new Redis(); - */ - public function __construct() - { - } - - /** - * Connects to a Redis instance. - * - * @param string $host can be a host, or the path to a unix domain socket - * @param int $port optional - * @param float $timeout value in seconds (optional, default is 0.0 meaning unlimited) - * @param null $reserved should be null if $retryInterval is specified - * @param int $retryInterval retry interval in milliseconds. - * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited) - * @param array $connectionParameters extra config to send to redis - * - * @return bool TRUE on success, FALSE on error - * - * @example - *
-     * $redis->connect('127.0.0.1', 6379);
-     * $redis->connect('127.0.0.1');            // port 6379 by default
-     * $redis->connect('127.0.0.1', 6379, 2.5); // 2.5 sec timeout.
-     * $redis->connect('/tmp/redis.sock');      // unix domain socket.
-     * 
- */ - public function connect( - $host, - $port = 6379, - $timeout = 0.0, - $reserved = null, - $retryInterval = 0, - $readTimeout = 0.0, - $connectionParameters = [] - ) { - } - - /** - * Connects to a Redis instance. - * - * @param string $host can be a host, or the path to a unix domain socket - * @param int $port optional - * @param float $timeout value in seconds (optional, default is 0.0 meaning unlimited) - * @param null $reserved should be null if $retry_interval is specified - * @param int $retryInterval retry interval in milliseconds. - * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited) - * - * @return bool TRUE on success, FALSE on error - */ - #[Deprecated(replacement: '%class%->connect(%parametersList%)')] - public function open( - $host, - $port = 6379, - $timeout = 0.0, - $reserved = null, - $retryInterval = 0, - $readTimeout = 0.0 - ) { - } - - /** - * A method to determine if a phpredis object thinks it's connected to a server - * - * @return bool Returns TRUE if phpredis thinks it's connected and FALSE if not - */ - public function isConnected() - { - } - - /** - * Retrieve our host or unix socket that we're connected to - * - * @return string|false The host or unix socket we're connected to or FALSE if we're not connected - */ - public function getHost() - { - } - - /** - * Get the port we're connected to - * - * @return int|false Returns the port we're connected to or FALSE if we're not connected - */ - public function getPort() - { - } - - /** - * Get the database number phpredis is pointed to - * - * @return int|bool Returns the database number (int) phpredis thinks it's pointing to - * or FALSE if we're not connected - */ - public function getDbNum() - { - } - - /** - * Get the (write) timeout in use for phpredis - * - * @return float|false The timeout (DOUBLE) specified in our connect call or FALSE if we're not connected - */ - public function getTimeout() - { - } - - /** - * Get the read timeout specified to phpredis or FALSE if we're not connected - * - * @return float|bool Returns the read timeout (which can be set using setOption and Redis::OPT_READ_TIMEOUT) - * or FALSE if we're not connected - */ - public function getReadTimeout() - { - } - - /** - * Gets the persistent ID that phpredis is using - * - * @return string|null|bool Returns the persistent id phpredis is using - * (which will only be set if connected with pconnect), - * NULL if we're not using a persistent ID, - * and FALSE if we're not connected - */ - public function getPersistentID() - { - } - - /** - * Get the password used to authenticate the phpredis connection - * - * @return string|null|bool Returns the password used to authenticate a phpredis session or NULL if none was used, - * and FALSE if we're not connected - */ - public function getAuth() - { - } - - /** - * Connects to a Redis instance or reuse a connection already established with pconnect/popen. - * - * The connection will not be closed on close or end of request until the php process ends. - * So be patient on to many open FD's (specially on redis server side) when using persistent connections on - * many servers connecting to one redis server. - * - * Also more than one persistent connection can be made identified by either host + port + timeout - * or host + persistentId or unix socket + timeout. - * - * This feature is not available in threaded versions. pconnect and popen then working like their non persistent - * equivalents. - * - * @param string $host can be a host, or the path to a unix domain socket - * @param int $port optional - * @param float $timeout value in seconds (optional, default is 0 meaning unlimited) - * @param string $persistentId identity for the requested persistent connection - * @param int $retryInterval retry interval in milliseconds. - * @param float $readTimeout value in seconds (optional, default is 0 meaning unlimited) - * - * @return bool TRUE on success, FALSE on ertcnror. - * - * @example - *
-     * $redis->pconnect('127.0.0.1', 6379);
-     *
-     * // port 6379 by default - same connection like before
-     * $redis->pconnect('127.0.0.1');
-     *
-     * // 2.5 sec timeout and would be another connection than the two before.
-     * $redis->pconnect('127.0.0.1', 6379, 2.5);
-     *
-     * // x is sent as persistent_id and would be another connection than the three before.
-     * $redis->pconnect('127.0.0.1', 6379, 2.5, 'x');
-     *
-     * // unix domain socket - would be another connection than the four before.
-     * $redis->pconnect('/tmp/redis.sock');
-     * 
- */ - public function pconnect( - $host, - $port = 6379, - $timeout = 0.0, - $persistentId = null, - $retryInterval = 0, - $readTimeout = 0.0 - ) { - } - - /** - * @param string $host - * @param int $port - * @param float $timeout - * @param string $persistentId - * @param int $retryInterval - * @param float $readTimeout - * - * @return bool - */ - #[Deprecated(replacement: '%class%->pconnect(%parametersList%)')] - public function popen( - $host, - $port = 6379, - $timeout = 0.0, - $persistentId = '', - $retryInterval = 0, - $readTimeout = 0.0 - ) { - } - - /** - * Disconnects from the Redis instance. - * - * Note: Closing a persistent connection requires PhpRedis >= 4.2.0 - * - * @since >= 4.2 Closing a persistent connection requires PhpRedis - * - * @return bool TRUE on success, FALSE on error - */ - public function close() - { - } - - /** - * Swap one Redis database with another atomically - * - * Note: Requires Redis >= 4.0.0 - * - * @param int $db1 - * @param int $db2 - * - * @return bool TRUE on success and FALSE on failure - * - * @link https://redis.io/commands/swapdb - * @since >= 4.0 - * @example - *
-     * // Swaps DB 0 with DB 1 atomically
-     * $redis->swapdb(0, 1);
-     * 
- */ - public function swapdb(int $db1, int $db2) - { - } - - /** - * Set client option - * - * @param int $option option name - * @param mixed $value option value - * - * @return bool TRUE on success, FALSE on error - * - * @example - *
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);        // don't serialize data
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);         // use built-in serialize/unserialize
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY);    // use igBinary serialize/unserialize
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK);     // Use msgpack serialize/unserialize
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);        // Use json serialize/unserialize
-     *
-     * $redis->setOption(Redis::OPT_PREFIX, 'myAppName:');                      // use custom prefix on all keys
-     *
-     * // Options for the SCAN family of commands, indicating whether to abstract
-     * // empty results from the user.  If set to SCAN_NORETRY (the default), phpredis
-     * // will just issue one SCAN command at a time, sometimes returning an empty
-     * // array of results.  If set to SCAN_RETRY, phpredis will retry the scan command
-     * // until keys come back OR Redis returns an iterator of zero
-     * $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY);
-     * $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
-     * 
- */ - public function setOption($option, $value) - { - } - - /** - * Get client option - * - * @param int $option parameter name - * - * @return mixed|null Parameter value - * - * @see setOption() - * @example - * // return option value - * $redis->getOption(Redis::OPT_SERIALIZER); - */ - public function getOption($option) - { - } - - /** - * Check the current connection status - * - * @param string $message [optional] - * - * @return bool|string TRUE if the command is successful or returns message - * Throws a RedisException object on connectivity error, as described above. - * @throws RedisException - * @link https://redis.io/commands/ping - */ - public function ping($message = null) - { - } - - /** - * Echo the given string - * - * @param string $message - * - * @return string Returns message - * - * @link https://redis.io/commands/echo - */ - public function echo($message) - { - } - - /** - * Get the value related to the specified key - * - * @param string $key - * - * @return string|mixed|false If key didn't exist, FALSE is returned. - * Otherwise, the value related to this key is returned - * - * @link https://redis.io/commands/get - * @example - *
-     * $redis->set('key', 'hello');
-     * $redis->get('key');
-     *
-     * // set and get with serializer
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);
-     *
-     * $redis->set('key', ['asd' => 'as', 'dd' => 123, 'b' => true]);
-     * var_dump($redis->get('key'));
-     * // Output:
-     * array(3) {
-     *  'asd' => string(2) "as"
-     *  'dd' => int(123)
-     *  'b' => bool(true)
-     * }
-     * 
- */ - public function get($key) - { - } - - /** - * Set the string value in argument as value of the key. - * - * @since If you're using Redis >= 2.6.12, you can pass extended options as explained in example - * - * @param string $key - * @param string|mixed $value string if not used serializer - * @param int|array $timeout [optional] Calling setex() is preferred if you want a timeout.
- * Since 2.6.12 it also supports different flags inside an array. Example ['NX', 'EX' => 60]
- * - EX seconds -- Set the specified expire time, in seconds.
- * - PX milliseconds -- Set the specified expire time, in milliseconds.
- * - NX -- Only set the key if it does not already exist.
- * - XX -- Only set the key if it already exist.
- *
-     * // Simple key -> value set
-     * $redis->set('key', 'value');
-     *
-     * // Will redirect, and actually make an SETEX call
-     * $redis->set('key','value', 10);
-     *
-     * // Will set the key, if it doesn't exist, with a ttl of 10 seconds
-     * $redis->set('key', 'value', ['nx', 'ex' => 10]);
-     *
-     * // Will set a key, if it does exist, with a ttl of 1000 milliseconds
-     * $redis->set('key', 'value', ['xx', 'px' => 1000]);
-     * 
- * - * @return bool TRUE if the command is successful - * - * @link https://redis.io/commands/set - */ - public function set($key, $value, $timeout = null) - { - } - - /** - * Set the string value in argument as value of the key, with a time to live. - * - * @param string $key - * @param int $ttl - * @param string|mixed $value - * - * @return bool TRUE if the command is successful - * - * @link https://redis.io/commands/setex - * @example $redis->setex('key', 3600, 'value'); // sets key → value, with 1h TTL. - */ - public function setex($key, $ttl, $value) - { - } - - /** - * Set the value and expiration in milliseconds of a key. - * - * @see setex() - * @param string $key - * @param int $ttl, in milliseconds. - * @param string|mixed $value - * - * @return bool TRUE if the command is successful - * - * @link https://redis.io/commands/psetex - * @example $redis->psetex('key', 1000, 'value'); // sets key → value, with 1sec TTL. - */ - public function psetex($key, $ttl, $value) - { - } - - /** - * Set the string value in argument as value of the key if the key doesn't already exist in the database. - * - * @param string $key - * @param string|mixed $value - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/setnx - * @example - *
-     * $redis->setnx('key', 'value');   // return TRUE
-     * $redis->setnx('key', 'value');   // return FALSE
-     * 
- */ - public function setnx($key, $value) - { - } - - /** - * Remove specified keys. - * - * @param int|string|array $key1 An array of keys, or an undefined number of parameters, each a key: key1 key2 key3 ... keyN - * @param int|string ...$otherKeys - * - * @return int Number of keys deleted - * - * @link https://redis.io/commands/del - * @example - *
-     * $redis->set('key1', 'val1');
-     * $redis->set('key2', 'val2');
-     * $redis->set('key3', 'val3');
-     * $redis->set('key4', 'val4');
-     *
-     * $redis->del('key1', 'key2');     // return 2
-     * $redis->del(['key3', 'key4']);   // return 2
-     * 
- */ - public function del($key1, ...$otherKeys) - { - } - - /** - * @param string|string[] $key1 - * @param string $key2 - * @param string $key3 - * - * @return int Number of keys deleted - */ - #[Deprecated(replacement: "%class%->del(%parametersList%)")] - public function delete($key1, $key2 = null, $key3 = null) - { - } - - /** - * Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking. - * - * @see del() - * @param string|string[] $key1 - * @param string $key2 - * @param string $key3 - * - * @return int Number of keys unlinked. - * - * @link https://redis.io/commands/unlink - * @example - *
-     * $redis->set('key1', 'val1');
-     * $redis->set('key2', 'val2');
-     * $redis->set('key3', 'val3');
-     * $redis->set('key4', 'val4');
-     * $redis->unlink('key1', 'key2');          // return 2
-     * $redis->unlink(array('key3', 'key4'));   // return 2
-     * 
- */ - public function unlink($key1, $key2 = null, $key3 = null) - { - } - - /** - * Enter and exit transactional mode. - * - * @param int $mode Redis::MULTI|Redis::PIPELINE - * Defaults to Redis::MULTI. - * A Redis::MULTI block of commands runs as a single transaction; - * a Redis::PIPELINE block is simply transmitted faster to the server, but without any guarantee of atomicity. - * discard cancels a transaction. - * - * @return static returns the Redis instance and enters multi-mode. - * Once in multi-mode, all subsequent method calls return the same object until exec() is called. - * - * @link https://redis.io/commands/multi - * @example - *
-     * $ret = $redis->multi()
-     *      ->set('key1', 'val1')
-     *      ->get('key1')
-     *      ->set('key2', 'val2')
-     *      ->get('key2')
-     *      ->exec();
-     *
-     * //$ret == array (
-     * //    0 => TRUE,
-     * //    1 => 'val1',
-     * //    2 => TRUE,
-     * //    3 => 'val2');
-     * 
- */ - public function multi($mode = Redis::MULTI) - { - } - - /** - * Returns a Redis instance which can simply transmitted faster to the server. - * - * @return Redis returns the Redis instance. - * Once in pipeline-mode, all subsequent method calls return the same object until exec() is called. - * Pay attention, that Pipeline is not a transaction, so you can get unexpected - * results in case of big pipelines and small read/write timeouts. - * - * @link https://redis.io/topics/pipelining - * @example - *
-     * $ret = $this->redis->pipeline()
-     *      ->ping()
-     *      ->multi()->set('x', 42)->incr('x')->exec()
-     *      ->ping()
-     *      ->multi()->get('x')->del('x')->exec()
-     *      ->ping()
-     *      ->exec();
-     *
-     * //$ret == array (
-     * //    0 => '+PONG',
-     * //    1 => [TRUE, 43],
-     * //    2 => '+PONG',
-     * //    3 => [43, 1],
-     * //    4 => '+PONG');
-     * 
- */ - public function pipeline() - { - } - - - /** - * @return void|array - * - * @see multi() - * @link https://redis.io/commands/exec - */ - public function exec() - { - } - - /** - * @see multi() - * @link https://redis.io/commands/discard - */ - public function discard() - { - } - - /** - * Watches a key for modifications by another client. If the key is modified between WATCH and EXEC, - * the MULTI/EXEC transaction will fail (return FALSE). unwatch cancels all the watching of all keys by this client. - * @param string|string[] $key a list of keys - * - * @return void - * - * @link https://redis.io/commands/watch - * @example - *
-     * $redis->watch('x');
-     * // long code here during the execution of which other clients could well modify `x`
-     * $ret = $redis->multi()
-     *          ->incr('x')
-     *          ->exec();
-     * // $ret = FALSE if x has been modified between the call to WATCH and the call to EXEC.
-     * 
- */ - public function watch($key) - { - } - - /** - * @see watch() - * @link https://redis.io/commands/unwatch - */ - public function unwatch() - { - } - - /** - * Subscribe to channels. - * - * Warning: this function will probably change in the future. - * - * @param string[] $channels an array of channels to subscribe - * @param string|array $callback either a string or an array($instance, 'method_name'). - * The callback function receives 3 parameters: the redis instance, the channel name, and the message. - * - * @return mixed|null Any non-null return value in the callback will be returned to the caller. - * - * @link https://redis.io/commands/subscribe - * @example - *
-     * function f($redis, $chan, $msg) {
-     *  switch($chan) {
-     *      case 'chan-1':
-     *          ...
-     *          break;
-     *
-     *      case 'chan-2':
-     *                     ...
-     *          break;
-     *
-     *      case 'chan-2':
-     *          ...
-     *          break;
-     *      }
-     * }
-     *
-     * $redis->subscribe(array('chan-1', 'chan-2', 'chan-3'), 'f'); // subscribe to 3 chans
-     * 
- */ - public function subscribe($channels, $callback) - { - } - - /** - * Subscribe to channels by pattern - * - * @param array $patterns an array of glob-style patterns to subscribe - * @param string|array $callback Either a string or an array with an object and method. - * The callback will get four arguments ($redis, $pattern, $channel, $message) - * @param mixed Any non-null return value in the callback will be returned to the caller - * - * @link https://redis.io/commands/psubscribe - * @example - *
-     * function psubscribe($redis, $pattern, $chan, $msg) {
-     *  echo "Pattern: $pattern\n";
-     *  echo "Channel: $chan\n";
-     *  echo "Payload: $msg\n";
-     * }
-     * 
- */ - public function psubscribe($patterns, $callback) - { - } - - /** - * Publish messages to channels. - * - * Warning: this function will probably change in the future. - * - * @param string $channel a channel to publish to - * @param string $message string - * - * @return int Number of clients that received the message - * - * @link https://redis.io/commands/publish - * @example $redis->publish('chan-1', 'hello, world!'); // send message. - */ - public function publish($channel, $message) - { - } - - /** - * A command allowing you to get information on the Redis pub/sub system - * - * @param string $keyword String, which can be: "channels", "numsub", or "numpat" - * @param string|array $argument Optional, variant. - * For the "channels" subcommand, you can pass a string pattern. - * For "numsub" an array of channel names - * - * @return array|int Either an integer or an array. - * - channels Returns an array where the members are the matching channels. - * - numsub Returns a key/value array where the keys are channel names and - * values are their counts. - * - numpat Integer return containing the number active pattern subscriptions - * - * @link https://redis.io/commands/pubsub - * @example - *
-     * $redis->pubsub('channels'); // All channels
-     * $redis->pubsub('channels', '*pattern*'); // Just channels matching your pattern
-     * $redis->pubsub('numsub', array('chan1', 'chan2')); // Get subscriber counts for 'chan1' and 'chan2'
-     * $redis->pubsub('numpat'); // Get the number of pattern subscribers
-     * 
- */ - public function pubsub($keyword, $argument) - { - } - - /** - * Stop listening for messages posted to the given channels. - * - * @param array $channels an array of channels to usubscribe - * - * @link https://redis.io/commands/unsubscribe - */ - public function unsubscribe($channels = null) - { - } - - /** - * Stop listening for messages posted to the given channels. - * - * @param array $patterns an array of glob-style patterns to unsubscribe - * - * @link https://redis.io/commands/punsubscribe - */ - public function punsubscribe($patterns = null) - { - } - - /** - * Verify if the specified key/keys exists - * - * This function took a single argument and returned TRUE or FALSE in phpredis versions < 4.0.0. - * - * @since >= 4.0 Returned int, if < 4.0 returned bool - * - * @param string|string[] $key - * - * @return int|bool The number of keys tested that do exist - * - * @link https://redis.io/commands/exists - * @link https://github.com/phpredis/phpredis#exists - * @example - *
-     * $redis->exists('key'); // 1
-     * $redis->exists('NonExistingKey'); // 0
-     *
-     * $redis->mset(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz']);
-     * $redis->exists(['foo', 'bar', 'baz]); // 3
-     * $redis->exists('foo', 'bar', 'baz'); // 3
-     * 
- */ - public function exists($key) - { - } - - /** - * Increment the number stored at key by one. - * - * @param string $key - * - * @return int the new value - * - * @link https://redis.io/commands/incr - * @example - *
-     * $redis->incr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value 1
-     * $redis->incr('key1'); // 2
-     * $redis->incr('key1'); // 3
-     * $redis->incr('key1'); // 4
-     * 
- */ - public function incr($key) - { - } - - /** - * Increment the float value of a key by the given amount - * - * @param string $key - * @param float $increment - * - * @return float - * - * @link https://redis.io/commands/incrbyfloat - * @example - *
-     * $redis->set('x', 3);
-     * $redis->incrByFloat('x', 1.5);   // float(4.5)
-     * $redis->get('x');                // float(4.5)
-     * 
- */ - public function incrByFloat($key, $increment) - { - } - - /** - * Increment the number stored at key by one. - * If the second argument is filled, it will be used as the integer value of the increment. - * - * @param string $key key - * @param int $value value that will be added to key (only for incrBy) - * - * @return int the new value - * - * @link https://redis.io/commands/incrby - * @example - *
-     * $redis->incr('key1');        // key1 didn't exists, set to 0 before the increment and now has the value 1
-     * $redis->incr('key1');        // 2
-     * $redis->incr('key1');        // 3
-     * $redis->incr('key1');        // 4
-     * $redis->incrBy('key1', 10);  // 14
-     * 
- */ - public function incrBy($key, $value) - { - } - - /** - * Decrement the number stored at key by one. - * - * @param string $key - * - * @return int the new value - * - * @link https://redis.io/commands/decr - * @example - *
-     * $redis->decr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value -1
-     * $redis->decr('key1'); // -2
-     * $redis->decr('key1'); // -3
-     * 
- */ - public function decr($key) - { - } - - /** - * Decrement the number stored at key by one. - * If the second argument is filled, it will be used as the integer value of the decrement. - * - * @param string $key - * @param int $value that will be subtracted to key (only for decrBy) - * - * @return int the new value - * - * @link https://redis.io/commands/decrby - * @example - *
-     * $redis->decr('key1');        // key1 didn't exists, set to 0 before the increment and now has the value -1
-     * $redis->decr('key1');        // -2
-     * $redis->decr('key1');        // -3
-     * $redis->decrBy('key1', 10);  // -13
-     * 
- */ - public function decrBy($key, $value) - { - } - - /** - * Adds the string values to the head (left) of the list. - * Creates the list if the key didn't exist. - * If the key exists and is not a list, FALSE is returned. - * - * @param string $key - * @param string|mixed ...$value1 Variadic list of values to push in key, if dont used serialized, used string - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure - * - * @link https://redis.io/commands/lpush - * @example - *
-     * $redis->lPush('l', 'v1', 'v2', 'v3', 'v4')   // int(4)
-     * var_dump( $redis->lRange('l', 0, -1) );
-     * // Output:
-     * // array(4) {
-     * //   [0]=> string(2) "v4"
-     * //   [1]=> string(2) "v3"
-     * //   [2]=> string(2) "v2"
-     * //   [3]=> string(2) "v1"
-     * // }
-     * 
- */ - public function lPush($key, ...$value1) - { - } - - /** - * Adds the string values to the tail (right) of the list. - * Creates the list if the key didn't exist. - * If the key exists and is not a list, FALSE is returned. - * - * @param string $key - * @param string|mixed ...$value1 Variadic list of values to push in key, if dont used serialized, used string - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure - * - * @link https://redis.io/commands/rpush - * @example - *
-     * $redis->rPush('l', 'v1', 'v2', 'v3', 'v4');    // int(4)
-     * var_dump( $redis->lRange('l', 0, -1) );
-     * // Output:
-     * // array(4) {
-     * //   [0]=> string(2) "v1"
-     * //   [1]=> string(2) "v2"
-     * //   [2]=> string(2) "v3"
-     * //   [3]=> string(2) "v4"
-     * // }
-     * 
- */ - public function rPush($key, ...$value1) - { - } - - /** - * Adds the string value to the head (left) of the list if the list exists. - * - * @param string $key - * @param string|mixed $value String, value to push in key - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * - * @link https://redis.io/commands/lpushx - * @example - *
-     * $redis->del('key1');
-     * $redis->lPushx('key1', 'A');     // returns 0
-     * $redis->lPush('key1', 'A');      // returns 1
-     * $redis->lPushx('key1', 'B');     // returns 2
-     * $redis->lPushx('key1', 'C');     // returns 3
-     * // key1 now points to the following list: [ 'A', 'B', 'C' ]
-     * 
- */ - public function lPushx($key, $value) - { - } - - /** - * Adds the string value to the tail (right) of the list if the ist exists. FALSE in case of Failure. - * - * @param string $key - * @param string|mixed $value String, value to push in key - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * - * @link https://redis.io/commands/rpushx - * @example - *
-     * $redis->del('key1');
-     * $redis->rPushx('key1', 'A'); // returns 0
-     * $redis->rPush('key1', 'A'); // returns 1
-     * $redis->rPushx('key1', 'B'); // returns 2
-     * $redis->rPushx('key1', 'C'); // returns 3
-     * // key1 now points to the following list: [ 'A', 'B', 'C' ]
-     * 
- */ - public function rPushx($key, $value) - { - } - - /** - * Returns and removes the first element of the list. - * - * @param string $key - * - * @return mixed|bool if command executed successfully BOOL FALSE in case of failure (empty list) - * - * @link https://redis.io/commands/lpop - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redis->lPop('key1');        // key1 => [ 'B', 'C' ]
-     * 
- */ - public function lPop($key) - { - } - - /** - * Returns and removes the last element of the list. - * - * @param string $key - * - * @return mixed|bool if command executed successfully BOOL FALSE in case of failure (empty list) - * - * @link https://redis.io/commands/rpop - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redis->rPop('key1');        // key1 => [ 'A', 'B' ]
-     * 
- */ - public function rPop($key) - { - } - - /** - * Is a blocking lPop primitive. If at least one of the lists contains at least one element, - * the element will be popped from the head of the list and returned to the caller. - * Il all the list identified by the keys passed in arguments are empty, blPop will block - * during the specified timeout until an element is pushed to one of those lists. This element will be popped. - * - * @param string|string[] $keys String array containing the keys of the lists OR variadic list of strings - * @param int $timeout Timeout is always the required final parameter - * - * @return array ['listName', 'element'] - * - * @link https://redis.io/commands/blpop - * @example - *
-     * // Non blocking feature
-     * $redis->lPush('key1', 'A');
-     * $redis->del('key2');
-     *
-     * $redis->blPop('key1', 'key2', 10);        // array('key1', 'A')
-     * // OR
-     * $redis->blPop(['key1', 'key2'], 10);      // array('key1', 'A')
-     *
-     * $redis->brPop('key1', 'key2', 10);        // array('key1', 'A')
-     * // OR
-     * $redis->brPop(['key1', 'key2'], 10); // array('key1', 'A')
-     *
-     * // Blocking feature
-     *
-     * // process 1
-     * $redis->del('key1');
-     * $redis->blPop('key1', 10);
-     * // blocking for 10 seconds
-     *
-     * // process 2
-     * $redis->lPush('key1', 'A');
-     *
-     * // process 1
-     * // array('key1', 'A') is returned
-     * 
- */ - public function blPop($keys, $timeout) - { - } - - /** - * Is a blocking rPop primitive. If at least one of the lists contains at least one element, - * the element will be popped from the head of the list and returned to the caller. - * Il all the list identified by the keys passed in arguments are empty, brPop will - * block during the specified timeout until an element is pushed to one of those lists. T - * his element will be popped. - * - * @param string|string[] $keys String array containing the keys of the lists OR variadic list of strings - * @param int $timeout Timeout is always the required final parameter - * - * @return array ['listName', 'element'] - * - * @link https://redis.io/commands/brpop - * @example - *
-     * // Non blocking feature
-     * $redis->lPush('key1', 'A');
-     * $redis->del('key2');
-     *
-     * $redis->blPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redis->blPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * $redis->brPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redis->brPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * // Blocking feature
-     *
-     * // process 1
-     * $redis->del('key1');
-     * $redis->blPop('key1', 10);
-     * // blocking for 10 seconds
-     *
-     * // process 2
-     * $redis->lPush('key1', 'A');
-     *
-     * // process 1
-     * // array('key1', 'A') is returned
-     * 
- */ - public function brPop(array $keys, $timeout) - { - } - - /** - * Returns the size of a list identified by Key. If the list didn't exist or is empty, - * the command returns 0. If the data type identified by Key is not a list, the command return FALSE. - * - * @param string $key - * - * @return int|bool The size of the list identified by Key exists. - * bool FALSE if the data type identified by Key is not list - * - * @link https://redis.io/commands/llen - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C'); // key1 => [ 'A', 'B', 'C' ]
-     * $redis->lLen('key1');       // 3
-     * $redis->rPop('key1');
-     * $redis->lLen('key1');       // 2
-     * 
- */ - public function lLen($key) - { - } - - /** - * @link https://redis.io/commands/llen - * - * @param string $key - * - * @return int The size of the list identified by Key exists - */ - #[Deprecated(replacement: '%class%->lLen(%parametersList%)')] - public function lSize($key) - { - } - - /** - * Return the specified element of the list stored at the specified key. - * 0 the first element, 1 the second ... -1 the last element, -2 the penultimate ... - * Return FALSE in case of a bad index or a key that doesn't point to a list. - * - * @param string $key - * @param int $index - * - * @return mixed|bool the element at this index - * - * Bool FALSE if the key identifies a non-string data type, or no value corresponds to this index in the list Key. - * - * @link https://redis.io/commands/lindex - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redis->lIndex('key1', 0);     // 'A'
-     * $redis->lIndex('key1', -1);    // 'C'
-     * $redis->lIndex('key1', 10);    // `FALSE`
-     * 
- */ - public function lIndex($key, $index) - { - } - - /** - * @link https://redis.io/commands/lindex - * - * @param string $key - * @param int $index - * @return mixed|bool the element at this index - */ - #[Deprecated(replacement: '%class%->lIndex(%parametersList%)')] - public function lGet($key, $index) - { - } - - /** - * Set the list at index with the new value. - * - * @param string $key - * @param int $index - * @param string $value - * - * @return bool TRUE if the new value is setted. - * FALSE if the index is out of range, or data type identified by key is not a list. - * - * @link https://redis.io/commands/lset - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');    // key1 => [ 'A', 'B', 'C' ]
-     * $redis->lIndex('key1', 0);     // 'A'
-     * $redis->lSet('key1', 0, 'X');
-     * $redis->lIndex('key1', 0);     // 'X'
-     * 
- */ - public function lSet($key, $index, $value) - { - } - - /** - * Returns the specified elements of the list stored at the specified key in - * the range [start, end]. start and stop are interpretated as indices: 0 the first element, - * 1 the second ... -1 the last element, -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * - * @return array containing the values in specified range. - * - * @link https://redis.io/commands/lrange - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');
-     * $redis->lRange('key1', 0, -1); // array('A', 'B', 'C')
-     * 
- */ - public function lRange($key, $start, $end) - { - } - - /** - * @link https://redis.io/commands/lrange - * - * @param string $key - * @param int $start - * @param int $end - * @return array - */ - #[Deprecated(replacement: '%class%->lRange(%parametersList%)')] - public function lGetRange($key, $start, $end) - { - } - - /** - * Trims an existing list so that it will contain only a specified range of elements. - * - * @param string $key - * @param int $start - * @param int $stop - * - * @return array|false Bool return FALSE if the key identify a non-list value - * - * @link https://redis.io/commands/ltrim - * @example - *
-     * $redis->rPush('key1', 'A');
-     * $redis->rPush('key1', 'B');
-     * $redis->rPush('key1', 'C');
-     * $redis->lRange('key1', 0, -1); // array('A', 'B', 'C')
-     * $redis->lTrim('key1', 0, 1);
-     * $redis->lRange('key1', 0, -1); // array('A', 'B')
-     * 
- */ - public function lTrim($key, $start, $stop) - { - } - - /** - * @link https://redis.io/commands/ltrim - * - * @param string $key - * @param int $start - * @param int $stop - */ - #[Deprecated(replacement: '%class%->lTrim(%parametersList%)')] - public function listTrim($key, $start, $stop) - { - } - - /** - * Removes the first count occurrences of the value element from the list. - * If count is zero, all the matching elements are removed. If count is negative, - * elements are removed from tail to head. - * - * @param string $key - * @param string $value - * @param int $count - * - * @return int|bool the number of elements to remove - * bool FALSE if the value identified by key is not a list. - * - * @link https://redis.io/commands/lrem - * @example - *
-     * $redis->lPush('key1', 'A');
-     * $redis->lPush('key1', 'B');
-     * $redis->lPush('key1', 'C');
-     * $redis->lPush('key1', 'A');
-     * $redis->lPush('key1', 'A');
-     *
-     * $redis->lRange('key1', 0, -1);   // array('A', 'A', 'C', 'B', 'A')
-     * $redis->lRem('key1', 'A', 2);    // 2
-     * $redis->lRange('key1', 0, -1);   // array('C', 'B', 'A')
-     * 
- */ - public function lRem($key, $value, $count) - { - } - - /** - * @link https://redis.io/commands/lremove - * - * @param string $key - * @param string $value - * @param int $count - */ - #[Deprecated(replacement: '%class%->lRem(%parametersList%)')] - public function lRemove($key, $value, $count) - { - } - - /** - * Insert value in the list before or after the pivot value. the parameter options - * specify the position of the insert (before or after). If the list didn't exists, - * or the pivot didn't exists, the value is not inserted. - * - * @param string $key - * @param int $position Redis::BEFORE | Redis::AFTER - * @param string $pivot - * @param string|mixed $value - * - * @return int The number of the elements in the list, -1 if the pivot didn't exists. - * - * @link https://redis.io/commands/linsert - * @example - *
-     * $redis->del('key1');
-     * $redis->lInsert('key1', Redis::AFTER, 'A', 'X');     // 0
-     *
-     * $redis->lPush('key1', 'A');
-     * $redis->lPush('key1', 'B');
-     * $redis->lPush('key1', 'C');
-     *
-     * $redis->lInsert('key1', Redis::BEFORE, 'C', 'X');    // 4
-     * $redis->lRange('key1', 0, -1);                       // array('A', 'B', 'X', 'C')
-     *
-     * $redis->lInsert('key1', Redis::AFTER, 'C', 'Y');     // 5
-     * $redis->lRange('key1', 0, -1);                       // array('A', 'B', 'X', 'C', 'Y')
-     *
-     * $redis->lInsert('key1', Redis::AFTER, 'W', 'value'); // -1
-     * 
- */ - public function lInsert($key, $position, $pivot, $value) - { - } - - /** - * Adds a values to the set value stored at key. - * - * @param string $key Required key - * @param string|mixed ...$value1 Variadic list of values - * - * @return int|bool The number of elements added to the set. - * If this value is already in the set, FALSE is returned - * - * @link https://redis.io/commands/sadd - * @example - *
-     * $redis->sAdd('k', 'v1');                // int(1)
-     * $redis->sAdd('k', 'v1', 'v2', 'v3');    // int(2)
-     * 
- */ - public function sAdd($key, ...$value1) - { - } - - /** - * Removes the specified members from the set value stored at key. - * - * @param string $key - * @param string|mixed ...$member1 Variadic list of members - * - * @return int The number of elements removed from the set - * - * @link https://redis.io/commands/srem - * @example - *
-     * var_dump( $redis->sAdd('k', 'v1', 'v2', 'v3') );    // int(3)
-     * var_dump( $redis->sRem('k', 'v2', 'v3') );          // int(2)
-     * var_dump( $redis->sMembers('k') );
-     * //// Output:
-     * // array(1) {
-     * //   [0]=> string(2) "v1"
-     * // }
-     * 
- */ - public function sRem($key, ...$member1) - { - } - - /** - * @link https://redis.io/commands/srem - * - * @param string $key - * @param string|mixed ...$member1 - */ - #[Deprecated(replacement: '%class%->sRem(%parametersList%)')] - public function sRemove($key, ...$member1) - { - } - - /** - * Moves the specified member from the set at srcKey to the set at dstKey. - * - * @param string $srcKey - * @param string $dstKey - * @param string|mixed $member - * - * @return bool If the operation is successful, return TRUE. - * If the srcKey and/or dstKey didn't exist, and/or the member didn't exist in srcKey, FALSE is returned. - * - * @link https://redis.io/commands/smove - * @example - *
-     * $redis->sAdd('key1' , 'set11');
-     * $redis->sAdd('key1' , 'set12');
-     * $redis->sAdd('key1' , 'set13');          // 'key1' => {'set11', 'set12', 'set13'}
-     * $redis->sAdd('key2' , 'set21');
-     * $redis->sAdd('key2' , 'set22');          // 'key2' => {'set21', 'set22'}
-     * $redis->sMove('key1', 'key2', 'set13');  // 'key1' =>  {'set11', 'set12'}
-     *                                          // 'key2' =>  {'set21', 'set22', 'set13'}
-     * 
- */ - public function sMove($srcKey, $dstKey, $member) - { - } - - /** - * Checks if value is a member of the set stored at the key key. - * - * @param string $key - * @param string|mixed $value - * - * @return bool TRUE if value is a member of the set at key key, FALSE otherwise - * - * @link https://redis.io/commands/sismember - * @example - *
-     * $redis->sAdd('key1' , 'set1');
-     * $redis->sAdd('key1' , 'set2');
-     * $redis->sAdd('key1' , 'set3'); // 'key1' => {'set1', 'set2', 'set3'}
-     *
-     * $redis->sIsMember('key1', 'set1'); // TRUE
-     * $redis->sIsMember('key1', 'setX'); // FALSE
-     * 
- */ - public function sIsMember($key, $value) - { - } - - /** - * @link https://redis.io/commands/sismember - * - * @param string $key - * @param string|mixed $value - */ - #[Deprecated(replacement: '%class%->sIsMember(%parametersList%)')] - public function sContains($key, $value) - { - } - - /** - * Returns the cardinality of the set identified by key. - * - * @param string $key - * - * @return int the cardinality of the set identified by key, 0 if the set doesn't exist. - * - * @link https://redis.io/commands/scard - * @example - *
-     * $redis->sAdd('key1' , 'set1');
-     * $redis->sAdd('key1' , 'set2');
-     * $redis->sAdd('key1' , 'set3');   // 'key1' => {'set1', 'set2', 'set3'}
-     * $redis->sCard('key1');           // 3
-     * $redis->sCard('keyX');           // 0
-     * 
- */ - public function sCard($key) - { - } - - /** - * Removes and returns a random element from the set value at Key. - * - * @param string $key - * @param int $count [optional] - * - * @return string|mixed|array|bool "popped" values - * bool FALSE if set identified by key is empty or doesn't exist. - * - * @link https://redis.io/commands/spop - * @example - *
-     * $redis->sAdd('key1' , 'set1');
-     * $redis->sAdd('key1' , 'set2');
-     * $redis->sAdd('key1' , 'set3');   // 'key1' => {'set3', 'set1', 'set2'}
-     * $redis->sPop('key1');            // 'set1', 'key1' => {'set3', 'set2'}
-     * $redis->sPop('key1');            // 'set3', 'key1' => {'set2'}
-     *
-     * // With count
-     * $redis->sAdd('key2', 'set1', 'set2', 'set3');
-     * var_dump( $redis->sPop('key2', 3) ); // Will return all members but in no particular order
-     *
-     * // array(3) {
-     * //   [0]=> string(4) "set2"
-     * //   [1]=> string(4) "set3"
-     * //   [2]=> string(4) "set1"
-     * // }
-     * 
- */ - public function sPop($key, $count = 1) - { - } - - /** - * Returns a random element(s) from the set value at Key, without removing it. - * - * @param string $key - * @param int $count [optional] - * - * @return string|mixed|array|bool value(s) from the set - * bool FALSE if set identified by key is empty or doesn't exist and count argument isn't passed. - * - * @link https://redis.io/commands/srandmember - * @example - *
-     * $redis->sAdd('key1' , 'one');
-     * $redis->sAdd('key1' , 'two');
-     * $redis->sAdd('key1' , 'three');              // 'key1' => {'one', 'two', 'three'}
-     *
-     * var_dump( $redis->sRandMember('key1') );     // 'key1' => {'one', 'two', 'three'}
-     *
-     * // string(5) "three"
-     *
-     * var_dump( $redis->sRandMember('key1', 2) );  // 'key1' => {'one', 'two', 'three'}
-     *
-     * // array(2) {
-     * //   [0]=> string(2) "one"
-     * //   [1]=> string(5) "three"
-     * // }
-     * 
- */ - public function sRandMember($key, $count = 1) - { - } - - /** - * Returns the members of a set resulting from the intersection of all the sets - * held at the specified keys. If just a single key is specified, then this command - * produces the members of this set. If one of the keys is missing, FALSE is returned. - * - * @param string $key1 keys identifying the different sets on which we will apply the intersection. - * @param string ...$otherKeys variadic list of keys - * - * @return array|false contain the result of the intersection between those keys - * If the intersection between the different sets is empty, the return value will be empty array. - * - * @link https://redis.io/commands/sinter - * @example - *
-     * $redis->sAdd('key1', 'val1');
-     * $redis->sAdd('key1', 'val2');
-     * $redis->sAdd('key1', 'val3');
-     * $redis->sAdd('key1', 'val4');
-     *
-     * $redis->sAdd('key2', 'val3');
-     * $redis->sAdd('key2', 'val4');
-     *
-     * $redis->sAdd('key3', 'val3');
-     * $redis->sAdd('key3', 'val4');
-     *
-     * var_dump($redis->sInter('key1', 'key2', 'key3'));
-     *
-     * //array(2) {
-     * //  [0]=>
-     * //  string(4) "val4"
-     * //  [1]=>
-     * //  string(4) "val3"
-     * //}
-     * 
- */ - public function sInter($key1, ...$otherKeys) - { - } - - /** - * Performs a sInter command and stores the result in a new set. - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 keys identifying the different sets on which we will apply the intersection. - * @param string ...$otherKeys variadic list of keys - * - * @return int|false The cardinality of the resulting set, or FALSE in case of a missing key - * - * @link https://redis.io/commands/sinterstore - * @example - *
-     * $redis->sAdd('key1', 'val1');
-     * $redis->sAdd('key1', 'val2');
-     * $redis->sAdd('key1', 'val3');
-     * $redis->sAdd('key1', 'val4');
-     *
-     * $redis->sAdd('key2', 'val3');
-     * $redis->sAdd('key2', 'val4');
-     *
-     * $redis->sAdd('key3', 'val3');
-     * $redis->sAdd('key3', 'val4');
-     *
-     * var_dump($redis->sInterStore('output', 'key1', 'key2', 'key3'));
-     * var_dump($redis->sMembers('output'));
-     *
-     * //int(2)
-     * //
-     * //array(2) {
-     * //  [0]=>
-     * //  string(4) "val4"
-     * //  [1]=>
-     * //  string(4) "val3"
-     * //}
-     * 
- */ - public function sInterStore($dstKey, $key1, ...$otherKeys) - { - } - - /** - * Performs the union between N sets and returns it. - * - * @param string $key1 first key for union - * @param string ...$otherKeys variadic list of keys corresponding to sets in redis - * - * @return array string[] The union of all these sets - * - * @link https://redis.io/commands/sunionstore - * @example - *
-     * $redis->sAdd('s0', '1');
-     * $redis->sAdd('s0', '2');
-     * $redis->sAdd('s1', '3');
-     * $redis->sAdd('s1', '1');
-     * $redis->sAdd('s2', '3');
-     * $redis->sAdd('s2', '4');
-     *
-     * var_dump($redis->sUnion('s0', 's1', 's2'));
-     *
-     * array(4) {
-     * //  [0]=>
-     * //  string(1) "3"
-     * //  [1]=>
-     * //  string(1) "4"
-     * //  [2]=>
-     * //  string(1) "1"
-     * //  [3]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sUnion($key1, ...$otherKeys) - { - } - - /** - * Performs the same action as sUnion, but stores the result in the first key - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 first key for union - * @param string ...$otherKeys variadic list of keys corresponding to sets in redis - * - * @return int Any number of keys corresponding to sets in redis - * - * @link https://redis.io/commands/sunionstore - * @example - *
-     * $redis->del('s0', 's1', 's2');
-     *
-     * $redis->sAdd('s0', '1');
-     * $redis->sAdd('s0', '2');
-     * $redis->sAdd('s1', '3');
-     * $redis->sAdd('s1', '1');
-     * $redis->sAdd('s2', '3');
-     * $redis->sAdd('s2', '4');
-     *
-     * var_dump($redis->sUnionStore('dst', 's0', 's1', 's2'));
-     * var_dump($redis->sMembers('dst'));
-     *
-     * //int(4)
-     * //array(4) {
-     * //  [0]=>
-     * //  string(1) "3"
-     * //  [1]=>
-     * //  string(1) "4"
-     * //  [2]=>
-     * //  string(1) "1"
-     * //  [3]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sUnionStore($dstKey, $key1, ...$otherKeys) - { - } - - /** - * Performs the difference between N sets and returns it. - * - * @param string $key1 first key for diff - * @param string ...$otherKeys variadic list of keys corresponding to sets in redis - * - * @return array string[] The difference of the first set will all the others - * - * @link https://redis.io/commands/sdiff - * @example - *
-     * $redis->del('s0', 's1', 's2');
-     *
-     * $redis->sAdd('s0', '1');
-     * $redis->sAdd('s0', '2');
-     * $redis->sAdd('s0', '3');
-     * $redis->sAdd('s0', '4');
-     *
-     * $redis->sAdd('s1', '1');
-     * $redis->sAdd('s2', '3');
-     *
-     * var_dump($redis->sDiff('s0', 's1', 's2'));
-     *
-     * //array(2) {
-     * //  [0]=>
-     * //  string(1) "4"
-     * //  [1]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sDiff($key1, ...$otherKeys) - { - } - - /** - * Performs the same action as sDiff, but stores the result in the first key - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 first key for diff - * @param string ...$otherKeys variadic list of keys corresponding to sets in redis - * - * @return int|false The cardinality of the resulting set, or FALSE in case of a missing key - * - * @link https://redis.io/commands/sdiffstore - * @example - *
-     * $redis->del('s0', 's1', 's2');
-     *
-     * $redis->sAdd('s0', '1');
-     * $redis->sAdd('s0', '2');
-     * $redis->sAdd('s0', '3');
-     * $redis->sAdd('s0', '4');
-     *
-     * $redis->sAdd('s1', '1');
-     * $redis->sAdd('s2', '3');
-     *
-     * var_dump($redis->sDiffStore('dst', 's0', 's1', 's2'));
-     * var_dump($redis->sMembers('dst'));
-     *
-     * //int(2)
-     * //array(2) {
-     * //  [0]=>
-     * //  string(1) "4"
-     * //  [1]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sDiffStore($dstKey, $key1, ...$otherKeys) - { - } - - /** - * Returns the contents of a set. - * - * @param string $key - * - * @return array An array of elements, the contents of the set - * - * @link https://redis.io/commands/smembers - * @example - *
-     * $redis->del('s');
-     * $redis->sAdd('s', 'a');
-     * $redis->sAdd('s', 'b');
-     * $redis->sAdd('s', 'a');
-     * $redis->sAdd('s', 'c');
-     * var_dump($redis->sMembers('s'));
-     *
-     * //array(3) {
-     * //  [0]=>
-     * //  string(1) "c"
-     * //  [1]=>
-     * //  string(1) "a"
-     * //  [2]=>
-     * //  string(1) "b"
-     * //}
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function sMembers($key) - { - } - - /** - * @link https://redis.io/commands/smembers - * - * @param string $key - * @return array An array of elements, the contents of the set - */ - #[Deprecated(replacement: '%class%->sMembers(%parametersList%)')] - public function sGetMembers($key) - { - } - - /** - * Scan a set for members - * - * @param string $key The set to search. - * @param int &$iterator LONG (reference) to the iterator as we go. - * @param string $pattern String, optional pattern to match against. - * @param int $count How many members to return at a time (Redis might return a different amount) - * - * @return array|false PHPRedis will return an array of keys or FALSE when we're done iterating - * - * @link https://redis.io/commands/sscan - * @example - *
-     * $iterator = null;
-     * while ($members = $redis->sScan('set', $iterator)) {
-     *     foreach ($members as $member) {
-     *         echo $member . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function sScan($key, &$iterator, $pattern = null, $count = 0) - { - } - - /** - * Sets a value and returns the previous entry at that key. - * - * @param string $key - * @param string|mixed $value - * - * @return string|mixed A string (mixed, if used serializer), the previous value located at this key - * - * @link https://redis.io/commands/getset - * @example - *
-     * $redis->set('x', '42');
-     * $exValue = $redis->getSet('x', 'lol');   // return '42', replaces x by 'lol'
-     * $newValue = $redis->get('x')'            // return 'lol'
-     * 
- */ - public function getSet($key, $value) - { - } - - /** - * Returns a random key - * - * @return string an existing key in redis - * - * @link https://redis.io/commands/randomkey - * @example - *
-     * $key = $redis->randomKey();
-     * $surprise = $redis->get($key);  // who knows what's in there.
-     * 
- */ - public function randomKey() - { - } - - /** - * Switches to a given database - * - * @param int $dbIndex - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/select - * @example - *
-     * $redis->select(0);       // switch to DB 0
-     * $redis->set('x', '42');  // write 42 to x
-     * $redis->move('x', 1);    // move to DB 1
-     * $redis->select(1);       // switch to DB 1
-     * $redis->get('x');        // will return 42
-     * 
- */ - public function select($dbIndex) - { - } - - /** - * Moves a key to a different database. - * - * @param string $key - * @param int $dbIndex - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/move - * @example - *
-     * $redis->select(0);       // switch to DB 0
-     * $redis->set('x', '42');  // write 42 to x
-     * $redis->move('x', 1);    // move to DB 1
-     * $redis->select(1);       // switch to DB 1
-     * $redis->get('x');        // will return 42
-     * 
- */ - public function move($key, $dbIndex) - { - } - - /** - * Renames a key - * - * @param string $srcKey - * @param string $dstKey - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/rename - * @example - *
-     * $redis->set('x', '42');
-     * $redis->rename('x', 'y');
-     * $redis->get('y');   // → 42
-     * $redis->get('x');   // → `FALSE`
-     * 
- */ - public function rename($srcKey, $dstKey) - { - } - - /** - * @link https://redis.io/commands/rename - * - * @param string $srcKey - * @param string $dstKey - */ - #[Deprecated(replacement: '%class%->rename(%parametersList%)')] - public function renameKey($srcKey, $dstKey) - { - } - - /** - * Renames a key - * - * Same as rename, but will not replace a key if the destination already exists. - * This is the same behaviour as setNx. - * - * @param string $srcKey - * @param string $dstKey - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/renamenx - * @example - *
-     * $redis->set('x', '42');
-     * $redis->rename('x', 'y');
-     * $redis->get('y');   // → 42
-     * $redis->get('x');   // → `FALSE`
-     * 
- */ - public function renameNx($srcKey, $dstKey) - { - } - - /** - * Sets an expiration date (a timeout) on an item - * - * @param string $key The key that will disappear - * @param int $ttl The key's remaining Time To Live, in seconds - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/expire - * @example - *
-     * $redis->set('x', '42');
-     * $redis->expire('x', 3);  // x will disappear in 3 seconds.
-     * sleep(5);                    // wait 5 seconds
-     * $redis->get('x');            // will return `FALSE`, as 'x' has expired.
-     * 
- */ - public function expire($key, $ttl) - { - } - - /** - * Sets an expiration date (a timeout in milliseconds) on an item - * - * @param string $key The key that will disappear. - * @param int $ttl The key's remaining Time To Live, in milliseconds - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/pexpire - * @example - *
-     * $redis->set('x', '42');
-     * $redis->pExpire('x', 11500); // x will disappear in 11500 milliseconds.
-     * $redis->ttl('x');            // 12
-     * $redis->pttl('x');           // 11500
-     * 
- */ - public function pExpire($key, $ttl) - { - } - - /** - * @link https://redis.io/commands/expire - * - * @param string $key - * @param int $ttl - * @return bool - */ - #[Deprecated(replacement: '%class%->expire(%parametersList%)')] - public function setTimeout($key, $ttl) - { - } - - /** - * Sets an expiration date (a timestamp) on an item. - * - * @param string $key The key that will disappear. - * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time. - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/expireat - * @example - *
-     * $redis->set('x', '42');
-     * $now = time(NULL);               // current timestamp
-     * $redis->expireAt('x', $now + 3); // x will disappear in 3 seconds.
-     * sleep(5);                        // wait 5 seconds
-     * $redis->get('x');                // will return `FALSE`, as 'x' has expired.
-     * 
- */ - public function expireAt($key, $timestamp) - { - } - - /** - * Sets an expiration date (a timestamp) on an item. Requires a timestamp in milliseconds - * - * @param string $key The key that will disappear - * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/pexpireat - * @example - *
-     * $redis->set('x', '42');
-     * $redis->pExpireAt('x', 1555555555005);
-     * echo $redis->ttl('x');                       // 218270121
-     * echo $redis->pttl('x');                      // 218270120575
-     * 
- */ - public function pExpireAt($key, $timestamp) - { - } - - /** - * Returns the keys that match a certain pattern. - * - * @param string $pattern pattern, using '*' as a wildcard - * - * @return string[]|false The keys that match a certain pattern. - * - * @link https://redis.io/commands/keys - * @example - *
-     * $allKeys = $redis->keys('*');   // all keys will match this.
-     * $keyWithUserPrefix = $redis->keys('user*');
-     * 
- */ - public function keys($pattern) - { - } - - /** - * @param string $pattern - * @link https://redis.io/commands/keys - */ - #[Deprecated(replacement: '%class%->keys(%parametersList%)')] - public function getKeys($pattern) - { - } - - /** - * Returns the current database's size - * - * @return int DB size, in number of keys - * - * @link https://redis.io/commands/dbsize - * @example - *
-     * $count = $redis->dbSize();
-     * echo "Redis has $count keys\n";
-     * 
- */ - public function dbSize() - { - } - - /** - * Authenticate the connection using a password. - * Warning: The password is sent in plain-text over the network. - * - * @param string|string[] $password - * - * @return bool TRUE if the connection is authenticated, FALSE otherwise - * - * @link https://redis.io/commands/auth - * @example $redis->auth('foobared'); - */ - public function auth($password) - { - } - - /** - * Starts the background rewrite of AOF (Append-Only File) - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/bgrewriteaof - * @example $redis->bgrewriteaof(); - */ - public function bgrewriteaof() - { - } - - /** - * Changes the slave status - * Either host and port, or no parameter to stop being a slave. - * - * @param string $host [optional] - * @param int $port [optional] - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/slaveof - * @example - *
-     * $redis->slaveof('10.0.1.7', 6379);
-     * // ...
-     * $redis->slaveof();
-     * 
- */ - public function slaveof($host = '127.0.0.1', $port = 6379) - { - } - - /** - * Access the Redis slowLog - * - * @param string $operation This can be either GET, LEN, or RESET - * @param int|null $length If executing a SLOWLOG GET command, you can pass an optional length. - * - * @return mixed The return value of SLOWLOG will depend on which operation was performed. - * - SLOWLOG GET: Array of slowLog entries, as provided by Redis - * - SLOGLOG LEN: Integer, the length of the slowLog - * - SLOWLOG RESET: Boolean, depending on success - * - * @example - *
-     * // Get ten slowLog entries
-     * $redis->slowLog('get', 10);
-     * // Get the default number of slowLog entries
-     *
-     * $redis->slowLog('get');
-     * // Reset our slowLog
-     * $redis->slowLog('reset');
-     *
-     * // Retrieve slowLog length
-     * $redis->slowLog('len');
-     * 
- * - * @link https://redis.io/commands/slowlog - */ - public function slowLog(string $operation, int $length = null) - { - } - - - /** - * Describes the object pointed to by a key. - * The information to retrieve (string) and the key (string). - * Info can be one of the following: - * - "encoding" - * - "refcount" - * - "idletime" - * - * @param string $string - * @param string $key - * - * @return string|int|false for "encoding", int for "refcount" and "idletime", FALSE if the key doesn't exist. - * - * @link https://redis.io/commands/object - * @example - *
-     * $redis->lPush('l', 'Hello, world!');
-     * $redis->object("encoding", "l"); // → ziplist
-     * $redis->object("refcount", "l"); // → 1
-     * $redis->object("idletime", "l"); // → 400 (in seconds, with a precision of 10 seconds).
-     * 
- */ - public function object($string = '', $key = '') - { - } - - /** - * Performs a synchronous save. - * - * @return bool TRUE in case of success, FALSE in case of failure - * If a save is already running, this command will fail and return FALSE. - * - * @link https://redis.io/commands/save - * @example $redis->save(); - */ - public function save() - { - } - - /** - * Performs a background save. - * - * @return bool TRUE in case of success, FALSE in case of failure - * If a save is already running, this command will fail and return FALSE - * - * @link https://redis.io/commands/bgsave - * @example $redis->bgSave(); - */ - public function bgsave() - { - } - - /** - * Returns the timestamp of the last disk save. - * - * @return int timestamp - * - * @link https://redis.io/commands/lastsave - * @example $redis->lastSave(); - */ - public function lastSave() - { - } - - /** - * Blocks the current client until all the previous write commands are successfully transferred and - * acknowledged by at least the specified number of slaves. - * - * @param int $numSlaves Number of slaves that need to acknowledge previous write commands. - * @param int $timeout Timeout in milliseconds. - * - * @return int The command returns the number of slaves reached by all the writes performed in the - * context of the current connection - * - * @link https://redis.io/commands/wait - * @example $redis->wait(2, 1000); - */ - public function wait($numSlaves, $timeout) - { - } - - /** - * Returns the type of data pointed by a given key. - * - * @param string $key - * - * @return int - * Depending on the type of the data pointed by the key, - * this method will return the following value: - * - string: Redis::REDIS_STRING - * - set: Redis::REDIS_SET - * - list: Redis::REDIS_LIST - * - zset: Redis::REDIS_ZSET - * - hash: Redis::REDIS_HASH - * - other: Redis::REDIS_NOT_FOUND - * - * @link https://redis.io/commands/type - * @example $redis->type('key'); - */ - public function type($key) - { - } - - /** - * Append specified string to the string stored in specified key. - * - * @param string $key - * @param string|mixed $value - * - * @return int Size of the value after the append - * - * @link https://redis.io/commands/append - * @example - *
-     * $redis->set('key', 'value1');
-     * $redis->append('key', 'value2'); // 12
-     * $redis->get('key');              // 'value1value2'
-     * 
- */ - public function append($key, $value) - { - } - - /** - * Return a substring of a larger string - * - * @param string $key - * @param int $start - * @param int $end - * - * @return string the substring - * - * @link https://redis.io/commands/getrange - * @example - *
-     * $redis->set('key', 'string value');
-     * $redis->getRange('key', 0, 5);   // 'string'
-     * $redis->getRange('key', -5, -1); // 'value'
-     * 
- */ - public function getRange($key, $start, $end) - { - } - - /** - * Return a substring of a larger string - * - * @param string $key - * @param int $start - * @param int $end - */ - #[Deprecated] - public function substr($key, $start, $end) - { - } - - /** - * Changes a substring of a larger string. - * - * @param string $key - * @param int $offset - * @param string $value - * - * @return int the length of the string after it was modified - * - * @link https://redis.io/commands/setrange - * @example - *
-     * $redis->set('key', 'Hello world');
-     * $redis->setRange('key', 6, "redis"); // returns 11
-     * $redis->get('key');                  // "Hello redis"
-     * 
- */ - public function setRange($key, $offset, $value) - { - } - - /** - * Get the length of a string value. - * - * @param string $key - * @return int - * - * @link https://redis.io/commands/strlen - * @example - *
-     * $redis->set('key', 'value');
-     * $redis->strlen('key'); // 5
-     * 
- */ - public function strlen($key) - { - } - - /** - * Return the position of the first bit set to 1 or 0 in a string. The position is returned, thinking of the - * string as an array of bits from left to right, where the first byte's most significant bit is at position 0, - * the second byte's most significant bit is at position 8, and so forth. - * - * @param string $key - * @param int $bit - * @param int $start - * @param int $end - * - * @return int The command returns the position of the first bit set to 1 or 0 according to the request. - * If we look for set bits (the bit argument is 1) and the string is empty or composed of just - * zero bytes, -1 is returned. If we look for clear bits (the bit argument is 0) and the string - * only contains bit set to 1, the function returns the first bit not part of the string on the - * right. So if the string is three bytes set to the value 0xff the command BITPOS key 0 will - * return 24, since up to bit 23 all the bits are 1. Basically, the function considers the right - * of the string as padded with zeros if you look for clear bits and specify no range or the - * start argument only. However, this behavior changes if you are looking for clear bits and - * specify a range with both start and end. If no clear bit is found in the specified range, the - * function returns -1 as the user specified a clear range and there are no 0 bits in that range. - * - * @link https://redis.io/commands/bitpos - * @example - *
-     * $redis->set('key', '\xff\xff');
-     * $redis->bitpos('key', 1); // int(0)
-     * $redis->bitpos('key', 1, 1); // int(8)
-     * $redis->bitpos('key', 1, 3); // int(-1)
-     * $redis->bitpos('key', 0); // int(16)
-     * $redis->bitpos('key', 0, 1); // int(16)
-     * $redis->bitpos('key', 0, 1, 5); // int(-1)
-     * 
- */ - public function bitpos($key, $bit, $start = 0, $end = null) - { - } - - /** - * Return a single bit out of a larger string - * - * @param string $key - * @param int $offset - * - * @return int the bit value (0 or 1) - * - * @link https://redis.io/commands/getbit - * @example - *
-     * $redis->set('key', "\x7f");  // this is 0111 1111
-     * $redis->getBit('key', 0);    // 0
-     * $redis->getBit('key', 1);    // 1
-     * 
- */ - public function getBit($key, $offset) - { - } - - /** - * Changes a single bit of a string. - * - * @param string $key - * @param int $offset - * @param bool|int $value bool or int (1 or 0) - * - * @return int 0 or 1, the value of the bit before it was set - * - * @link https://redis.io/commands/setbit - * @example - *
-     * $redis->set('key', "*");     // ord("*") = 42 = 0x2f = "0010 1010"
-     * $redis->setBit('key', 5, 1); // returns 0
-     * $redis->setBit('key', 7, 1); // returns 0
-     * $redis->get('key');          // chr(0x2f) = "/" = b("0010 1111")
-     * 
- */ - public function setBit($key, $offset, $value) - { - } - - /** - * Count bits in a string - * - * @param string $key - * - * @return int The number of bits set to 1 in the value behind the input key - * - * @link https://redis.io/commands/bitcount - * @example - *
-     * $redis->set('bit', '345'); // // 11 0011  0011 0100  0011 0101
-     * var_dump( $redis->bitCount('bit', 0, 0) ); // int(4)
-     * var_dump( $redis->bitCount('bit', 1, 1) ); // int(3)
-     * var_dump( $redis->bitCount('bit', 2, 2) ); // int(4)
-     * var_dump( $redis->bitCount('bit', 0, 2) ); // int(11)
-     * 
- */ - public function bitCount($key) - { - } - - /** - * Bitwise operation on multiple keys. - * - * @param string $operation either "AND", "OR", "NOT", "XOR" - * @param string $retKey return key - * @param string $key1 first key - * @param string ...$otherKeys variadic list of keys - * - * @return int The size of the string stored in the destination key - * - * @link https://redis.io/commands/bitop - * @example - *
-     * $redis->set('bit1', '1'); // 11 0001
-     * $redis->set('bit2', '2'); // 11 0010
-     *
-     * $redis->bitOp('AND', 'bit', 'bit1', 'bit2'); // bit = 110000
-     * $redis->bitOp('OR',  'bit', 'bit1', 'bit2'); // bit = 110011
-     * $redis->bitOp('NOT', 'bit', 'bit1', 'bit2'); // bit = 110011
-     * $redis->bitOp('XOR', 'bit', 'bit1', 'bit2'); // bit = 11
-     * 
- */ - public function bitOp($operation, $retKey, $key1, ...$otherKeys) - { - } - - /** - * Removes all entries from the current database. - * - * @return bool Always TRUE - * @link https://redis.io/commands/flushdb - * @example $redis->flushDB(); - */ - public function flushDB() - { - } - - /** - * Removes all entries from all databases. - * - * @return bool Always TRUE - * - * @link https://redis.io/commands/flushall - * @example $redis->flushAll(); - */ - public function flushAll() - { - } - - /** - * Sort - * - * @param string $key - * @param array $option array(key => value, ...) - optional, with the following keys and values: - * - 'by' => 'some_pattern_*', - * - 'limit' => array(0, 1), - * - 'get' => 'some_other_pattern_*' or an array of patterns, - * - 'sort' => 'asc' or 'desc', - * - 'alpha' => TRUE, - * - 'store' => 'external-key' - * - * @return array - * An array of values, or a number corresponding to the number of elements stored if that was used - * - * @link https://redis.io/commands/sort - * @example - *
-     * $redis->del('s');
-     * $redis->sadd('s', 5);
-     * $redis->sadd('s', 4);
-     * $redis->sadd('s', 2);
-     * $redis->sadd('s', 1);
-     * $redis->sadd('s', 3);
-     *
-     * var_dump($redis->sort('s')); // 1,2,3,4,5
-     * var_dump($redis->sort('s', array('sort' => 'desc'))); // 5,4,3,2,1
-     * var_dump($redis->sort('s', array('sort' => 'desc', 'store' => 'out'))); // (int)5
-     * 
- */ - public function sort($key, $option = null) - { - } - - /** - * Returns an associative array of strings and integers - * - * @param string $option Optional. The option to provide redis. - * SERVER | CLIENTS | MEMORY | PERSISTENCE | STATS | REPLICATION | CPU | CLASTER | KEYSPACE | COMANDSTATS - * - * Returns an associative array of strings and integers, with the following keys: - * - redis_version - * - redis_git_sha1 - * - redis_git_dirty - * - arch_bits - * - multiplexing_api - * - process_id - * - uptime_in_seconds - * - uptime_in_days - * - lru_clock - * - used_cpu_sys - * - used_cpu_user - * - used_cpu_sys_children - * - used_cpu_user_children - * - connected_clients - * - connected_slaves - * - client_longest_output_list - * - client_biggest_input_buf - * - blocked_clients - * - used_memory - * - used_memory_human - * - used_memory_peak - * - used_memory_peak_human - * - mem_fragmentation_ratio - * - mem_allocator - * - loading - * - aof_enabled - * - changes_since_last_save - * - bgsave_in_progress - * - last_save_time - * - total_connections_received - * - total_commands_processed - * - expired_keys - * - evicted_keys - * - keyspace_hits - * - keyspace_misses - * - hash_max_zipmap_entries - * - hash_max_zipmap_value - * - pubsub_channels - * - pubsub_patterns - * - latest_fork_usec - * - vm_enabled - * - role - * - * @return array - * - * @link https://redis.io/commands/info - * @example - *
-     * $redis->info();
-     *
-     * or
-     *
-     * $redis->info("COMMANDSTATS"); //Information on the commands that have been run (>=2.6 only)
-     * $redis->info("CPU"); // just CPU information from Redis INFO
-     * 
- */ - public function info($option = null) - { - } - - /** - * Resets the statistics reported by Redis using the INFO command (`info()` function). - * These are the counters that are reset: - * - Keyspace hits - * - Keyspace misses - * - Number of commands processed - * - Number of connections received - * - Number of expired keys - * - * @return bool `TRUE` in case of success, `FALSE` in case of failure. - * - * @example $redis->resetStat(); - * @link https://redis.io/commands/config-resetstat - */ - public function resetStat() - { - } - - /** - * Returns the time to live left for a given key, in seconds. If the key doesn't exist, FALSE is returned. - * - * @param string $key - * - * @return int|bool the time left to live in seconds - * - * @link https://redis.io/commands/ttl - * @example - *
-     * $redis->setex('key', 123, 'test');
-     * $redis->ttl('key'); // int(123)
-     * 
- */ - public function ttl($key) - { - } - - /** - * Returns a time to live left for a given key, in milliseconds. - * - * If the key doesn't exist, FALSE is returned. - * - * @param string $key - * - * @return int|bool the time left to live in milliseconds - * - * @link https://redis.io/commands/pttl - * @example - *
-     * $redis->setex('key', 123, 'test');
-     * $redis->pttl('key'); // int(122999)
-     * 
- */ - public function pttl($key) - { - } - - /** - * Remove the expiration timer from a key. - * - * @param string $key - * - * @return bool TRUE if a timeout was removed, FALSE if the key didn’t exist or didn’t have an expiration timer. - * - * @link https://redis.io/commands/persist - * @example $redis->persist('key'); - */ - public function persist($key) - { - } - - /** - * Sets multiple key-value pairs in one atomic command. - * MSETNX only returns TRUE if all the keys were set (see SETNX). - * - * @param array $array Pairs: array(key => value, ...) - * - * @return bool TRUE in case of success, FALSE in case of failure - * - * @link https://redis.io/commands/mset - * @example - *
-     * $redis->mset(array('key0' => 'value0', 'key1' => 'value1'));
-     * var_dump($redis->get('key0'));
-     * var_dump($redis->get('key1'));
-     * // Output:
-     * // string(6) "value0"
-     * // string(6) "value1"
-     * 
- */ - public function mset(array $array) - { - } - - /** - * Get the values of all the specified keys. - * If one or more keys dont exist, the array will contain FALSE at the position of the key. - * - * @param array $keys Array containing the list of the keys - * - * @return array Array containing the values related to keys in argument - * - * @example - *
-     * $redis->set('key1', 'value1');
-     * $redis->set('key2', 'value2');
-     * $redis->set('key3', 'value3');
-     * $redis->getMultiple(array('key1', 'key2', 'key3')); // array('value1', 'value2', 'value3');
-     * $redis->getMultiple(array('key0', 'key1', 'key5')); // array(`FALSE`, 'value2', `FALSE`);
-     * 
- */ - #[Deprecated(replacement: '%class%->mGet(%parametersList%)')] - public function getMultiple(array $keys) - { - } - - /** - * Returns the values of all specified keys. - * - * For every key that does not hold a string value or does not exist, - * the special value false is returned. Because of this, the operation never fails. - * - * @param array $array - * - * @return array - * - * @link https://redis.io/commands/mget - * @example - *
-     * $redis->del('x', 'y', 'z', 'h');  // remove x y z
-     * $redis->mset(array('x' => 'a', 'y' => 'b', 'z' => 'c'));
-     * $redis->hset('h', 'field', 'value');
-     * var_dump($redis->mget(array('x', 'y', 'z', 'h')));
-     * // Output:
-     * // array(3) {
-     * //   [0]=> string(1) "a"
-     * //   [1]=> string(1) "b"
-     * //   [2]=> string(1) "c"
-     * //   [3]=> bool(false)
-     * // }
-     * 
- */ - public function mget(array $array) - { - } - - /** - * @see mset() - * @param array $array - * @return int 1 (if the keys were set) or 0 (no key was set) - * - * @link https://redis.io/commands/msetnx - */ - public function msetnx(array $array) - { - } - - /** - * Pops a value from the tail of a list, and pushes it to the front of another list. - * Also return this value. - * - * @since redis >= 1.1 - * - * @param string $srcKey - * @param string $dstKey - * - * @return string|mixed|false The element that was moved in case of success, FALSE in case of failure. - * - * @link https://redis.io/commands/rpoplpush - * @example - *
-     * $redis->del('x', 'y');
-     *
-     * $redis->lPush('x', 'abc');
-     * $redis->lPush('x', 'def');
-     * $redis->lPush('y', '123');
-     * $redis->lPush('y', '456');
-     *
-     * // move the last of x to the front of y.
-     * var_dump($redis->rpoplpush('x', 'y'));
-     * var_dump($redis->lRange('x', 0, -1));
-     * var_dump($redis->lRange('y', 0, -1));
-     *
-     * //Output:
-     * //
-     * //string(3) "abc"
-     * //array(1) {
-     * //  [0]=>
-     * //  string(3) "def"
-     * //}
-     * //array(3) {
-     * //  [0]=>
-     * //  string(3) "abc"
-     * //  [1]=>
-     * //  string(3) "456"
-     * //  [2]=>
-     * //  string(3) "123"
-     * //}
-     * 
- */ - public function rpoplpush($srcKey, $dstKey) - { - } - - /** - * A blocking version of rpoplpush, with an integral timeout in the third parameter. - * - * @param string $srcKey - * @param string $dstKey - * @param int $timeout - * - * @return string|mixed|bool The element that was moved in case of success, FALSE in case of timeout - * - * @link https://redis.io/commands/brpoplpush - */ - public function brpoplpush($srcKey, $dstKey, $timeout) - { - } - - /** - * Adds the specified member with a given score to the sorted set stored at key - * - * @param string $key Required key - * @param array|float $options Options if needed or score if omitted - * @param float|string|mixed $score1 Required score or value if options omitted - * @param string|float|mixed $value1 Required value or optional score if options omitted - * @param float|string|mixed $score2 Optional score or value if options omitted - * @param string|float|mixed $value2 Optional value or score if options omitted - * @param float|string|mixed $scoreN Optional score or value if options omitted - * @param string|float|mixed $valueN Optional value or score if options omitted - * - * @return int Number of values added - * - * @link https://redis.io/commands/zadd - * @example - *
-     * $redis->zAdd('z', 1, 'v1', 2, 'v2', 3, 'v3', 4, 'v4' );  // int(2)
-     * $redis->zRem('z', 'v2', 'v3');                           // int(2)
-     * $redis->zAdd('z', ['NX'], 5, 'v5');                      // int(1)
-     * $redis->zAdd('z', ['NX'], 6, 'v5');                      // int(0)
-     * $redis->zAdd('z', 7, 'v6');                              // int(1)
-     * $redis->zAdd('z', 8, 'v6');                              // int(0)
-     *
-     * var_dump( $redis->zRange('z', 0, -1) );
-     * // Output:
-     * // array(4) {
-     * //   [0]=> string(2) "v1"
-     * //   [1]=> string(2) "v4"
-     * //   [2]=> string(2) "v5"
-     * //   [3]=> string(2) "v8"
-     * // }
-     *
-     * var_dump( $redis->zRange('z', 0, -1, true) );
-     * // Output:
-     * // array(4) {
-     * //   ["v1"]=> float(1)
-     * //   ["v4"]=> float(4)
-     * //   ["v5"]=> float(5)
-     * //   ["v6"]=> float(8)
-     * 
- */ - public function zAdd($key, $options, $score1, $value1 = null, $score2 = null, $value2 = null, $scoreN = null, $valueN = null) - { - } - - /** - * Returns a range of elements from the ordered set stored at the specified key, - * with values in the range [start, end]. start and stop are interpreted as zero-based indices: - * 0 the first element, - * 1 the second ... - * -1 the last element, - * -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * @param bool $withscores - * - * @return array Array containing the values in specified range. - * - * @link https://redis.io/commands/zrange - * @example - *
-     * $redis->zAdd('key1', 0, 'val0');
-     * $redis->zAdd('key1', 2, 'val2');
-     * $redis->zAdd('key1', 10, 'val10');
-     * $redis->zRange('key1', 0, -1); // array('val0', 'val2', 'val10')
-     * // with scores
-     * $redis->zRange('key1', 0, -1, true); // array('val0' => 0, 'val2' => 2, 'val10' => 10)
-     * 
- */ - public function zRange($key, $start, $end, $withscores = null) - { - } - - /** - * Deletes a specified member from the ordered set. - * - * @param string $key - * @param string|mixed $member1 - * @param string|mixed ...$otherMembers - * - * @return int Number of deleted values - * - * @link https://redis.io/commands/zrem - * @example - *
-     * $redis->zAdd('z', 1, 'v1', 2, 'v2', 3, 'v3', 4, 'v4' );  // int(2)
-     * $redis->zRem('z', 'v2', 'v3');                           // int(2)
-     * var_dump( $redis->zRange('z', 0, -1) );
-     * //// Output:
-     * // array(2) {
-     * //   [0]=> string(2) "v1"
-     * //   [1]=> string(2) "v4"
-     * // }
-     * 
- */ - public function zRem($key, $member1, ...$otherMembers) - { - } - - /** - * @link https://redis.io/commands/zrem - * - * @param string $key - * @param string|mixed $member1 - * @param string|mixed ...$otherMembers - * - * @return int Number of deleted values - */ - #[Deprecated(replacement: '%class%->zRem(%parametersList%)')] - public function zDelete($key, $member1, ...$otherMembers) - { - } - - /** - * Returns the elements of the sorted set stored at the specified key in the range [start, end] - * in reverse order. start and stop are interpretated as zero-based indices: - * 0 the first element, - * 1 the second ... - * -1 the last element, - * -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * @param bool $withscore - * - * @return array Array containing the values in specified range. - * - * @link https://redis.io/commands/zrevrange - * @example - *
-     * $redis->zAdd('key', 0, 'val0');
-     * $redis->zAdd('key', 2, 'val2');
-     * $redis->zAdd('key', 10, 'val10');
-     * $redis->zRevRange('key', 0, -1); // array('val10', 'val2', 'val0')
-     *
-     * // with scores
-     * $redis->zRevRange('key', 0, -1, true); // array('val10' => 10, 'val2' => 2, 'val0' => 0)
-     * 
- */ - public function zRevRange($key, $start, $end, $withscore = null) - { - } - - /** - * Returns the elements of the sorted set stored at the specified key which have scores in the - * range [start,end]. Adding a parenthesis before start or end excludes it from the range. - * +inf and -inf are also valid limits. - * - * zRevRangeByScore returns the same items in reverse order, when the start and end parameters are swapped. - * - * @param string $key - * @param int $start - * @param int $end - * @param array $options Two options are available: - * - withscores => TRUE, - * - and limit => array($offset, $count) - * - * @return array Array containing the values in specified range. - * - * @link https://redis.io/commands/zrangebyscore - * @example - *
-     * $redis->zAdd('key', 0, 'val0');
-     * $redis->zAdd('key', 2, 'val2');
-     * $redis->zAdd('key', 10, 'val10');
-     * $redis->zRangeByScore('key', 0, 3);                                          // array('val0', 'val2')
-     * $redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE);              // array('val0' => 0, 'val2' => 2)
-     * $redis->zRangeByScore('key', 0, 3, array('limit' => array(1, 1));                        // array('val2')
-     * $redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE, 'limit' => array(1, 1));  // array('val2' => 2)
-     * 
- */ - public function zRangeByScore($key, $start, $end, array $options = array()) - { - } - - /** - * @see zRangeByScore() - * @param string $key - * @param int $start - * @param int $end - * @param array $options - * - * @return array - */ - public function zRevRangeByScore($key, $start, $end, array $options = array()) - { - } - - /** - * Returns a lexigraphical range of members in a sorted set, assuming the members have the same score. The - * min and max values are required to start with '(' (exclusive), '[' (inclusive), or be exactly the values - * '-' (negative inf) or '+' (positive inf). The command must be called with either three *or* five - * arguments or will return FALSE. - * - * @param string $key The ZSET you wish to run against. - * @param int $min The minimum alphanumeric value you wish to get. - * @param int $max The maximum alphanumeric value you wish to get. - * @param int $offset Optional argument if you wish to start somewhere other than the first element. - * @param int $limit Optional argument if you wish to limit the number of elements returned. - * - * @return array|false Array containing the values in the specified range. - * - * @link https://redis.io/commands/zrangebylex - * @example - *
-     * foreach (array('a', 'b', 'c', 'd', 'e', 'f', 'g') as $char) {
-     *     $redis->zAdd('key', $char);
-     * }
-     *
-     * $redis->zRangeByLex('key', '-', '[c'); // array('a', 'b', 'c')
-     * $redis->zRangeByLex('key', '-', '(c'); // array('a', 'b')
-     * $redis->zRangeByLex('key', '-', '[c'); // array('b', 'c')
-     * 
- */ - public function zRangeByLex($key, $min, $max, $offset = null, $limit = null) - { - } - - /** - * @see zRangeByLex() - * @param string $key - * @param int $min - * @param int $max - * @param int $offset - * @param int $limit - * - * @return array - * - * @link https://redis.io/commands/zrevrangebylex - */ - public function zRevRangeByLex($key, $min, $max, $offset = null, $limit = null) - { - } - - /** - * Returns the number of elements of the sorted set stored at the specified key which have - * scores in the range [start,end]. Adding a parenthesis before start or end excludes it - * from the range. +inf and -inf are also valid limits. - * - * @param string $key - * @param string $start - * @param string $end - * - * @return int the size of a corresponding zRangeByScore - * - * @link https://redis.io/commands/zcount - * @example - *
-     * $redis->zAdd('key', 0, 'val0');
-     * $redis->zAdd('key', 2, 'val2');
-     * $redis->zAdd('key', 10, 'val10');
-     * $redis->zCount('key', 0, 3); // 2, corresponding to array('val0', 'val2')
-     * 
- */ - public function zCount($key, $start, $end) - { - } - - /** - * Deletes the elements of the sorted set stored at the specified key which have scores in the range [start,end]. - * - * @param string $key - * @param float|string $start double or "+inf" or "-inf" string - * @param float|string $end double or "+inf" or "-inf" string - * - * @return int The number of values deleted from the sorted set - * - * @link https://redis.io/commands/zremrangebyscore - * @example - *
-     * $redis->zAdd('key', 0, 'val0');
-     * $redis->zAdd('key', 2, 'val2');
-     * $redis->zAdd('key', 10, 'val10');
-     * $redis->zRemRangeByScore('key', 0, 3); // 2
-     * 
- */ - public function zRemRangeByScore($key, $start, $end) - { - } - - /** - * @param string $key - * @param float $start - * @param float $end - */ - #[Deprecated(replacement: '%class%->zRemRangeByScore(%parametersList%)')] - public function zDeleteRangeByScore($key, $start, $end) - { - } - - /** - * Deletes the elements of the sorted set stored at the specified key which have rank in the range [start,end]. - * - * @param string $key - * @param int $start - * @param int $end - * - * @return int The number of values deleted from the sorted set - * - * @link https://redis.io/commands/zremrangebyrank - * @example - *
-     * $redis->zAdd('key', 1, 'one');
-     * $redis->zAdd('key', 2, 'two');
-     * $redis->zAdd('key', 3, 'three');
-     * $redis->zRemRangeByRank('key', 0, 1); // 2
-     * $redis->zRange('key', 0, -1, array('withscores' => TRUE)); // array('three' => 3)
-     * 
- */ - public function zRemRangeByRank($key, $start, $end) - { - } - - /** - * @link https://redis.io/commands/zremrangebyscore - * - * @param string $key - * @param int $start - * @param int $end - */ - #[Deprecated(replacement: '%class%->zRemRangeByRank(%parametersList%)')] - public function zDeleteRangeByRank($key, $start, $end) - { - } - - /** - * Returns the cardinality of an ordered set. - * - * @param string $key - * - * @return int the set's cardinality - * - * @link https://redis.io/commands/zsize - * @example - *
-     * $redis->zAdd('key', 0, 'val0');
-     * $redis->zAdd('key', 2, 'val2');
-     * $redis->zAdd('key', 10, 'val10');
-     * $redis->zCard('key');            // 3
-     * 
- */ - public function zCard($key) - { - } - - /** - * @param string $key - * @return int - */ - #[Deprecated(replacement: '%class%->zCard(%parametersList%)')] - public function zSize($key) - { - } - - /** - * Returns the score of a given member in the specified sorted set. - * - * @param string $key - * @param string|mixed $member - * - * @return float|bool false if member or key not exists - * - * @link https://redis.io/commands/zscore - * @example - *
-     * $redis->zAdd('key', 2.5, 'val2');
-     * $redis->zScore('key', 'val2'); // 2.5
-     * 
- */ - public function zScore($key, $member) - { - } - - /** - * Returns the rank of a given member in the specified sorted set, starting at 0 for the item - * with the smallest score. zRevRank starts at 0 for the item with the largest score. - * - * @param string $key - * @param string|mixed $member - * - * @return int|false the item's score, or false if key or member is not exists - * - * @link https://redis.io/commands/zrank - * @example - *
-     * $redis->del('z');
-     * $redis->zAdd('key', 1, 'one');
-     * $redis->zAdd('key', 2, 'two');
-     * $redis->zRank('key', 'one');     // 0
-     * $redis->zRank('key', 'two');     // 1
-     * $redis->zRevRank('key', 'one');  // 1
-     * $redis->zRevRank('key', 'two');  // 0
-     * 
- */ - public function zRank($key, $member) - { - } - - /** - * @see zRank() - * @param string $key - * @param string|mixed $member - * - * @return int|false the item's score, false - if key or member is not exists - * - * @link https://redis.io/commands/zrevrank - */ - public function zRevRank($key, $member) - { - } - - /** - * Increments the score of a member from a sorted set by a given amount. - * - * @param string $key - * @param float $value (double) value that will be added to the member's score - * @param string $member - * - * @return float the new value - * - * @link https://redis.io/commands/zincrby - * @example - *
-     * $redis->del('key');
-     * $redis->zIncrBy('key', 2.5, 'member1');  // key or member1 didn't exist, so member1's score is to 0
-     *                                          // before the increment and now has the value 2.5
-     * $redis->zIncrBy('key', 1, 'member1');    // 3.5
-     * 
- */ - public function zIncrBy($key, $value, $member) - { - } - - /** - * Creates an union of sorted sets given in second argument. - * The result of the union will be stored in the sorted set defined by the first argument. - * The third optionnel argument defines weights to apply to the sorted sets in input. - * In this case, the weights will be multiplied by the score of each element in the sorted set - * before applying the aggregation. The forth argument defines the AGGREGATE option which - * specify how the results of the union are aggregated. - * - * @param string $output - * @param array $zSetKeys - * @param null|array $weights - * @param string $aggregateFunction Either "SUM", "MIN", or "MAX": defines the behaviour to use on - * duplicate entries during the zUnionStore - * - * @return int The number of values in the new sorted set - * - * @link https://redis.io/commands/zunionstore - * @example - *
-     * $redis->del('k1');
-     * $redis->del('k2');
-     * $redis->del('k3');
-     * $redis->del('ko1');
-     * $redis->del('ko2');
-     * $redis->del('ko3');
-     *
-     * $redis->zAdd('k1', 0, 'val0');
-     * $redis->zAdd('k1', 1, 'val1');
-     *
-     * $redis->zAdd('k2', 2, 'val2');
-     * $redis->zAdd('k2', 3, 'val3');
-     *
-     * $redis->zUnionStore('ko1', array('k1', 'k2')); // 4, 'ko1' => array('val0', 'val1', 'val2', 'val3')
-     *
-     * // Weighted zUnionStore
-     * $redis->zUnionStore('ko2', array('k1', 'k2'), array(1, 1)); // 4, 'ko2' => array('val0', 'val1', 'val2', 'val3')
-     * $redis->zUnionStore('ko3', array('k1', 'k2'), array(5, 1)); // 4, 'ko3' => array('val0', 'val2', 'val3', 'val1')
-     * 
- */ - public function zUnionStore($output, $zSetKeys, ?array $weights = null, $aggregateFunction = 'SUM') - { - } - - /** - * @param string $Output - * @param array $ZSetKeys - * @param array|null $Weights - * @param string $aggregateFunction - */ - #[Deprecated(replacement: '%class%->zUnionStore(%parametersList%)')] - public function zUnion($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM') - { - } - - /** - * Creates an intersection of sorted sets given in second argument. - * The result of the union will be stored in the sorted set defined by the first argument. - * The third optional argument defines weights to apply to the sorted sets in input. - * In this case, the weights will be multiplied by the score of each element in the sorted set - * before applying the aggregation. The forth argument defines the AGGREGATE option which - * specify how the results of the union are aggregated. - * - * @param string $output - * @param array $zSetKeys - * @param null|array $weights - * @param string $aggregateFunction Either "SUM", "MIN", or "MAX": - * defines the behaviour to use on duplicate entries during the zInterStore. - * - * @return int The number of values in the new sorted set. - * - * @link https://redis.io/commands/zinterstore - * @example - *
-     * $redis->del('k1');
-     * $redis->del('k2');
-     * $redis->del('k3');
-     *
-     * $redis->del('ko1');
-     * $redis->del('ko2');
-     * $redis->del('ko3');
-     * $redis->del('ko4');
-     *
-     * $redis->zAdd('k1', 0, 'val0');
-     * $redis->zAdd('k1', 1, 'val1');
-     * $redis->zAdd('k1', 3, 'val3');
-     *
-     * $redis->zAdd('k2', 2, 'val1');
-     * $redis->zAdd('k2', 3, 'val3');
-     *
-     * $redis->zInterStore('ko1', array('k1', 'k2'));               // 2, 'ko1' => array('val1', 'val3')
-     * $redis->zInterStore('ko2', array('k1', 'k2'), array(1, 1));  // 2, 'ko2' => array('val1', 'val3')
-     *
-     * // Weighted zInterStore
-     * $redis->zInterStore('ko3', array('k1', 'k2'), array(1, 5), 'min'); // 2, 'ko3' => array('val1', 'val3')
-     * $redis->zInterStore('ko4', array('k1', 'k2'), array(1, 5), 'max'); // 2, 'ko4' => array('val3', 'val1')
-     * 
- */ - public function zInterStore($output, $zSetKeys, array $weights = null, $aggregateFunction = 'SUM') - { - } - - /** - * @param $Output - * @param $ZSetKeys - * @param array|null $Weights - * @param string $aggregateFunction - */ - #[Deprecated(replacement: '%class%->zInterStore(%parametersList%)')] - public function zInter($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM') - { - } - - /** - * Scan a sorted set for members, with optional pattern and count - * - * @param string $key String, the set to scan. - * @param int &$iterator Long (reference), initialized to NULL. - * @param string $pattern String (optional), the pattern to match. - * @param int $count How many keys to return per iteration (Redis might return a different number). - * - * @return array|false PHPRedis will return matching keys from Redis, or FALSE when iteration is complete - * - * @link https://redis.io/commands/zscan - * @example - *
-     * $iterator = null;
-     * while ($members = $redis-zscan('zset', $iterator)) {
-     *     foreach ($members as $member => $score) {
-     *         echo $member . ' => ' . $score . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function zScan($key, &$iterator, $pattern = null, $count = 0) - { - } - - /** - * Block until Redis can pop the highest or lowest scoring members from one or more ZSETs. - * There are two commands (BZPOPMIN and BZPOPMAX for popping the lowest and highest scoring elements respectively.) - * - * @param string|array $key1 - * @param string|array $key2 ... - * @param int $timeout - * - * @return array Either an array with the key member and score of the highest or lowest element or an empty array - * if the timeout was reached without an element to pop. - * - * @since >= 5.0 - * @link https://redis.io/commands/bzpopmax - * @example - *
-     * // Wait up to 5 seconds to pop the *lowest* scoring member from sets `zs1` and `zs2`.
-     * $redis->bzPopMin(['zs1', 'zs2'], 5);
-     * $redis->bzPopMin('zs1', 'zs2', 5);
-     *
-     * // Wait up to 5 seconds to pop the *highest* scoring member from sets `zs1` and `zs2`
-     * $redis->bzPopMax(['zs1', 'zs2'], 5);
-     * $redis->bzPopMax('zs1', 'zs2', 5);
-     * 
- */ - public function bzPopMax($key1, $key2, $timeout) - { - } - - /** - * @param string|array $key1 - * @param string|array $key2 ... - * @param int $timeout - * - * @return array Either an array with the key member and score of the highest or lowest element or an empty array - * if the timeout was reached without an element to pop. - * - * @see bzPopMax - * @since >= 5.0 - * @link https://redis.io/commands/bzpopmin - */ - public function bzPopMin($key1, $key2, $timeout) - { - } - - /** - * Can pop the highest scoring members from one ZSET. - * - * @param string $key - * @param int $count - * - * @return array Either an array with the key member and score of the highest element or an empty array - * if there is no element to pop. - * - * @since >= 5.0 - * @link https://redis.io/commands/zpopmax - * @example - *
-     * // Pop the *lowest* scoring member from set `zs1`.
-     * $redis->zPopMax('zs1');
-     * // Pop the *lowest* 3 scoring member from set `zs1`.
-     * $redis->zPopMax('zs1', 3);
-     * 
- */ - public function zPopMax($key, $count = 1) - { - } - - /** - * Can pop the lowest scoring members from one ZSET. - * - * @param string $key - * @param int $count - * - * @return array Either an array with the key member and score of the lowest element or an empty array - * if there is no element to pop. - * - * @since >= 5.0 - * @link https://redis.io/commands/zpopmin - * @example - *
-     * // Pop the *lowest* scoring member from set `zs1`.
-     * $redis->zPopMin('zs1');
-     * // Pop the *lowest* 3 scoring member from set `zs1`.
-     * $redis->zPopMin('zs1', 3);
-     * 
- */ - public function zPopMin($key, $count = 1) - { - } - - /** - * Adds a value to the hash stored at key. If this value is already in the hash, FALSE is returned. - * - * @param string $key - * @param string $hashKey - * @param string $value - * - * @return int|bool - * - 1 if value didn't exist and was added successfully, - * - 0 if the value was already present and was replaced, FALSE if there was an error. - * - * @link https://redis.io/commands/hset - * @example - *
-     * $redis->del('h')
-     * $redis->hSet('h', 'key1', 'hello');  // 1, 'key1' => 'hello' in the hash at "h"
-     * $redis->hGet('h', 'key1');           // returns "hello"
-     *
-     * $redis->hSet('h', 'key1', 'plop');   // 0, value was replaced.
-     * $redis->hGet('h', 'key1');           // returns "plop"
-     * 
- */ - public function hSet($key, $hashKey, $value) - { - } - - /** - * Adds a value to the hash stored at key only if this field isn't already in the hash. - * - * @param string $key - * @param string $hashKey - * @param string $value - * - * @return bool TRUE if the field was set, FALSE if it was already present. - * - * @link https://redis.io/commands/hsetnx - * @example - *
-     * $redis->del('h')
-     * $redis->hSetNx('h', 'key1', 'hello'); // TRUE, 'key1' => 'hello' in the hash at "h"
-     * $redis->hSetNx('h', 'key1', 'world'); // FALSE, 'key1' => 'hello' in the hash at "h". No change since the field
-     * wasn't replaced.
-     * 
- */ - public function hSetNx($key, $hashKey, $value) - { - } - - /** - * Gets a value from the hash stored at key. - * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned. - * - * @param string $key - * @param string $hashKey - * - * @return string|false The value, if the command executed successfully BOOL FALSE in case of failure - * - * @link https://redis.io/commands/hget - */ - public function hGet($key, $hashKey) - { - } - - /** - * Returns the length of a hash, in number of items - * - * @param string $key - * - * @return int|false the number of items in a hash, FALSE if the key doesn't exist or isn't a hash - * - * @link https://redis.io/commands/hlen - * @example - *
-     * $redis->del('h')
-     * $redis->hSet('h', 'key1', 'hello');
-     * $redis->hSet('h', 'key2', 'plop');
-     * $redis->hLen('h'); // returns 2
-     * 
- */ - public function hLen($key) - { - } - - /** - * Removes a values from the hash stored at key. - * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned. - * - * @param string $key - * @param string $hashKey1 - * @param string ...$otherHashKeys - * - * @return int|bool Number of deleted fields - * - * @link https://redis.io/commands/hdel - * @example - *
-     * $redis->hMSet('h',
-     *               array(
-     *                    'f1' => 'v1',
-     *                    'f2' => 'v2',
-     *                    'f3' => 'v3',
-     *                    'f4' => 'v4',
-     *               ));
-     *
-     * var_dump( $redis->hDel('h', 'f1') );        // int(1)
-     * var_dump( $redis->hDel('h', 'f2', 'f3') );  // int(2)
-     * s
-     * var_dump( $redis->hGetAll('h') );
-     * //// Output:
-     * //  array(1) {
-     * //    ["f4"]=> string(2) "v4"
-     * //  }
-     * 
- */ - public function hDel($key, $hashKey1, ...$otherHashKeys) - { - } - - /** - * Returns the keys in a hash, as an array of strings. - * - * @param string $key - * - * @return array An array of elements, the keys of the hash. This works like PHP's array_keys(). - * - * @link https://redis.io/commands/hkeys - * @example - *
-     * $redis->del('h');
-     * $redis->hSet('h', 'a', 'x');
-     * $redis->hSet('h', 'b', 'y');
-     * $redis->hSet('h', 'c', 'z');
-     * $redis->hSet('h', 'd', 't');
-     * var_dump($redis->hKeys('h'));
-     *
-     * // Output:
-     * // array(4) {
-     * // [0]=>
-     * // string(1) "a"
-     * // [1]=>
-     * // string(1) "b"
-     * // [2]=>
-     * // string(1) "c"
-     * // [3]=>
-     * // string(1) "d"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hKeys($key) - { - } - - /** - * Returns the values in a hash, as an array of strings. - * - * @param string $key - * - * @return array An array of elements, the values of the hash. This works like PHP's array_values(). - * - * @link https://redis.io/commands/hvals - * @example - *
-     * $redis->del('h');
-     * $redis->hSet('h', 'a', 'x');
-     * $redis->hSet('h', 'b', 'y');
-     * $redis->hSet('h', 'c', 'z');
-     * $redis->hSet('h', 'd', 't');
-     * var_dump($redis->hVals('h'));
-     *
-     * // Output
-     * // array(4) {
-     * //   [0]=>
-     * //   string(1) "x"
-     * //   [1]=>
-     * //   string(1) "y"
-     * //   [2]=>
-     * //   string(1) "z"
-     * //   [3]=>
-     * //   string(1) "t"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hVals($key) - { - } - - /** - * Returns the whole hash, as an array of strings indexed by strings. - * - * @param string $key - * - * @return array An array of elements, the contents of the hash. - * - * @link https://redis.io/commands/hgetall - * @example - *
-     * $redis->del('h');
-     * $redis->hSet('h', 'a', 'x');
-     * $redis->hSet('h', 'b', 'y');
-     * $redis->hSet('h', 'c', 'z');
-     * $redis->hSet('h', 'd', 't');
-     * var_dump($redis->hGetAll('h'));
-     *
-     * // Output:
-     * // array(4) {
-     * //   ["a"]=>
-     * //   string(1) "x"
-     * //   ["b"]=>
-     * //   string(1) "y"
-     * //   ["c"]=>
-     * //   string(1) "z"
-     * //   ["d"]=>
-     * //   string(1) "t"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hGetAll($key) - { - } - - /** - * Verify if the specified member exists in a key. - * - * @param string $key - * @param string $hashKey - * - * @return bool If the member exists in the hash table, return TRUE, otherwise return FALSE. - * - * @link https://redis.io/commands/hexists - * @example - *
-     * $redis->hSet('h', 'a', 'x');
-     * $redis->hExists('h', 'a');               //  TRUE
-     * $redis->hExists('h', 'NonExistingKey');  // FALSE
-     * 
- */ - public function hExists($key, $hashKey) - { - } - - /** - * Increments the value of a member from a hash by a given amount. - * - * @param string $key - * @param string $hashKey - * @param int $value (integer) value that will be added to the member's value - * - * @return int the new value - * - * @link https://redis.io/commands/hincrby - * @example - *
-     * $redis->del('h');
-     * $redis->hIncrBy('h', 'x', 2); // returns 2: h[x] = 2 now.
-     * $redis->hIncrBy('h', 'x', 1); // h[x] ← 2 + 1. Returns 3
-     * 
- */ - public function hIncrBy($key, $hashKey, $value) - { - } - - /** - * Increment the float value of a hash field by the given amount - * - * @param string $key - * @param string $field - * @param float $increment - * - * @return float - * - * @link https://redis.io/commands/hincrbyfloat - * @example - *
-     * $redis = new Redis();
-     * $redis->connect('127.0.0.1');
-     * $redis->hset('h', 'float', 3);
-     * $redis->hset('h', 'int',   3);
-     * var_dump( $redis->hIncrByFloat('h', 'float', 1.5) ); // float(4.5)
-     *
-     * var_dump( $redis->hGetAll('h') );
-     *
-     * // Output
-     *  array(2) {
-     *    ["float"]=>
-     *    string(3) "4.5"
-     *    ["int"]=>
-     *    string(1) "3"
-     *  }
-     * 
- */ - public function hIncrByFloat($key, $field, $increment) - { - } - - /** - * Fills in a whole hash. Non-string values are converted to string, using the standard (string) cast. - * NULL values are stored as empty strings - * - * @param string $key - * @param array $hashKeys key → value array - * - * @return bool - * - * @link https://redis.io/commands/hmset - * @example - *
-     * $redis->del('user:1');
-     * $redis->hMSet('user:1', array('name' => 'Joe', 'salary' => 2000));
-     * $redis->hIncrBy('user:1', 'salary', 100); // Joe earns 100 more now.
-     * 
- */ - public function hMSet($key, $hashKeys) - { - } - - /** - * Retrieve the values associated to the specified fields in the hash. - * - * @param string $key - * @param array $hashKeys - * - * @return array Array An array of elements, the values of the specified fields in the hash, - * with the hash keys as array keys. - * - * @link https://redis.io/commands/hmget - * @example - *
-     * $redis->del('h');
-     * $redis->hSet('h', 'field1', 'value1');
-     * $redis->hSet('h', 'field2', 'value2');
-     * $redis->hmGet('h', array('field1', 'field2')); // returns array('field1' => 'value1', 'field2' => 'value2')
-     * 
- */ - public function hMGet($key, $hashKeys) - { - } - - /** - * Scan a HASH value for members, with an optional pattern and count. - * - * @param string $key - * @param int &$iterator - * @param string $pattern Optional pattern to match against. - * @param int $count How many keys to return in a go (only a sugestion to Redis). - * - * @return array An array of members that match our pattern. - * - * @link https://redis.io/commands/hscan - * @example - *
-     * // $iterator = null;
-     * // while($elements = $redis->hscan('hash', $iterator)) {
-     * //     foreach($elements as $key => $value) {
-     * //         echo $key . ' => ' . $value . PHP_EOL;
-     * //     }
-     * // }
-     * 
- */ - public function hScan($key, &$iterator, $pattern = null, $count = 0) - { - } - - /** - * Get the string length of the value associated with field in the hash stored at key - * - * @param string $key - * @param string $field - * - * @return int the string length of the value associated with field, or zero when field is not present in the hash - * or key does not exist at all. - * - * @link https://redis.io/commands/hstrlen - * @since >= 3.2 - */ - public function hStrLen(string $key, string $field) - { - } - - /** - * Add one or more geospatial items to the specified key. - * This function must be called with at least one longitude, latitude, member triplet. - * - * @param string $key - * @param float $longitude - * @param float $latitude - * @param string $member - * - * @return int The number of elements added to the geospatial key - * - * @link https://redis.io/commands/geoadd - * @since >= 3.2 - * - * @example - *
-     * $redis->del("myplaces");
-     *
-     * // Since the key will be new, $result will be 2
-     * $result = $redis->geoAdd(
-     *   "myplaces",
-     *   -122.431, 37.773, "San Francisco",
-     *   -157.858, 21.315, "Honolulu"
-     * ); // 2
-     * 
- */ - public function geoadd($key, $longitude, $latitude, $member) - { - } - - /** - * Retrieve Geohash strings for one or more elements of a geospatial index. - - * @param string $key - * @param string ...$member variadic list of members - * - * @return array One or more Redis Geohash encoded strings - * - * @link https://redis.io/commands/geohash - * @since >= 3.2 - * - * @example - *
-     * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
-     * $hashes = $redis->geoHash("hawaii", "Honolulu", "Maui");
-     * var_dump($hashes);
-     * // Output: array(2) {
-     * //   [0]=>
-     * //   string(11) "87z9pyek3y0"
-     * //   [1]=>
-     * //   string(11) "8e8y6d5jps0"
-     * // }
-     * 
- */ - public function geohash($key, ...$member) - { - } - - /** - * Return longitude, latitude positions for each requested member. - * - * @param string $key - * @param string $member - * @return array One or more longitude/latitude positions - * - * @link https://redis.io/commands/geopos - * @since >= 3.2 - * - * @example - *
-     * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
-     * $positions = $redis->geoPos("hawaii", "Honolulu", "Maui");
-     * var_dump($positions);
-     *
-     * // Output:
-     * array(2) {
-     *  [0]=> array(2) {
-     *      [0]=> string(22) "-157.85800248384475708"
-     *      [1]=> string(19) "21.3060004581273077"
-     *  }
-     *  [1]=> array(2) {
-     *      [0]=> string(22) "-156.33099943399429321"
-     *      [1]=> string(20) "20.79799924753607598"
-     *  }
-     * }
-     * 
- */ - public function geopos(string $key, string $member) - { - } - - /** - * Return the distance between two members in a geospatial set. - * - * If units are passed it must be one of the following values: - * - 'm' => Meters - * - 'km' => Kilometers - * - 'mi' => Miles - * - 'ft' => Feet - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string|null $unit - * - * @return float The distance between the two passed members in the units requested (meters by default) - * - * @link https://redis.io/commands/geodist - * @since >= 3.2 - * - * @example - *
-     * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
-     *
-     * $meters = $redis->geoDist("hawaii", "Honolulu", "Maui");
-     * $kilometers = $redis->geoDist("hawaii", "Honolulu", "Maui", 'km');
-     * $miles = $redis->geoDist("hawaii", "Honolulu", "Maui", 'mi');
-     * $feet = $redis->geoDist("hawaii", "Honolulu", "Maui", 'ft');
-     *
-     * echo "Distance between Honolulu and Maui:\n";
-     * echo "  meters    : $meters\n";
-     * echo "  kilometers: $kilometers\n";
-     * echo "  miles     : $miles\n";
-     * echo "  feet      : $feet\n";
-     *
-     * // Bad unit
-     * $inches = $redis->geoDist("hawaii", "Honolulu", "Maui", 'in');
-     * echo "Invalid unit returned:\n";
-     * var_dump($inches);
-     *
-     * // Output
-     * Distance between Honolulu and Maui:
-     * meters    : 168275.204
-     * kilometers: 168.2752
-     * miles     : 104.5616
-     * feet      : 552084.0028
-     * Invalid unit returned:
-     * bool(false)
-     * 
- */ - public function geodist($key, $member1, $member2, $unit = null) - { - } - - /** - * Return members of a set with geospatial information that are within the radius specified by the caller. - * - * @param $key - * @param $longitude - * @param $latitude - * @param $radius - * @param $unit - * @param array|null $options - *
-     * |Key         |Value          |Description                                        |
-     * |------------|---------------|---------------------------------------------------|
-     * |COUNT       |integer > 0    |Limit how many results are returned                |
-     * |            |WITHCOORD      |Return longitude and latitude of matching members  |
-     * |            |WITHDIST       |Return the distance from the center                |
-     * |            |WITHHASH       |Return the raw geohash-encoded score               |
-     * |            |ASC            |Sort results in ascending order                    |
-     * |            |DESC           |Sort results in descending order                   |
-     * |STORE       |key            |Store results in key                               |
-     * |STOREDIST   |key            |Store the results as distances in key              |
-     * 
- * Note: It doesn't make sense to pass both ASC and DESC options but if both are passed - * the last one passed will be used. - * Note: When using STORE[DIST] in Redis Cluster, the store key must has to the same slot as - * the query key or you will get a CROSSLOT error. - * @return mixed When no STORE option is passed, this function returns an array of results. - * If it is passed this function returns the number of stored entries. - * - * @link https://redis.io/commands/georadius - * @since >= 3.2 - * @example - *
-     * // Add some cities
-     * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
-     *
-     * echo "Within 300 miles of Honolulu:\n";
-     * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi'));
-     *
-     * echo "\nWithin 300 miles of Honolulu with distances:\n";
-     * $options = ['WITHDIST'];
-     * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
-     *
-     * echo "\nFirst result within 300 miles of Honolulu with distances:\n";
-     * $options['count'] = 1;
-     * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
-     *
-     * echo "\nFirst result within 300 miles of Honolulu with distances in descending sort order:\n";
-     * $options[] = 'DESC';
-     * var_dump($redis->geoRadius("hawaii", -157.858, 21.306, 300, 'mi', $options));
-     *
-     * // Output
-     * Within 300 miles of Honolulu:
-     * array(2) {
-     *  [0]=> string(8) "Honolulu"
-     *  [1]=> string(4) "Maui"
-     * }
-     *
-     * Within 300 miles of Honolulu with distances:
-     * array(2) {
-     *     [0]=>
-     *   array(2) {
-     *         [0]=>
-     *     string(8) "Honolulu"
-     *         [1]=>
-     *     string(6) "0.0002"
-     *   }
-     *   [1]=>
-     *   array(2) {
-     *         [0]=>
-     *     string(4) "Maui"
-     *         [1]=>
-     *     string(8) "104.5615"
-     *   }
-     * }
-     *
-     * First result within 300 miles of Honolulu with distances:
-     * array(1) {
-     *     [0]=>
-     *   array(2) {
-     *         [0]=>
-     *     string(8) "Honolulu"
-     *         [1]=>
-     *     string(6) "0.0002"
-     *   }
-     * }
-     *
-     * First result within 300 miles of Honolulu with distances in descending sort order:
-     * array(1) {
-     *     [0]=>
-     *   array(2) {
-     *         [0]=>
-     *     string(4) "Maui"
-     *         [1]=>
-     *     string(8) "104.5615"
-     *   }
-     * }
-     * 
- */ - public function georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) - { - } - - /** - * This method is identical to geoRadius except that instead of passing a longitude and latitude as the "source" - * you pass an existing member in the geospatial set - * - * @param string $key - * @param string $member - * @param $radius - * @param $units - * @param array|null $options see georadius - * - * @return array The zero or more entries that are close enough to the member given the distance and radius specified - * - * @link https://redis.io/commands/georadiusbymember - * @since >= 3.2 - * @see georadius - * @example - *
-     * $redis->geoAdd("hawaii", -157.858, 21.306, "Honolulu", -156.331, 20.798, "Maui");
-     *
-     * echo "Within 300 miles of Honolulu:\n";
-     * var_dump($redis->geoRadiusByMember("hawaii", "Honolulu", 300, 'mi'));
-     *
-     * echo "\nFirst match within 300 miles of Honolulu:\n";
-     * var_dump($redis->geoRadiusByMember("hawaii", "Honolulu", 300, 'mi', ['count' => 1]));
-     *
-     * // Output
-     * Within 300 miles of Honolulu:
-     * array(2) {
-     *  [0]=> string(8) "Honolulu"
-     *  [1]=> string(4) "Maui"
-     * }
-     *
-     * First match within 300 miles of Honolulu:
-     * array(1) {
-     *  [0]=> string(8) "Honolulu"
-     * }
-     * 
- */ - public function georadiusbymember($key, $member, $radius, $units, array $options = null) - { - } - - /** - * Get or Set the redis config keys. - * - * @param string $operation either `GET` or `SET` - * @param string $key for `SET`, glob-pattern for `GET` - * @param string|mixed $value optional string (only for `SET`) - * - * @return array Associative array for `GET`, key -> value - * - * @link https://redis.io/commands/config-get - * @example - *
-     * $redis->config("GET", "*max-*-entries*");
-     * $redis->config("SET", "dir", "/var/run/redis/dumps/");
-     * 
- */ - public function config($operation, $key, $value) - { - } - - /** - * Evaluate a LUA script serverside - * - * @param string $script - * @param array $args - * @param int $numKeys - * - * @return mixed What is returned depends on what the LUA script itself returns, which could be a scalar value - * (int/string), or an array. Arrays that are returned can also contain other arrays, if that's how it was set up in - * your LUA script. If there is an error executing the LUA script, the getLastError() function can tell you the - * message that came back from Redis (e.g. compile error). - * - * @link https://redis.io/commands/eval - * @example - *
-     * $redis->eval("return 1"); // Returns an integer: 1
-     * $redis->eval("return {1,2,3}"); // Returns Array(1,2,3)
-     * $redis->del('mylist');
-     * $redis->rpush('mylist','a');
-     * $redis->rpush('mylist','b');
-     * $redis->rpush('mylist','c');
-     * // Nested response:  Array(1,2,3,Array('a','b','c'));
-     * $redis->eval("return {1,2,3,redis.call('lrange','mylist',0,-1)}}");
-     * 
- */ - public function eval($script, $args = array(), $numKeys = 0) - { - } - - /** - * @param string $script - * @param array $args - * @param int $numKeys - * @return mixed @see eval() - */ - #[Deprecated(replacement: '%class%->eval(%parametersList%)')] - public function evaluate($script, $args = array(), $numKeys = 0) - { - } - - /** - * Evaluate a LUA script serverside, from the SHA1 hash of the script instead of the script itself. - * In order to run this command Redis will have to have already loaded the script, either by running it or via - * the SCRIPT LOAD command. - * - * @param string $scriptSha - * @param array $args - * @param int $numKeys - * - * @return mixed @see eval() - * - * @see eval() - * @link https://redis.io/commands/evalsha - * @example - *
-     * $script = 'return 1';
-     * $sha = $redis->script('load', $script);
-     * $redis->evalSha($sha); // Returns 1
-     * 
- */ - public function evalSha($scriptSha, $args = array(), $numKeys = 0) - { - } - - /** - * @param string $scriptSha - * @param array $args - * @param int $numKeys - */ - #[Deprecated(replacement: '%class%->evalSha(%parametersList%)')] - public function evaluateSha($scriptSha, $args = array(), $numKeys = 0) - { - } - - /** - * Execute the Redis SCRIPT command to perform various operations on the scripting subsystem. - * @param string $command load | flush | kill | exists - * @param string $script - * - * @return mixed - * - * @link https://redis.io/commands/script-load - * @link https://redis.io/commands/script-kill - * @link https://redis.io/commands/script-flush - * @link https://redis.io/commands/script-exists - * @example - *
-     * $redis->script('load', $script);
-     * $redis->script('flush');
-     * $redis->script('kill');
-     * $redis->script('exists', $script1, [$script2, $script3, ...]);
-     * 
- * - * SCRIPT LOAD will return the SHA1 hash of the passed script on success, and FALSE on failure. - * SCRIPT FLUSH should always return TRUE - * SCRIPT KILL will return true if a script was able to be killed and false if not - * SCRIPT EXISTS will return an array with TRUE or FALSE for each passed script - */ - public function script($command, $script) - { - } - - /** - * The last error message (if any) - * - * @return string|null A string with the last returned script based error message, or NULL if there is no error - * - * @example - *
-     * $redis->eval('this-is-not-lua');
-     * $err = $redis->getLastError();
-     * // "ERR Error compiling script (new function): user_script:1: '=' expected near '-'"
-     * 
- */ - public function getLastError() - { - } - - /** - * Clear the last error message - * - * @return bool true - * - * @example - *
-     * $redis->set('x', 'a');
-     * $redis->incr('x');
-     * $err = $redis->getLastError();
-     * // "ERR value is not an integer or out of range"
-     * $redis->clearLastError();
-     * $err = $redis->getLastError();
-     * // NULL
-     * 
- */ - public function clearLastError() - { - } - - /** - * Issue the CLIENT command with various arguments. - * The Redis CLIENT command can be used in four ways: - * - CLIENT LIST - * - CLIENT GETNAME - * - CLIENT SETNAME [name] - * - CLIENT KILL [ip:port] - * - * @param string $command - * @param string $value - * @return mixed This will vary depending on which client command was executed: - * - CLIENT LIST will return an array of arrays with client information. - * - CLIENT GETNAME will return the client name or false if none has been set - * - CLIENT SETNAME will return true if it can be set and false if not - * - CLIENT KILL will return true if the client can be killed, and false if not - * - * Note: phpredis will attempt to reconnect so you can actually kill your own connection but may not notice losing it! - * - * @link https://redis.io/commands/client-list - * @link https://redis.io/commands/client-getname - * @link https://redis.io/commands/client-setname - * @link https://redis.io/commands/client-kill - * - * @example - *
-     * $redis->client('list'); // Get a list of clients
-     * $redis->client('getname'); // Get the name of the current connection
-     * $redis->client('setname', 'somename'); // Set the name of the current connection
-     * $redis->client('kill', ); // Kill the process at ip:port
-     * 
- */ - public function client($command, $value = '') - { - } - - /** - * A utility method to prefix the value with the prefix setting for phpredis. - * - * @param mixed $value The value you wish to prefix - * - * @return string If a prefix is set up, the value now prefixed. - * If there is no prefix, the value will be returned unchanged. - * - * @example - *
-     * $redis->setOption(Redis::OPT_PREFIX, 'my-prefix:');
-     * $redis->_prefix('my-value'); // Will return 'my-prefix:my-value'
-     * 
- */ - public function _prefix($value) - { - } - - /** - * A utility method to unserialize data with whatever serializer is set up. If there is no serializer set, the - * value will be returned unchanged. If there is a serializer set up, and the data passed in is malformed, an - * exception will be thrown. This can be useful if phpredis is serializing values, and you return something from - * redis in a LUA script that is serialized. - * - * @param string $value The value to be unserialized - * - * @return mixed - * @example - *
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
-     * $redis->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)
-     * 
- */ - public function _unserialize($value) - { - } - - /** - * A utility method to serialize values manually. This method allows you to serialize a value with whatever - * serializer is configured, manually. This can be useful for serialization/unserialization of data going in - * and out of EVAL commands as phpredis can't automatically do this itself. Note that if no serializer is - * set, phpredis will change Array values to 'Array', and Objects to 'Object'. - * - * @param mixed $value The value to be serialized. - * - * @return mixed - * @example - *
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
-     * $redis->_serialize("foo"); // returns "foo"
-     * $redis->_serialize(Array()); // Returns "Array"
-     * $redis->_serialize(new stdClass()); // Returns "Object"
-     *
-     * $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
-     * $redis->_serialize("foo"); // Returns 's:3:"foo";'
-     * 
- */ - public function _serialize($value) - { - } - - /** - * Dump a key out of a redis database, the value of which can later be passed into redis using the RESTORE command. - * The data that comes out of DUMP is a binary representation of the key as Redis stores it. - * @param string $key - * - * @return string|false The Redis encoded value of the key, or FALSE if the key doesn't exist - * - * @link https://redis.io/commands/dump - * @example - *
-     * $redis->set('foo', 'bar');
-     * $val = $redis->dump('foo'); // $val will be the Redis encoded key value
-     * 
- */ - public function dump($key) - { - } - - /** - * Restore a key from the result of a DUMP operation. - * - * @param string $key The key name - * @param int $ttl How long the key should live (if zero, no expire will be set on the key) - * @param string $value (binary). The Redis encoded key value (from DUMP) - * - * @return bool - * - * @link https://redis.io/commands/restore - * @example - *
-     * $redis->set('foo', 'bar');
-     * $val = $redis->dump('foo');
-     * $redis->restore('bar', 0, $val); // The key 'bar', will now be equal to the key 'foo'
-     * 
- */ - public function restore($key, $ttl, $value) - { - } - - /** - * Migrates a key to a different Redis instance. - * - * @param string $host The destination host - * @param int $port The TCP port to connect to. - * @param string $key The key to migrate. - * @param int $db The target DB. - * @param int $timeout The maximum amount of time given to this transfer. - * @param bool $copy Should we send the COPY flag to redis. - * @param bool $replace Should we send the REPLACE flag to redis. - * - * @return bool - * - * @link https://redis.io/commands/migrate - * @example - *
-     * $redis->migrate('backup', 6379, 'foo', 0, 3600);
-     * 
- */ - public function migrate($host, $port, $key, $db, $timeout, $copy = false, $replace = false) - { - } - - /** - * Return the current Redis server time. - * - * @return array If successful, the time will come back as an associative array with element zero being the - * unix timestamp, and element one being microseconds. - * - * @link https://redis.io/commands/time - * @example - *
-     * var_dump( $redis->time() );
-     * // array(2) {
-     * //   [0] => string(10) "1342364352"
-     * //   [1] => string(6) "253002"
-     * // }
-     * 
- */ - public function time() - { - } - - /** - * Scan the keyspace for keys - * - * @param int &$iterator Iterator, initialized to NULL. - * @param string $pattern Pattern to match. - * @param int $count Count of keys per iteration (only a suggestion to Redis). - * - * @return array|false This function will return an array of keys or FALSE if there are no more keys. - * - * @link https://redis.io/commands/scan - * @example - *
-     * $iterator = null;
-     * while(false !== ($keys = $redis->scan($iterator))) {
-     *     foreach($keys as $key) {
-     *         echo $key . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function scan(&$iterator, $pattern = null, $count = 0) - { - } - - /** - * Adds all the element arguments to the HyperLogLog data structure stored at the key. - * - * @param string $key - * @param array $elements - * - * @return bool - * - * @link https://redis.io/commands/pfadd - * @example $redis->pfAdd('key', array('elem1', 'elem2')) - */ - public function pfAdd($key, array $elements) - { - } - - /** - * When called with a single key, returns the approximated cardinality computed by the HyperLogLog data - * structure stored at the specified variable, which is 0 if the variable does not exist. - * - * @param string|array $key - * - * @return int - * - * @link https://redis.io/commands/pfcount - * @example - *
-     * $redis->pfAdd('key1', array('elem1', 'elem2'));
-     * $redis->pfAdd('key2', array('elem3', 'elem2'));
-     * $redis->pfCount('key1'); // int(2)
-     * $redis->pfCount(array('key1', 'key2')); // int(3)
-     * 
- */ - public function pfCount($key) - { - } - - /** - * Merge multiple HyperLogLog values into an unique value that will approximate the cardinality - * of the union of the observed Sets of the source HyperLogLog structures. - * - * @param string $destKey - * @param array $sourceKeys - * - * @return bool - * - * @link https://redis.io/commands/pfmerge - * @example - *
-     * $redis->pfAdd('key1', array('elem1', 'elem2'));
-     * $redis->pfAdd('key2', array('elem3', 'elem2'));
-     * $redis->pfMerge('key3', array('key1', 'key2'));
-     * $redis->pfCount('key3'); // int(3)
-     * 
- */ - public function pfMerge($destKey, array $sourceKeys) - { - } - - /** - * Send arbitrary things to the redis server. - * - * @param string $command Required command to send to the server. - * @param mixed $arguments Optional variable amount of arguments to send to the server. - * - * @return mixed - * - * @example - *
-     * $redis->rawCommand('SET', 'key', 'value'); // bool(true)
-     * $redis->rawCommand('GET", 'key'); // string(5) "value"
-     * 
- */ - public function rawCommand($command, $arguments) - { - } - - /** - * Detect whether we're in ATOMIC/MULTI/PIPELINE mode. - * - * @return int Either Redis::ATOMIC, Redis::MULTI or Redis::PIPELINE - * - * @example $redis->getMode(); - */ - public function getMode() - { - } - - /** - * Acknowledge one or more messages on behalf of a consumer group. - * - * @param string $stream - * @param string $group - * @param array $messages - * - * @return int The number of messages Redis reports as acknowledged. - * - * @link https://redis.io/commands/xack - * @example - *
-     * $redis->xAck('stream', 'group1', ['1530063064286-0', '1530063064286-1']);
-     * 
- */ - public function xAck($stream, $group, $messages) - { - } - - /** - * Add a message to a stream - * - * @param string $key - * @param string $id - * @param array $messages - * @param int $maxLen - * @param bool $isApproximate - * - * @return string The added message ID. - * - * @link https://redis.io/commands/xadd - * @example - *
-     * $redis->xAdd('mystream', "*", ['field' => 'value']);
-     * $redis->xAdd('mystream', "*", ['field' => 'value'], 10);
-     * $redis->xAdd('mystream', "*", ['field' => 'value'], 10, true);
-     * 
- */ - public function xAdd($key, $id, $messages, $maxLen = 0, $isApproximate = false) - { - } - - /** - * Claim ownership of one or more pending messages - * - * @param string $key - * @param string $group - * @param string $consumer - * @param int $minIdleTime - * @param array $ids - * @param array $options ['IDLE' => $value, 'TIME' => $value, 'RETRYCOUNT' => $value, 'FORCE', 'JUSTID'] - * - * @return array Either an array of message IDs along with corresponding data, or just an array of IDs - * (if the 'JUSTID' option was passed). - * - * @link https://redis.io/commands/xclaim - * @example - *
-     * $ids = ['1530113681011-0', '1530113681011-1', '1530113681011-2'];
-     *
-     * // Without any options
-     * $redis->xClaim('mystream', 'group1', 'myconsumer1', 0, $ids);
-     *
-     * // With options
-     * $redis->xClaim(
-     *     'mystream', 'group1', 'myconsumer2', 0, $ids,
-     *     [
-     *         'IDLE' => time() * 1000,
-     *         'RETRYCOUNT' => 5,
-     *         'FORCE',
-     *         'JUSTID'
-     *     ]
-     * );
-     * 
- */ - public function xClaim($key, $group, $consumer, $minIdleTime, $ids, $options = []) - { - } - - /** - * Delete one or more messages from a stream - * - * @param string $key - * @param array $ids - * - * @return int The number of messages removed - * - * @link https://redis.io/commands/xdel - * @example - *
-     * $redis->xDel('mystream', ['1530115304877-0', '1530115305731-0']);
-     * 
- */ - public function xDel($key, $ids) - { - } - - /** - * @param string $operation e.g.: 'HELP', 'SETID', 'DELGROUP', 'CREATE', 'DELCONSUMER' - * @param string $key - * @param string $group - * @param string $msgId - * @param bool $mkStream - * - * @return mixed This command returns different types depending on the specific XGROUP command executed. - * - * @link https://redis.io/commands/xgroup - * @example - *
-     * $redis->xGroup('CREATE', 'mystream', 'mygroup', 0);
-     * $redis->xGroup('CREATE', 'mystream', 'mygroup', 0, true); // create stream
-     * $redis->xGroup('DESTROY', 'mystream', 'mygroup');
-     * 
- */ - public function xGroup($operation, $key, $group, $msgId = '', $mkStream = false) - { - } - - /** - * Get information about a stream or consumer groups - * - * @param string $operation e.g.: 'CONSUMERS', 'GROUPS', 'STREAM', 'HELP' - * @param string $stream - * @param string $group - * - * @return mixed This command returns different types depending on which subcommand is used. - * - * @link https://redis.io/commands/xinfo - * @example - *
-     * $redis->xInfo('STREAM', 'mystream');
-     * 
- */ - public function xInfo($operation, $stream, $group) - { - } - - /** - * Get the length of a given stream. - * - * @param string $stream - * - * @return int The number of messages in the stream. - * - * @link https://redis.io/commands/xlen - * @example - *
-     * $redis->xLen('mystream');
-     * 
- */ - public function xLen($stream) - { - } - - /** - * Get information about pending messages in a given stream - * - * @param string $stream - * @param string $group - * @param string $start - * @param string $end - * @param int $count - * @param string $consumer - * - * @return array Information about the pending messages, in various forms depending on - * the specific invocation of XPENDING. - * - * @link https://redis.io/commands/xpending - * @example - *
-     * $redis->xPending('mystream', 'mygroup');
-     * $redis->xPending('mystream', 'mygroup', '-', '+', 1, 'consumer-1');
-     * 
- */ - public function xPending($stream, $group, $start = null, $end = null, $count = null, $consumer = null) - { - } - - /** - * Get a range of messages from a given stream - * - * @param string $stream - * @param string $start - * @param string $end - * @param int $count - * - * @return array The messages in the stream within the requested range. - * - * @link https://redis.io/commands/xrange - * @example - *
-     * // Get everything in this stream
-     * $redis->xRange('mystream', '-', '+');
-     * // Only the first two messages
-     * $redis->xRange('mystream', '-', '+', 2);
-     * 
- */ - public function xRange($stream, $start, $end, $count = null) - { - } - - /** - * Read data from one or more streams and only return IDs greater than sent in the command. - * - * @param array $streams - * @param int|string $count - * @param int|string $block - * - * @return array The messages in the stream newer than the IDs passed to Redis (if any) - * - * @link https://redis.io/commands/xread - * @example - *
-     * $redis->xRead(['stream1' => '1535222584555-0', 'stream2' => '1535222584555-0']);
-     * 
- */ - public function xRead($streams, $count = null, $block = null) - { - } - - /** - * This method is similar to xRead except that it supports reading messages for a specific consumer group. - * - * @param string $group - * @param string $consumer - * @param array $streams - * @param int|null $count - * @param int|null $block - * - * @return array The messages delivered to this consumer group (if any). - * - * @link https://redis.io/commands/xreadgroup - * @example - *
-     * // Consume messages for 'mygroup', 'consumer1'
-     * $redis->xReadGroup('mygroup', 'consumer1', ['s1' => 0, 's2' => 0]);
-     * // Read a single message as 'consumer2' for up to a second until a message arrives.
-     * $redis->xReadGroup('mygroup', 'consumer2', ['s1' => 0, 's2' => 0], 1, 1000);
-     * 
- */ - public function xReadGroup($group, $consumer, $streams, $count = null, $block = null) - { - } - - /** - * This is identical to xRange except the results come back in reverse order. - * Also note that Redis reverses the order of "start" and "end". - * - * @param string $stream - * @param string $end - * @param string $start - * @param int $count - * - * @return array The messages in the range specified - * - * @link https://redis.io/commands/xrevrange - * @example - *
-     * $redis->xRevRange('mystream', '+', '-');
-     * 
- */ - public function xRevRange($stream, $end, $start, $count = null) - { - } - - /** - * Trim the stream length to a given maximum. - * If the "approximate" flag is pasesed, Redis will use your size as a hint but only trim trees in whole nodes - * (this is more efficient) - * - * @param string $stream - * @param int $maxLen - * @param bool $isApproximate - * - * @return int The number of messages trimed from the stream. - * - * @link https://redis.io/commands/xtrim - * @example - *
-     * // Trim to exactly 100 messages
-     * $redis->xTrim('mystream', 100);
-     * // Let Redis approximate the trimming
-     * $redis->xTrim('mystream', 100, true);
-     * 
- */ - public function xTrim($stream, $maxLen, $isApproximate) - { - } - - /** - * Adds a values to the set value stored at key. - * - * @param string $key Required key - * @param array $values Required values - * - * @return int|bool The number of elements added to the set. - * If this value is already in the set, FALSE is returned - * - * @link https://redis.io/commands/sadd - * @link https://github.com/phpredis/phpredis/commit/3491b188e0022f75b938738f7542603c7aae9077 - * @since phpredis 2.2.8 - * @example - *
-     * $redis->sAddArray('k', array('v1'));                // boolean
-     * $redis->sAddArray('k', array('v1', 'v2', 'v3'));    // boolean
-     * 
- */ - public function sAddArray($key, array $values) - { - } -} - -class RedisException extends Exception -{ -} - -/** - * @mixin \Redis - */ -class RedisArray -{ - /** - * Constructor - * - * @param string|string[] $hosts Name of the redis array from redis.ini or array of hosts to construct the array with - * @param null|array $opts Array of options - * - * @link https://github.com/nicolasff/phpredis/blob/master/arrays.markdown - */ - public function __construct($hosts, ?array $opts = null) - { - } - - /** - * @return array list of hosts for the selected array - */ - public function _hosts() - { - } - - /** - * @return string the name of the function used to extract key parts during consistent hashing - */ - public function _function() - { - } - - /** - * @param string $key The key for which you want to lookup the host - * - * @return string the host to be used for a certain key - */ - public function _target($key) - { - } - - /** - * @param string $host The host you want to retrieve the instance for - * - * @return Redis a redis instance connected to a specific node - */ - public function _instance($host) - { - } - - /** - * Use this function when a new node is added and keys need to be rehashed. - */ - public function _rehash() - { - } - - /** - * Returns an associative array of strings and integers, with the following keys: - * - redis_version - * - redis_git_sha1 - * - redis_git_dirty - * - redis_build_id - * - redis_mode - * - os - * - arch_bits - * - multiplexing_api - * - atomicvar_api - * - gcc_version - * - process_id - * - run_id - * - tcp_port - * - uptime_in_seconds - * - uptime_in_days - * - hz - * - lru_clock - * - executable - * - config_file - * - connected_clients - * - client_longest_output_list - * - client_biggest_input_buf - * - blocked_clients - * - used_memory - * - used_memory_human - * - used_memory_rss - * - used_memory_rss_human - * - used_memory_peak - * - used_memory_peak_human - * - used_memory_peak_perc - * - used_memory_peak - * - used_memory_overhead - * - used_memory_startup - * - used_memory_dataset - * - used_memory_dataset_perc - * - total_system_memory - * - total_system_memory_human - * - used_memory_lua - * - used_memory_lua_human - * - maxmemory - * - maxmemory_human - * - maxmemory_policy - * - mem_fragmentation_ratio - * - mem_allocator - * - active_defrag_running - * - lazyfree_pending_objects - * - mem_fragmentation_ratio - * - loading - * - rdb_changes_since_last_save - * - rdb_bgsave_in_progress - * - rdb_last_save_time - * - rdb_last_bgsave_status - * - rdb_last_bgsave_time_sec - * - rdb_current_bgsave_time_sec - * - rdb_last_cow_size - * - aof_enabled - * - aof_rewrite_in_progress - * - aof_rewrite_scheduled - * - aof_last_rewrite_time_sec - * - aof_current_rewrite_time_sec - * - aof_last_bgrewrite_status - * - aof_last_write_status - * - aof_last_cow_size - * - changes_since_last_save - * - aof_current_size - * - aof_base_size - * - aof_pending_rewrite - * - aof_buffer_length - * - aof_rewrite_buffer_length - * - aof_pending_bio_fsync - * - aof_delayed_fsync - * - loading_start_time - * - loading_total_bytes - * - loading_loaded_bytes - * - loading_loaded_perc - * - loading_eta_seconds - * - total_connections_received - * - total_commands_processed - * - instantaneous_ops_per_sec - * - total_net_input_bytes - * - total_net_output_bytes - * - instantaneous_input_kbps - * - instantaneous_output_kbps - * - rejected_connections - * - maxclients - * - sync_full - * - sync_partial_ok - * - sync_partial_err - * - expired_keys - * - evicted_keys - * - keyspace_hits - * - keyspace_misses - * - pubsub_channels - * - pubsub_patterns - * - latest_fork_usec - * - migrate_cached_sockets - * - slave_expires_tracked_keys - * - active_defrag_hits - * - active_defrag_misses - * - active_defrag_key_hits - * - active_defrag_key_misses - * - role - * - master_replid - * - master_replid2 - * - master_repl_offset - * - second_repl_offset - * - repl_backlog_active - * - repl_backlog_size - * - repl_backlog_first_byte_offset - * - repl_backlog_histlen - * - master_host - * - master_port - * - master_link_status - * - master_last_io_seconds_ago - * - master_sync_in_progress - * - slave_repl_offset - * - slave_priority - * - slave_read_only - * - master_sync_left_bytes - * - master_sync_last_io_seconds_ago - * - master_link_down_since_seconds - * - connected_slaves - * - min-slaves-to-write - * - min-replicas-to-write - * - min_slaves_good_slaves - * - used_cpu_sys - * - used_cpu_user - * - used_cpu_sys_children - * - used_cpu_user_children - * - cluster_enabled - * - * @link https://redis.io/commands/info - * @return array - * @example - *
-     * $redis->info();
-     * 
- */ - public function info() { - } -} --- a/build/stubs/redis_cluster.php +++ /dev/null @@ -1,3575 +0,0 @@ - - * @link https://github.com/zgb7mtr/phpredis_cluster_phpdoc - * - * @method mixed eval($script, $args = array(), $numKeys = 0) - * - */ -class RedisCluster { - const AFTER = 'after'; - const BEFORE = 'before'; - - /** - * Options - */ - const OPT_SERIALIZER = 1; - const OPT_PREFIX = 2; - const OPT_READ_TIMEOUT = 3; - const OPT_SCAN = 4; - const OPT_SLAVE_FAILOVER = 5; - - /** - * Cluster options - */ - const FAILOVER_NONE = 0; - const FAILOVER_ERROR = 1; - const FAILOVER_DISTRIBUTE = 2; - const FAILOVER_DISTRIBUTE_SLAVES = 3; - - /** - * SCAN options - */ - const SCAN_NORETRY = 0; - const SCAN_RETRY = 1; - - /** - * @since 5.3.0 - */ - const SCAN_PREFIX = 2; - - /** - * @since 5.3.0 - */ - const SCAN_NOPREFIX = 3; - - /** - * Serializers - */ - const SERIALIZER_NONE = 0; - const SERIALIZER_PHP = 1; - const SERIALIZER_IGBINARY = 2; - const SERIALIZER_MSGPACK = 3; - const SERIALIZER_JSON = 4; - - /** - * Multi - */ - const ATOMIC = 0; - const MULTI = 1; - const PIPELINE = 2; - - /** - * Type - */ - const REDIS_NOT_FOUND = 0; - const REDIS_STRING = 1; - const REDIS_SET = 2; - const REDIS_LIST = 3; - const REDIS_ZSET = 4; - const REDIS_HASH = 5; - - /** - * Creates a Redis Cluster client - * - * @param string|null $name - * @param array $seeds - * @param float $timeout - * @param float $readTimeout - * @param bool $persistent - * @param string|string[]|null $auth - * @param array $connectionParameters extra config to send to redis - * @throws RedisClusterException - * - * @example - *
-     * // Declaring a cluster with an array of seeds
-     * $redisCluster = new RedisCluster(null,['127.0.0.1:6379']);
-     *
-     * // Loading a cluster configuration by name
-     * // In order to load a named array, one must first define the seed nodes in redis.ini.
-     * // The following lines would define the cluster 'mycluster', and be loaded automatically by phpredis.
-     *
-     * // # In redis.ini
-     * // redis.clusters.seeds = "mycluster[]=localhost:7000&test[]=localhost:7001"
-     * // redis.clusters.timeout = "mycluster=5"
-     * // redis.clusters.read_timeout = "mycluster=10"
-     *
-     * //Then, this cluster can be loaded by doing the following
-     *
-     * $redisClusterPro = new RedisCluster('mycluster');
-     * $redisClusterDev = new RedisCluster('test');
-     * 
- */ - public function __construct($name, $seeds, $timeout = null, $readTimeout = null, $persistent = false, $auth = null, $connectionParameters = []) { } - - /** - * Disconnects from the Redis instance, except when pconnect is used. - */ - public function close() { } - - /** - * Get the value related to the specified key - * - * @param string $key - * - * @return string|false If key didn't exist, FALSE is returned. Otherwise, the value related to this key is - * returned. - * - * @link https://redis.io/commands/get - * @example - *
-     * $redisCluster->get('key');
-     * 
- */ - public function get($key) { } - - /** - * Set the string value in argument as value of the key. - * - * @since If you're using Redis >= 2.6.12, you can pass extended options as explained in example - * - * @param string $key - * @param string $value - * @param int|array $timeout If you pass an integer, phpredis will redirect to SETEX, and will try to use Redis - * >= 2.6.12 extended options if you pass an array with valid values. - * - * @return bool TRUE if the command is successful. - * - * @link https://redis.io/commands/set - * @example - *
-     * // Simple key -> value set
-     * $redisCluster->set('key', 'value');
-     *
-     * // Will redirect, and actually make an SETEX call
-     * $redisCluster->set('key','value', 10);
-     *
-     * // Will set the key, if it doesn't exist, with a ttl of 10 seconds
-     * $redisCluster->set('key', 'value', Array('nx', 'ex'=>10));
-     *
-     * // Will set a key, if it does exist, with a ttl of 1000 milliseconds
-     * $redisCluster->set('key', 'value', Array('xx', 'px'=>1000));
-     * 
- */ - public function set($key, $value, $timeout = null) { } - - /** - * Returns the values of all specified keys. - * - * For every key that does not hold a string value or does not exist, - * the special value false is returned. Because of this, the operation never fails. - * - * @param array $array - * - * @return array - * - * @link https://redis.io/commands/mget - * @example - *
-     * $redisCluster->del('x', 'y', 'z', 'h');    // remove x y z
-     * $redisCluster->mset(array('x' => 'a', 'y' => 'b', 'z' => 'c'));
-     * $redisCluster->hset('h', 'field', 'value');
-     * var_dump($redisCluster->mget(array('x', 'y', 'z', 'h')));
-     * // Output:
-     * // array(3) {
-     * // [0]=>
-     * // string(1) "a"
-     * // [1]=>
-     * // string(1) "b"
-     * // [2]=>
-     * // string(1) "c"
-     * // [3]=>
-     * // bool(false)
-     * // }
-     * 
- */ - public function mget(array $array) { } - - /** - * Sets multiple key-value pairs in one atomic command. - * MSETNX only returns TRUE if all the keys were set (see SETNX). - * - * @param array $array Pairs: array(key => value, ...) - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/mset - * @example - *
-     * $redisCluster->mset(array('key0' => 'value0', 'key1' => 'value1'));
-     * var_dump($redisCluster->get('key0'));
-     * var_dump($redisCluster->get('key1'));
-     * // Output:
-     * // string(6) "value0"
-     * // string(6) "value1"
-     * 
- */ - public function mset(array $array) { } - - /** - * @see mset() - * - * @param array $array - * - * @return int 1 (if the keys were set) or 0 (no key was set) - * @link https://redis.io/commands/msetnx - */ - public function msetnx(array $array) { } - - /** - * Remove specified keys. - * - * @param int|string|array $key1 An array of keys, or an undefined number of parameters, each a key: key1 key2 key3 - * ... keyN - * @param int|string ...$otherKeys - * - * @return int Number of keys deleted. - * @link https://redis.io/commands/del - * @example - *
-     * $redisCluster->set('key1', 'val1');
-     * $redisCluster->set('key2', 'val2');
-     * $redisCluster->set('key3', 'val3');
-     * $redisCluster->set('key4', 'val4');
-     * $redisCluster->del('key1', 'key2');          // return 2
-     * $redisCluster->del(array('key3', 'key4'));   // return 2
-     * 
- */ - public function del($key1, ...$otherKeys) { } - - /** - * Set the string value in argument as value of the key, with a time to live. - * - * @param string $key - * @param int $ttl - * @param string $value - * - * @return bool TRUE if the command is successful. - * @link https://redis.io/commands/setex - * @example - *
-     * $redisCluster->setex('key', 3600, 'value'); // sets key → value, with 1h TTL.
-     * 
- */ - public function setex($key, $ttl, $value) { } - - /** - * PSETEX works exactly like SETEX with the sole difference that the expire time is specified in milliseconds - * instead of seconds. - * - * @param string $key - * @param int $ttl - * @param string $value - * - * @return bool TRUE if the command is successful. - * @link https://redis.io/commands/psetex - * @example - *
-     * $redisCluster->psetex('key', 1000, 'value'); // sets key → value, with 1s TTL.
-     * 
- */ - public function psetex($key, $ttl, $value) { } - - /** - * Set the string value in argument as value of the key if the key doesn't already exist in the database. - * - * @param string $key - * @param string $value - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/setnx - * @example - *
-     * $redisCluster->setnx('key', 'value');   // return TRUE
-     * $redisCluster->setnx('key', 'value');   // return FALSE
-     * 
- */ - public function setnx($key, $value) { } - - /** - * Sets a value and returns the previous entry at that key. - * - * @param string $key - * @param string $value - * - * @return string A string, the previous value located at this key. - * @link https://redis.io/commands/getset - * @example - *
-     * $redisCluster->set('x', '42');
-     * $exValue = $redisCluster->getSet('x', 'lol');   // return '42', replaces x by 'lol'
-     * $newValue = $redisCluster->get('x');            // return 'lol'
-     * 
- */ - public function getSet($key, $value) { } - - /** - * Verify if the specified key exists. - * - * @param string $key - * - * @return bool If the key exists, return TRUE, otherwise return FALSE. - * @link https://redis.io/commands/exists - * @example - *
-     * $redisCluster->set('key', 'value');
-     * $redisCluster->exists('key');               //  TRUE
-     * $redisCluster->exists('NonExistingKey');    // FALSE
-     * 
- */ - public function exists($key) { } - - /** - * Returns the keys that match a certain pattern. - * - * @param string $pattern pattern, using '*' as a wildcard. - * - * @return array of STRING: The keys that match a certain pattern. - * @link https://redis.io/commands/keys - * @example - *
-     * $allKeys = $redisCluster->keys('*');   // all keys will match this.
-     * $keyWithUserPrefix = $redisCluster->keys('user*');
-     * 
- */ - public function keys($pattern) { } - - /** - * Returns the type of data pointed by a given key. - * - * @param string $key - * - * @return int - * - * Depending on the type of the data pointed by the key, - * this method will return the following value: - * - string: RedisCluster::REDIS_STRING - * - set: RedisCluster::REDIS_SET - * - list: RedisCluster::REDIS_LIST - * - zset: RedisCluster::REDIS_ZSET - * - hash: RedisCluster::REDIS_HASH - * - other: RedisCluster::REDIS_NOT_FOUND - * @link https://redis.io/commands/type - * @example $redisCluster->type('key'); - */ - public function type($key) { } - - /** - * Returns and removes the first element of the list. - * - * @param string $key - * - * @return string|false if command executed successfully BOOL FALSE in case of failure (empty list) - * @link https://redis.io/commands/lpop - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');
-     * var_dump( $redisCluster->lRange('key1', 0, -1) );
-     * // Output:
-     * // array(3) {
-     * //   [0]=> string(1) "A"
-     * //   [1]=> string(1) "B"
-     * //   [2]=> string(1) "C"
-     * // }
-     * $redisCluster->lPop('key1');
-     * var_dump( $redisCluster->lRange('key1', 0, -1) );
-     * // Output:
-     * // array(2) {
-     * //   [0]=> string(1) "B"
-     * //   [1]=> string(1) "C"
-     * // }
-     * 
- */ - public function lPop($key) { } - - /** - * Returns and removes the last element of the list. - * - * @param string $key - * - * @return string|false if command executed successfully BOOL FALSE in case of failure (empty list) - * @link https://redis.io/commands/rpop - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');
-     * var_dump( $redisCluster->lRange('key1', 0, -1) );
-     * // Output:
-     * // array(3) {
-     * //   [0]=> string(1) "A"
-     * //   [1]=> string(1) "B"
-     * //   [2]=> string(1) "C"
-     * // }
-     * $redisCluster->rPop('key1');
-     * var_dump( $redisCluster->lRange('key1', 0, -1) );
-     * // Output:
-     * // array(2) {
-     * //   [0]=> string(1) "A"
-     * //   [1]=> string(1) "B"
-     * // }
-     * 
- */ - public function rPop($key) { } - - /** - * Set the list at index with the new value. - * - * @param string $key - * @param int $index - * @param string $value - * - * @return bool TRUE if the new value is setted. FALSE if the index is out of range, or data type identified by key - * is not a list. - * @link https://redis.io/commands/lset - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redisCluster->lGet('key1', 0);     // 'A'
-     * $redisCluster->lSet('key1', 0, 'X');
-     * $redisCluster->lGet('key1', 0);     // 'X'
-     * 
- */ - public function lSet($key, $index, $value) { } - - /** - * Removes and returns a random element from the set value at Key. - * - * @param string $key - * - * @return string "popped" value - * bool FALSE if set identified by key is empty or doesn't exist. - * @link https://redis.io/commands/spop - * @example - *
-     * $redisCluster->sAdd('key1' , 'set1');
-     * $redisCluster->sAdd('key1' , 'set2');
-     * $redisCluster->sAdd('key1' , 'set3');
-     * var_dump($redisCluster->sMembers('key1'));// 'key1' => {'set3', 'set1', 'set2'}
-     * $redisCluster->sPop('key1');// 'set1'
-     * var_dump($redisCluster->sMembers('key1'));// 'key1' => {'set3', 'set2'}
-     * $redisCluster->sPop('key1');// 'set3',
-     * var_dump($redisCluster->sMembers('key1'));// 'key1' => {'set2'}
-     * 
- */ - public function sPop($key) { } - - /** - * Adds the string values to the head (left) of the list. Creates the list if the key didn't exist. - * If the key exists and is not a list, FALSE is returned. - * - * @param string $key - * @param string $value1 String, value to push in key - * @param string $value2 Optional - * @param string $valueN Optional - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * @link https://redis.io/commands/lpush - * @example - *
-     * $redisCluster->lPush('l', 'v1', 'v2', 'v3', 'v4')   // int(4)
-     * var_dump( $redisCluster->lRange('l', 0, -1) );
-     * //// Output:
-     * // array(4) {
-     * //   [0]=> string(2) "v4"
-     * //   [1]=> string(2) "v3"
-     * //   [2]=> string(2) "v2"
-     * //   [3]=> string(2) "v1"
-     * // }
-     * 
- */ - public function lPush($key, $value1, $value2 = null, $valueN = null) { } - - /** - * Adds the string values to the tail (right) of the list. Creates the list if the key didn't exist. - * If the key exists and is not a list, FALSE is returned. - * - * @param string $key - * @param string $value1 String, value to push in key - * @param string $value2 Optional - * @param string $valueN Optional - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * @link https://redis.io/commands/rpush - * @example - *
-     * $redisCluster->rPush('r', 'v1', 'v2', 'v3', 'v4');    // int(4)
-     * var_dump( $redisCluster->lRange('r', 0, -1) );
-     * //// Output:
-     * // array(4) {
-     * //   [0]=> string(2) "v1"
-     * //   [1]=> string(2) "v2"
-     * //   [2]=> string(2) "v3"
-     * //   [3]=> string(2) "v4"
-     * // }
-     * 
- */ - public function rPush($key, $value1, $value2 = null, $valueN = null) { } - - /** - * BLPOP is a blocking list pop primitive. - * It is the blocking version of LPOP because it blocks the connection when - * there are no elements to pop from any of the given lists. - * An element is popped from the head of the first list that is non-empty, - * with the given keys being checked in the order that they are given. - * - * @param array $keys Array containing the keys of the lists - * Or STRING Key1 STRING Key2 STRING Key3 ... STRING Keyn - * @param int $timeout Timeout - * - * @return array array('listName', 'element') - * @link https://redis.io/commands/blpop - * @example - *
-     * // Non blocking feature
-     * $redisCluster->lPush('key1', 'A');
-     * $redisCluster->del('key2');
-     *
-     * $redisCluster->blPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redisCluster->blPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * $redisCluster->brPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redisCluster->brPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * // Blocking feature
-     *
-     * // process 1
-     * $redisCluster->del('key1');
-     * $redisCluster->blPop('key1', 10);
-     * // blocking for 10 seconds
-     *
-     * // process 2
-     * $redisCluster->lPush('key1', 'A');
-     *
-     * // process 1
-     * // array('key1', 'A') is returned
-     * 
- */ - public function blPop(array $keys, $timeout) { } - - /** - * BRPOP is a blocking list pop primitive. - * It is the blocking version of RPOP because it blocks the connection when - * there are no elements to pop from any of the given lists. - * An element is popped from the tail of the first list that is non-empty, - * with the given keys being checked in the order that they are given. - * See the BLPOP documentation(https://redis.io/commands/blpop) for the exact semantics, - * since BRPOP is identical to BLPOP with the only difference being that - * it pops elements from the tail of a list instead of popping from the head. - * - * @param array $keys Array containing the keys of the lists - * Or STRING Key1 STRING Key2 STRING Key3 ... STRING Keyn - * @param int $timeout Timeout - * - * @return array array('listName', 'element') - * @link https://redis.io/commands/brpop - * @example - *
-     * // Non blocking feature
-     * $redisCluster->lPush('key1', 'A');
-     * $redisCluster->del('key2');
-     *
-     * $redisCluster->blPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redisCluster->blPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * $redisCluster->brPop('key1', 'key2', 10); // array('key1', 'A')
-     * // OR
-     * $redisCluster->brPop(array('key1', 'key2'), 10); // array('key1', 'A')
-     *
-     * // Blocking feature
-     *
-     * // process 1
-     * $redisCluster->del('key1');
-     * $redisCluster->blPop('key1', 10);
-     * // blocking for 10 seconds
-     *
-     * // process 2
-     * $redisCluster->lPush('key1', 'A');
-     *
-     * // process 1
-     * // array('key1', 'A') is returned
-     * 
- */ - public function brPop(array $keys, $timeout) { } - - /** - * Adds the string value to the tail (right) of the list if the ist exists. FALSE in case of Failure. - * - * @param string $key - * @param string $value String, value to push in key - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * @link https://redis.io/commands/rpushx - * @example - *
-     * $redisCluster->del('key1');
-     * $redisCluster->rPushx('key1', 'A'); // returns 0
-     * $redisCluster->rPush('key1', 'A'); // returns 1
-     * $redisCluster->rPushx('key1', 'B'); // returns 2
-     * $redisCluster->rPushx('key1', 'C'); // returns 3
-     * // key1 now points to the following list: [ 'A', 'B', 'C' ]
-     * 
- */ - public function rPushx($key, $value) { } - - /** - * Adds the string value to the head (left) of the list if the list exists. - * - * @param string $key - * @param string $value String, value to push in key - * - * @return int|false The new length of the list in case of success, FALSE in case of Failure. - * @link https://redis.io/commands/lpushx - * @example - *
-     * $redisCluster->del('key1');
-     * $redisCluster->lPushx('key1', 'A');     // returns 0
-     * $redisCluster->lPush('key1', 'A');      // returns 1
-     * $redisCluster->lPushx('key1', 'B');     // returns 2
-     * $redisCluster->lPushx('key1', 'C');     // returns 3
-     * // key1 now points to the following list: [ 'C', 'B', 'A' ]
-     * 
- */ - public function lPushx($key, $value) { } - - /** - * Insert value in the list before or after the pivot value. the parameter options - * specify the position of the insert (before or after). If the list didn't exists, - * or the pivot didn't exists, the value is not inserted. - * - * @param string $key - * @param int $position RedisCluster::BEFORE | RedisCluster::AFTER - * @param string $pivot - * @param string $value - * - * @return int The number of the elements in the list, -1 if the pivot didn't exists. - * @link https://redis.io/commands/linsert - * @example - *
-     * $redisCluster->del('key1');
-     * $redisCluster->lInsert('key1', RedisCluster::AFTER, 'A', 'X');    // 0
-     *
-     * $redisCluster->lPush('key1', 'A');
-     * $redisCluster->lPush('key1', 'B');
-     * $redisCluster->lPush('key1', 'C');
-     *
-     * $redisCluster->lInsert('key1', RedisCluster::BEFORE, 'C', 'X');   // 4
-     * $redisCluster->lRange('key1', 0, -1);                      // array('X', 'C', 'B', 'A')
-     *
-     * $redisCluster->lInsert('key1', RedisCluster::AFTER, 'C', 'Y');    // 5
-     * $redisCluster->lRange('key1', 0, -1);                      // array('X', 'C', 'Y', 'B', 'A')
-     *
-     * $redisCluster->lInsert('key1', RedisCluster::AFTER, 'W', 'value'); // -1
-     * 
- */ - public function lInsert($key, $position, $pivot, $value) { } - - /** - * Return the specified element of the list stored at the specified key. - * 0 the first element, 1 the second ... -1 the last element, -2 the penultimate ... - * Return FALSE in case of a bad index or a key that doesn't point to a list. - * - * @param string $key - * @param int $index - * - * @return string|false the element at this index - * Bool FALSE if the key identifies a non-string data type, or no value corresponds to this index in the list Key. - * @link https://redis.io/commands/lindex - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redisCluster->lGet('key1', 0);     // 'A'
-     * $redisCluster->lGet('key1', -1);    // 'C'
-     * $redisCluster->lGet('key1', 10);    // `FALSE`
-     * 
- */ - public function lIndex($key, $index) { } - - /** - * Removes the first count occurrences of the value element from the list. - * If count is zero, all the matching elements are removed. If count is negative, - * elements are removed from tail to head. - * - * @param string $key - * @param string $value - * @param int $count - * - * @return int the number of elements to remove - * bool FALSE if the value identified by key is not a list. - * @link https://redis.io/commands/lrem - * @example - *
-     * $redisCluster->lPush('key1', 'A');
-     * $redisCluster->lPush('key1', 'B');
-     * $redisCluster->lPush('key1', 'C');
-     * $redisCluster->lPush('key1', 'A');
-     * $redisCluster->lPush('key1', 'A');
-     *
-     * $redisCluster->lRange('key1', 0, -1);   // array('A', 'A', 'C', 'B', 'A')
-     * $redisCluster->lRem('key1', 'A', 2);    // 2
-     * $redisCluster->lRange('key1', 0, -1);   // array('C', 'B', 'A')
-     * 
- */ - public function lRem($key, $value, $count) { } - - /** - * A blocking version of rpoplpush, with an integral timeout in the third parameter. - * - * @param string $srcKey - * @param string $dstKey - * @param int $timeout - * - * @return string|false The element that was moved in case of success, FALSE in case of timeout. - * @link https://redis.io/commands/brpoplpush - */ - public function brpoplpush($srcKey, $dstKey, $timeout) { } - - /** - * Pops a value from the tail of a list, and pushes it to the front of another list. - * Also return this value. - * - * @since redis >= 1.2 - * - * @param string $srcKey - * @param string $dstKey - * - * @return string|false The element that was moved in case of success, FALSE in case of failure. - * @link https://redis.io/commands/rpoplpush - * @example - *
-     * $redisCluster->del('x', 'y');
-     *
-     * $redisCluster->lPush('x', 'abc');
-     * $redisCluster->lPush('x', 'def');
-     * $redisCluster->lPush('y', '123');
-     * $redisCluster->lPush('y', '456');
-     *
-     * // move the last of x to the front of y.
-     * var_dump($redisCluster->rpoplpush('x', 'y'));
-     * var_dump($redisCluster->lRange('x', 0, -1));
-     * var_dump($redisCluster->lRange('y', 0, -1));
-     *
-     * ////Output:
-     * //
-     * //string(3) "abc"
-     * //array(1) {
-     * //  [0]=>
-     * //  string(3) "def"
-     * //}
-     * //array(3) {
-     * //  [0]=>
-     * //  string(3) "abc"
-     * //  [1]=>
-     * //  string(3) "456"
-     * //  [2]=>
-     * //  string(3) "123"
-     * //}
-     * 
- */ - public function rpoplpush($srcKey, $dstKey) { } - - /** - * Returns the size of a list identified by Key. If the list didn't exist or is empty, - * the command returns 0. If the data type identified by Key is not a list, the command return FALSE. - * - * @param string $key - * - * @return int The size of the list identified by Key exists. - * bool FALSE if the data type identified by Key is not list - * @link https://redis.io/commands/llen - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');  // key1 => [ 'A', 'B', 'C' ]
-     * $redisCluster->lLen('key1');       // 3
-     * $redisCluster->rPop('key1');
-     * $redisCluster->lLen('key1');       // 2
-     * 
- */ - public function lLen($key) { } - - /** - * Returns the set cardinality (number of elements) of the set stored at key. - * - * @param string $key - * - * @return int the cardinality (number of elements) of the set, or 0 if key does not exist. - * @link https://redis.io/commands/scard - * @example - *
-     * $redisCluster->sAdd('key1' , 'set1');
-     * $redisCluster->sAdd('key1' , 'set2');
-     * $redisCluster->sAdd('key1' , 'set3');   // 'key1' => {'set1', 'set2', 'set3'}
-     * $redisCluster->sCard('key1');           // 3
-     * $redisCluster->sCard('keyX');           // 0
-     * 
- */ - public function sCard($key) { } - - /** - * Returns all the members of the set value stored at key. - * This has the same effect as running SINTER with one argument key. - * - * @param string $key - * - * @return array All elements of the set. - * @link https://redis.io/commands/smembers - * @example - *
-     * $redisCluster->del('s');
-     * $redisCluster->sAdd('s', 'a');
-     * $redisCluster->sAdd('s', 'b');
-     * $redisCluster->sAdd('s', 'a');
-     * $redisCluster->sAdd('s', 'c');
-     * var_dump($redisCluster->sMembers('s'));
-     *
-     * ////Output:
-     * //
-     * //array(3) {
-     * //  [0]=>
-     * //  string(1) "b"
-     * //  [1]=>
-     * //  string(1) "c"
-     * //  [2]=>
-     * //  string(1) "a"
-     * //}
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function sMembers($key) { } - - /** - * Returns if member is a member of the set stored at key. - * - * @param string $key - * @param string $value - * - * @return bool TRUE if value is a member of the set at key key, FALSE otherwise. - * @link https://redis.io/commands/sismember - * @example - *
-     * $redisCluster->sAdd('key1' , 'set1');
-     * $redisCluster->sAdd('key1' , 'set2');
-     * $redisCluster->sAdd('key1' , 'set3'); // 'key1' => {'set1', 'set2', 'set3'}
-     *
-     * $redisCluster->sIsMember('key1', 'set1'); // TRUE
-     * $redisCluster->sIsMember('key1', 'setX'); // FALSE
-     * 
- */ - public function sIsMember($key, $value) { } - - /** - * Adds a values to the set value stored at key. - * If this value is already in the set, FALSE is returned. - * - * @param string $key Required key - * @param string $value1 Required value - * @param string $value2 Optional value - * @param string $valueN Optional value - * - * @return int|false The number of elements added to the set - * @link https://redis.io/commands/sadd - * @example - *
-     * $redisCluster->sAdd('k', 'v1');                // int(1)
-     * $redisCluster->sAdd('k', 'v1', 'v2', 'v3');    // int(2)
-     * 
- */ - public function sAdd($key, $value1, $value2 = null, $valueN = null) { } - - /** - * Adds a values to the set value stored at key. - * If this value is already in the set, FALSE is returned. - * - * @param string $key Required key - * @param array $valueArray - * - * @return int|false The number of elements added to the set - * @example - *
-     * $redisCluster->sAddArray('k', ['v1', 'v2', 'v3']);
-     * //This is a feature in php only. Same as $redisCluster->sAdd('k', 'v1', 'v2', 'v3');
-     * 
- */ - public function sAddArray($key, array $valueArray) { } - - /** - * Removes the specified members from the set value stored at key. - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string $memberN - * - * @return int The number of elements removed from the set. - * @link https://redis.io/commands/srem - * @example - *
-     * var_dump( $redisCluster->sAdd('k', 'v1', 'v2', 'v3') );    // int(3)
-     * var_dump( $redisCluster->sRem('k', 'v2', 'v3') );          // int(2)
-     * var_dump( $redisCluster->sMembers('k') );
-     * //// Output:
-     * // array(1) {
-     * //   [0]=> string(2) "v1"
-     * // }
-     * 
- */ - public function sRem($key, $member1, $member2 = null, $memberN = null) { } - - /** - * Performs the union between N sets and returns it. - * - * @param string $key1 Any number of keys corresponding to sets in redis. - * @param string $key2 ... - * @param string $keyN ... - * - * @return array of strings: The union of all these sets. - * @link https://redis.io/commands/sunionstore - * @example - *
-     * $redisCluster->del('s0', 's1', 's2');
-     *
-     * $redisCluster->sAdd('s0', '1');
-     * $redisCluster->sAdd('s0', '2');
-     * $redisCluster->sAdd('s1', '3');
-     * $redisCluster->sAdd('s1', '1');
-     * $redisCluster->sAdd('s2', '3');
-     * $redisCluster->sAdd('s2', '4');
-     *
-     * var_dump($redisCluster->sUnion('s0', 's1', 's2'));
-     *
-     * //// Output:
-     * //
-     * //array(4) {
-     * //  [0]=>
-     * //  string(1) "3"
-     * //  [1]=>
-     * //  string(1) "4"
-     * //  [2]=>
-     * //  string(1) "1"
-     * //  [3]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sUnion($key1, $key2, $keyN = null) { } - - /** - * Performs the same action as sUnion, but stores the result in the first key - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 Any number of keys corresponding to sets in redis. - * @param string $key2 ... - * @param string $keyN ... - * - * @return int Any number of keys corresponding to sets in redis. - * @link https://redis.io/commands/sunionstore - * @example - *
-     * $redisCluster->del('s0', 's1', 's2');
-     *
-     * $redisCluster->sAdd('s0', '1');
-     * $redisCluster->sAdd('s0', '2');
-     * $redisCluster->sAdd('s1', '3');
-     * $redisCluster->sAdd('s1', '1');
-     * $redisCluster->sAdd('s2', '3');
-     * $redisCluster->sAdd('s2', '4');
-     *
-     * var_dump($redisCluster->sUnionStore('dst', 's0', 's1', 's2'));
-     * var_dump($redisCluster->sMembers('dst'));
-     *
-     * //// Output:
-     * //
-     * //int(4)
-     * //array(4) {
-     * //  [0]=>
-     * //  string(1) "3"
-     * //  [1]=>
-     * //  string(1) "4"
-     * //  [2]=>
-     * //  string(1) "1"
-     * //  [3]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sUnionStore($dstKey, $key1, $key2, $keyN = null) { } - - /** - * Returns the members of a set resulting from the intersection of all the sets - * held at the specified keys. If just a single key is specified, then this command - * produces the members of this set. If one of the keys is missing, FALSE is returned. - * - * @param string $key1 keys identifying the different sets on which we will apply the intersection. - * @param string $key2 ... - * @param string $keyN ... - * - * @return array contain the result of the intersection between those keys. - * If the intersection between the different sets is empty, the return value will be empty array. - * @link https://redis.io/commands/sinterstore - * @example - *
-     * $redisCluster->sAdd('key1', 'val1');
-     * $redisCluster->sAdd('key1', 'val2');
-     * $redisCluster->sAdd('key1', 'val3');
-     * $redisCluster->sAdd('key1', 'val4');
-     *
-     * $redisCluster->sAdd('key2', 'val3');
-     * $redisCluster->sAdd('key2', 'val4');
-     *
-     * $redisCluster->sAdd('key3', 'val3');
-     * $redisCluster->sAdd('key3', 'val4');
-     *
-     * var_dump($redisCluster->sInter('key1', 'key2', 'key3'));
-     *
-     * // Output:
-     * //
-     * //array(2) {
-     * //  [0]=>
-     * //  string(4) "val4"
-     * //  [1]=>
-     * //  string(4) "val3"
-     * //}
-     * 
- */ - public function sInter($key1, $key2, $keyN = null) { } - - /** - * Performs a sInter command and stores the result in a new set. - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 are intersected as in sInter. - * @param string $key2 ... - * @param string $keyN ... - * - * @return int|false The cardinality of the resulting set, or FALSE in case of a missing key. - * @link https://redis.io/commands/sinterstore - * @example - *
-     * $redisCluster->sAdd('key1', 'val1');
-     * $redisCluster->sAdd('key1', 'val2');
-     * $redisCluster->sAdd('key1', 'val3');
-     * $redisCluster->sAdd('key1', 'val4');
-     *
-     * $redisCluster->sAdd('key2', 'val3');
-     * $redisCluster->sAdd('key2', 'val4');
-     *
-     * $redisCluster->sAdd('key3', 'val3');
-     * $redisCluster->sAdd('key3', 'val4');
-     *
-     * var_dump($redisCluster->sInterStore('output', 'key1', 'key2', 'key3'));
-     * var_dump($redisCluster->sMembers('output'));
-     *
-     * //// Output:
-     * //
-     * //int(2)
-     * //array(2) {
-     * //  [0]=>
-     * //  string(4) "val4"
-     * //  [1]=>
-     * //  string(4) "val3"
-     * //}
-     * 
- */ - public function sInterStore($dstKey, $key1, $key2, $keyN = null) { } - - /** - * Performs the difference between N sets and returns it. - * - * @param string $key1 Any number of keys corresponding to sets in redis. - * @param string $key2 ... - * @param string $keyN ... - * - * @return array of strings: The difference of the first set will all the others. - * @link https://redis.io/commands/sdiff - * @example - *
-     * $redisCluster->del('s0', 's1', 's2');
-     *
-     * $redisCluster->sAdd('s0', '1');
-     * $redisCluster->sAdd('s0', '2');
-     * $redisCluster->sAdd('s0', '3');
-     * $redisCluster->sAdd('s0', '4');
-     *
-     * $redisCluster->sAdd('s1', '1');
-     * $redisCluster->sAdd('s2', '3');
-     *
-     * var_dump($redisCluster->sDiff('s0', 's1', 's2'));
-     *
-     * //// Output:
-     * //
-     * //array(2) {
-     * //  [0]=>
-     * //  string(1) "4"
-     * //  [1]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sDiff($key1, $key2, $keyN = null) { } - - /** - * Performs the same action as sDiff, but stores the result in the first key - * - * @param string $dstKey the key to store the diff into. - * @param string $key1 Any number of keys corresponding to sets in redis - * @param string $key2 ... - * @param string $keyN ... - * - * @return int|false The cardinality of the resulting set, or FALSE in case of a missing key. - * @link https://redis.io/commands/sdiffstore - * @example - *
-     * $redisCluster->del('s0', 's1', 's2');
-     *
-     * $redisCluster->sAdd('s0', '1');
-     * $redisCluster->sAdd('s0', '2');
-     * $redisCluster->sAdd('s0', '3');
-     * $redisCluster->sAdd('s0', '4');
-     *
-     * $redisCluster->sAdd('s1', '1');
-     * $redisCluster->sAdd('s2', '3');
-     *
-     * var_dump($redisCluster->sDiffStore('dst', 's0', 's1', 's2'));
-     * var_dump($redisCluster->sMembers('dst'));
-     *
-     * //// Output:
-     * //
-     * //int(2)
-     * //array(2) {
-     * //  [0]=>
-     * //  string(1) "4"
-     * //  [1]=>
-     * //  string(1) "2"
-     * //}
-     * 
- */ - public function sDiffStore($dstKey, $key1, $key2, $keyN = null) { } - - /** - * Returns a random element(s) from the set value at Key, without removing it. - * - * @param string $key - * @param int $count [optional] - * - * @return string|array value(s) from the set - * bool FALSE if set identified by key is empty or doesn't exist and count argument isn't passed. - * @link https://redis.io/commands/srandmember - * @example - *
-     * $redisCluster->sAdd('key1' , 'one');
-     * $redisCluster->sAdd('key1' , 'two');
-     * $redisCluster->sAdd('key1' , 'three');              // 'key1' => {'one', 'two', 'three'}
-     *
-     * var_dump( $redisCluster->sRandMember('key1') );     // 'key1' => {'one', 'two', 'three'}
-     *
-     * // string(5) "three"
-     *
-     * var_dump( $redisCluster->sRandMember('key1', 2) );  // 'key1' => {'one', 'two', 'three'}
-     *
-     * // array(2) {
-     * //   [0]=> string(2) "one"
-     * //   [1]=> string(2) "three"
-     * // }
-     * 
- */ - public function sRandMember($key, $count = null) { } - - /** - * Get the length of a string value. - * - * @param string $key - * - * @return int - * @link https://redis.io/commands/strlen - * @example - *
-     * $redisCluster->set('key', 'value');
-     * $redisCluster->strlen('key'); // 5
-     * 
- */ - public function strlen($key) { } - - /** - * Remove the expiration timer from a key. - * - * @param string $key - * - * @return bool TRUE if a timeout was removed, FALSE if the key didn’t exist or didn’t have an expiration timer. - * @link https://redis.io/commands/persist - * @example $redisCluster->persist('key'); - */ - public function persist($key) { } - - /** - * Returns the remaining time to live of a key that has a timeout. - * This introspection capability allows a Redis client to check how many seconds a given key will continue to be - * part of the dataset. In Redis 2.6 or older the command returns -1 if the key does not exist or if the key exist - * but has no associated expire. Starting with Redis 2.8 the return value in case of error changed: Returns -2 if - * the key does not exist. Returns -1 if the key exists but has no associated expire. - * - * @param string $key - * - * @return int the time left to live in seconds. - * @link https://redis.io/commands/ttl - * @example $redisCluster->ttl('key'); - */ - public function ttl($key) { } - - /** - * Returns the remaining time to live of a key that has an expire set, - * with the sole difference that TTL returns the amount of remaining time in seconds while PTTL returns it in - * milliseconds. In Redis 2.6 or older the command returns -1 if the key does not exist or if the key exist but has - * no associated expire. Starting with Redis 2.8 the return value in case of error changed: Returns -2 if the key - * does not exist. Returns -1 if the key exists but has no associated expire. - * - * @param string $key - * - * @return int the time left to live in milliseconds. - * @link https://redis.io/commands/pttl - * @example $redisCluster->pttl('key'); - */ - public function pttl($key) { } - - /** - * Returns the cardinality of an ordered set. - * - * @param string $key - * - * @return int the set's cardinality - * @link https://redis.io/commands/zsize - * @example - *
-     * $redisCluster->zAdd('key', 0, 'val0');
-     * $redisCluster->zAdd('key', 2, 'val2');
-     * $redisCluster->zAdd('key', 10, 'val10');
-     * $redisCluster->zCard('key');            // 3
-     * 
- */ - public function zCard($key) { } - - /** - * Returns the number of elements of the sorted set stored at the specified key which have - * scores in the range [start,end]. Adding a parenthesis before start or end excludes it - * from the range. +inf and -inf are also valid limits. - * - * @param string $key - * @param string $start - * @param string $end - * - * @return int the size of a corresponding zRangeByScore. - * @link https://redis.io/commands/zcount - * @example - *
-     * $redisCluster->zAdd('key', 0, 'val0');
-     * $redisCluster->zAdd('key', 2, 'val2');
-     * $redisCluster->zAdd('key', 10, 'val10');
-     * $redisCluster->zCount('key', 0, 3); // 2, corresponding to array('val0', 'val2')
-     * 
- */ - public function zCount($key, $start, $end) { } - - /** - * Deletes the elements of the sorted set stored at the specified key which have scores in the range [start,end]. - * - * @param string $key - * @param float|string $start double or "+inf" or "-inf" string - * @param float|string $end double or "+inf" or "-inf" string - * - * @return int The number of values deleted from the sorted set - * @link https://redis.io/commands/zremrangebyscore - * @example - *
-     * $redisCluster->zAdd('key', 0, 'val0');
-     * $redisCluster->zAdd('key', 2, 'val2');
-     * $redisCluster->zAdd('key', 10, 'val10');
-     * $redisCluster->zRemRangeByScore('key', 0, 3); // 2
-     * 
- */ - public function zRemRangeByScore($key, $start, $end) { } - - /** - * Returns the score of a given member in the specified sorted set. - * - * @param string $key - * @param string $member - * - * @return float - * @link https://redis.io/commands/zscore - * @example - *
-     * $redisCluster->zAdd('key', 2.5, 'val2');
-     * $redisCluster->zScore('key', 'val2'); // 2.5
-     * 
- */ - public function zScore($key, $member) { } - - /** - * Adds the specified member with a given score to the sorted set stored at key. - * - * @param string $key Required key - * @param float $score1 Required score - * @param string $value1 Required value - * @param float $score2 Optional score - * @param string $value2 Optional value - * @param float $scoreN Optional score - * @param string $valueN Optional value - * - * @return int Number of values added - * @link https://redis.io/commands/zadd - * @example - *
-     * $redisCluster->zAdd('z', 1, 'v2', 2, 'v2', 3, 'v3', 4, 'v4' );  // int(3)
-     * $redisCluster->zRem('z', 'v2', 'v3');                           // int(2)
-     * var_dump( $redisCluster->zRange('z', 0, -1) );
-     *
-     * //// Output:
-     * // array(1) {
-     * //   [0]=> string(2) "v4"
-     * // }
-     * 
- */ - public function zAdd($key, $score1, $value1, $score2 = null, $value2 = null, $scoreN = null, $valueN = null) { } - - /** - * Increments the score of a member from a sorted set by a given amount. - * - * @param string $key - * @param float $value (double) value that will be added to the member's score - * @param string $member - * - * @return float the new value - * @link https://redis.io/commands/zincrby - * @example - *
-     * $redisCluster->del('key');
-     * $redisCluster->zIncrBy('key', 2.5, 'member1');// key or member1 didn't exist, so member1's score is to 0 ;
-     *                                              //before the increment and now has the value 2.5
-     * $redisCluster->zIncrBy('key', 1, 'member1');    // 3.5
-     * 
- */ - public function zIncrBy($key, $value, $member) { } - - /** - * Returns the length of a hash, in number of items - * - * @param string $key - * - * @return int|false the number of items in a hash, FALSE if the key doesn't exist or isn't a hash. - * @link https://redis.io/commands/hlen - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'key1', 'hello');
-     * $redisCluster->hSet('h', 'key2', 'plop');
-     * $redisCluster->hLen('h'); // returns 2
-     * 
- */ - public function hLen($key) { } - - /** - * Returns the keys in a hash, as an array of strings. - * - * @param string $key - * - * @return array An array of elements, the keys of the hash. This works like PHP's array_keys(). - * @link https://redis.io/commands/hkeys - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'a', 'x');
-     * $redisCluster->hSet('h', 'b', 'y');
-     * $redisCluster->hSet('h', 'c', 'z');
-     * $redisCluster->hSet('h', 'd', 't');
-     * var_dump($redisCluster->hKeys('h'));
-     *
-     * //// Output:
-     * //
-     * // array(4) {
-     * // [0]=>
-     * // string(1) "a"
-     * // [1]=>
-     * // string(1) "b"
-     * // [2]=>
-     * // string(1) "c"
-     * // [3]=>
-     * // string(1) "d"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hKeys($key) { } - - /** - * Returns the values in a hash, as an array of strings. - * - * @param string $key - * - * @return array An array of elements, the values of the hash. This works like PHP's array_values(). - * @link https://redis.io/commands/hvals - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'a', 'x');
-     * $redisCluster->hSet('h', 'b', 'y');
-     * $redisCluster->hSet('h', 'c', 'z');
-     * $redisCluster->hSet('h', 'd', 't');
-     * var_dump($redisCluster->hVals('h'));
-     *
-     * //// Output:
-     * //
-     * // array(4) {
-     * //   [0]=>
-     * //   string(1) "x"
-     * //   [1]=>
-     * //   string(1) "y"
-     * //   [2]=>
-     * //   string(1) "z"
-     * //   [3]=>
-     * //   string(1) "t"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hVals($key) { } - - /** - * Gets a value from the hash stored at key. - * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned. - * - * @param string $key - * @param string $hashKey - * - * @return string|false The value, if the command executed successfully BOOL FALSE in case of failure - * @link https://redis.io/commands/hget - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'a', 'x');
-     * $redisCluster->hGet('h', 'a'); // 'X'
-     * 
- */ - public function hGet($key, $hashKey) { } - - /** - * Returns the whole hash, as an array of strings indexed by strings. - * - * @param string $key - * - * @return array An array of elements, the contents of the hash. - * @link https://redis.io/commands/hgetall - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'a', 'x');
-     * $redisCluster->hSet('h', 'b', 'y');
-     * $redisCluster->hSet('h', 'c', 'z');
-     * $redisCluster->hSet('h', 'd', 't');
-     * var_dump($redisCluster->hGetAll('h'));
-     *
-     * //// Output:
-     * //
-     * // array(4) {
-     * //   ["a"]=>
-     * //   string(1) "x"
-     * //   ["b"]=>
-     * //   string(1) "y"
-     * //   ["c"]=>
-     * //   string(1) "z"
-     * //   ["d"]=>
-     * //   string(1) "t"
-     * // }
-     * // The order is random and corresponds to redis' own internal representation of the set structure.
-     * 
- */ - public function hGetAll($key) { } - - /** - * Verify if the specified member exists in a key. - * - * @param string $key - * @param string $hashKey - * - * @return bool If the member exists in the hash table, return TRUE, otherwise return FALSE. - * @link https://redis.io/commands/hexists - * @example - *
-     * $redisCluster->hSet('h', 'a', 'x');
-     * $redisCluster->hExists('h', 'a');               //  TRUE
-     * $redisCluster->hExists('h', 'NonExistingKey');  // FALSE
-     * 
- */ - public function hExists($key, $hashKey) { } - - /** - * Increments the value of a member from a hash by a given amount. - * - * @param string $key - * @param string $hashKey - * @param int $value (integer) value that will be added to the member's value - * - * @return int the new value - * @link https://redis.io/commands/hincrby - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hIncrBy('h', 'x', 2); // returns 2: h[x] = 2 now.
-     * $redisCluster->hIncrBy('h', 'x', 1); // h[x] ← 2 + 1. Returns 3
-     * 
- */ - public function hIncrBy($key, $hashKey, $value) { } - - /** - * Adds a value to the hash stored at key. If this value is already in the hash, FALSE is returned. - * - * @param string $key - * @param string $hashKey - * @param string $value - * - * @return int - * 1 if value didn't exist and was added successfully, - * 0 if the value was already present and was replaced, FALSE if there was an error. - * @link https://redis.io/commands/hset - * @example - *
-     * $redisCluster->del('h')
-     * $redisCluster->hSet('h', 'key1', 'hello');  // 1, 'key1' => 'hello' in the hash at "h"
-     * $redisCluster->hGet('h', 'key1');           // returns "hello"
-     *
-     * $redisCluster->hSet('h', 'key1', 'plop');   // 0, value was replaced.
-     * $redisCluster->hGet('h', 'key1');           // returns "plop"
-     * 
- */ - public function hSet($key, $hashKey, $value) { } - - /** - * Adds a value to the hash stored at key only if this field isn't already in the hash. - * - * @param string $key - * @param string $hashKey - * @param string $value - * - * @return bool TRUE if the field was set, FALSE if it was already present. - * @link https://redis.io/commands/hsetnx - * @example - *
-     * $redisCluster->del('h')
-     * $redisCluster->hSetNx('h', 'key1', 'hello'); // TRUE, 'key1' => 'hello' in the hash at "h"
-     * $redisCluster->hSetNx('h', 'key1', 'world'); // FALSE, 'key1' => 'hello' in the hash at "h". No change since the
-     * field wasn't replaced.
-     * 
- */ - public function hSetNx($key, $hashKey, $value) { } - - /** - * Retrieve the values associated to the specified fields in the hash. - * - * @param string $key - * @param array $hashKeys - * - * @return array Array An array of elements, the values of the specified fields in the hash, - * with the hash keys as array keys. - * @link https://redis.io/commands/hmget - * @example - *
-     * $redisCluster->del('h');
-     * $redisCluster->hSet('h', 'field1', 'value1');
-     * $redisCluster->hSet('h', 'field2', 'value2');
-     * $redisCluster->hMGet('h', array('field1', 'field2')); // returns array('field1' => 'value1', 'field2' =>
-     * 'value2')
-     * 
- */ - public function hMGet($key, $hashKeys) { } - - /** - * Fills in a whole hash. Non-string values are converted to string, using the standard (string) cast. - * NULL values are stored as empty strings - * - * @param string $key - * @param array $hashKeys key → value array - * - * @return bool - * @link https://redis.io/commands/hmset - * @example - *
-     * $redisCluster->del('user:1');
-     * $redisCluster->hMSet('user:1', array('name' => 'Joe', 'salary' => 2000));
-     * $redisCluster->hIncrBy('user:1', 'salary', 100); // Joe earns 100 more now.
-     * 
- */ - public function hMSet($key, $hashKeys) { } - - /** - * Removes a values from the hash stored at key. - * If the hash table doesn't exist, or the key doesn't exist, FALSE is returned. - * - * @param string $key - * @param string $hashKey1 - * @param string $hashKey2 - * @param string $hashKeyN - * - * @return int Number of deleted fields - * @link https://redis.io/commands/hdel - * @example - *
-     * $redisCluster->hMSet('h',
-     *               array(
-     *                    'f1' => 'v1',
-     *                    'f2' => 'v2',
-     *                    'f3' => 'v3',
-     *                    'f4' => 'v4',
-     *               ));
-     *
-     * var_dump( $redisCluster->hDel('h', 'f1') );        // int(1)
-     * var_dump( $redisCluster->hDel('h', 'f2', 'f3') );  // int(2)
-     *
-     * var_dump( $redisCluster->hGetAll('h') );
-     *
-     * //// Output:
-     * //
-     * //  array(1) {
-     * //    ["f4"]=> string(2) "v4"
-     * //  }
-     * 
- */ - public function hDel($key, $hashKey1, $hashKey2 = null, $hashKeyN = null) { } - - /** - * Increment the float value of a hash field by the given amount - * - * @param string $key - * @param string $field - * @param float $increment - * - * @return float - * @link https://redis.io/commands/hincrbyfloat - * @example - *
-     * $redisCluster->hset('h', 'float', 3);
-     * $redisCluster->hset('h', 'int',   3);
-     * var_dump( $redisCluster->hIncrByFloat('h', 'float', 1.5) ); // float(4.5)
-     *
-     * var_dump( $redisCluster->hGetAll('h') );
-     *
-     * //// Output:
-     * //
-     * // array(2) {
-     * //   ["float"]=>
-     * //   string(3) "4.5"
-     * //   ["int"]=>
-     * //   string(1) "3"
-     * // }
-     * 
- */ - public function hIncrByFloat($key, $field, $increment) { } - - /** - * Dump a key out of a redis database, the value of which can later be passed into redis using the RESTORE command. - * The data that comes out of DUMP is a binary representation of the key as Redis stores it. - * - * @param string $key - * - * @return string|false The Redis encoded value of the key, or FALSE if the key doesn't exist - * @link https://redis.io/commands/dump - * @example - *
-     * $redisCluster->set('foo', 'bar');
-     * $val = $redisCluster->dump('foo'); // $val will be the Redis encoded key value
-     * 
- */ - public function dump($key) { } - - /** - * Returns the rank of a given member in the specified sorted set, starting at 0 for the item - * with the smallest score. zRevRank starts at 0 for the item with the largest score. - * - * @param string $key - * @param string $member - * - * @return int the item's score. - * @link https://redis.io/commands/zrank - * @example - *
-     * $redisCluster->del('z');
-     * $redisCluster->zAdd('key', 1, 'one');
-     * $redisCluster->zAdd('key', 2, 'two');
-     * $redisCluster->zRank('key', 'one');     // 0
-     * $redisCluster->zRank('key', 'two');     // 1
-     * $redisCluster->zRevRank('key', 'one');  // 1
-     * $redisCluster->zRevRank('key', 'two');  // 0
-     * 
- */ - public function zRank($key, $member) { } - - /** - * @see zRank() - * - * @param string $key - * @param string $member - * - * @return int the item's score - * @link https://redis.io/commands/zrevrank - */ - public function zRevRank($key, $member) { } - - /** - * Increment the number stored at key by one. - * - * @param string $key - * - * @return int the new value - * @link https://redis.io/commands/incr - * @example - *
-     * $redisCluster->incr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value 1
-     * $redisCluster->incr('key1'); // 2
-     * $redisCluster->incr('key1'); // 3
-     * $redisCluster->incr('key1'); // 4
-     * 
- */ - public function incr($key) { } - - /** - * Decrement the number stored at key by one. - * - * @param string $key - * - * @return int the new value - * @link https://redis.io/commands/decr - * @example - *
-     * $redisCluster->decr('key1'); // key1 didn't exists, set to 0 before the increment and now has the value -1
-     * $redisCluster->decr('key1'); // -2
-     * $redisCluster->decr('key1'); // -3
-     * 
- */ - public function decr($key) { } - - /** - * Increment the number stored at key by one. If the second argument is filled, it will be used as the integer - * value of the increment. - * - * @param string $key key - * @param int $value value that will be added to key (only for incrBy) - * - * @return int the new value - * @link https://redis.io/commands/incrby - * @example - *
-     * $redisCluster->incr('key1');        // key1 didn't exists, set to 0 before the increment and now has the value 1
-     * $redisCluster->incr('key1');        // 2
-     * $redisCluster->incr('key1');        // 3
-     * $redisCluster->incr('key1');        // 4
-     * $redisCluster->incrBy('key1', 10);  // 14
-     * 
- */ - public function incrBy($key, $value) { } - - /** - * Decrement the number stored at key by one. If the second argument is filled, it will be used as the integer - * value of the decrement. - * - * @param string $key - * @param int $value that will be subtracted to key (only for decrBy) - * - * @return int the new value - * @link https://redis.io/commands/decrby - * @example - *
-     * $redisCluster->decr('key1');        // key1 didn't exists, set to 0 before the increment and now has the value -1
-     * $redisCluster->decr('key1');        // -2
-     * $redisCluster->decr('key1');        // -3
-     * $redisCluster->decrBy('key1', 10);  // -13
-     * 
- */ - public function decrBy($key, $value) { } - - /** - * Increment the float value of a key by the given amount - * - * @param string $key - * @param float $increment - * - * @return float - * @link https://redis.io/commands/incrbyfloat - * @example - *
-     * $redisCluster->set('x', 3);
-     * var_dump( $redisCluster->incrByFloat('x', 1.5) );   // float(4.5)
-     *
-     * var_dump( $redisCluster->get('x') );                // string(3) "4.5"
-     * 
- */ - public function incrByFloat($key, $increment) { } - - /** - * Sets an expiration date (a timeout) on an item. - * - * @param string $key The key that will disappear. - * @param int $ttl The key's remaining Time To Live, in seconds. - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/expire - * @example - *
-     * $redisCluster->set('x', '42');
-     * $redisCluster->expire('x', 3);  // x will disappear in 3 seconds.
-     * sleep(5);                    // wait 5 seconds
-     * $redisCluster->get('x');            // will return `FALSE`, as 'x' has expired.
-     * 
- */ - public function expire($key, $ttl) { } - - /** - * Sets an expiration date (a timeout in milliseconds) on an item. - * - * @param string $key The key that will disappear. - * @param int $ttl The key's remaining Time To Live, in milliseconds. - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/pexpire - * @example - *
-     * $redisCluster->set('x', '42');
-     * $redisCluster->pExpire('x', 11500); // x will disappear in 11500 milliseconds.
-     * $redisCluster->ttl('x');            // 12
-     * $redisCluster->pttl('x');           // 11500
-     * 
- */ - public function pExpire($key, $ttl) { } - - /** - * Sets an expiration date (a timestamp) on an item. - * - * @param string $key The key that will disappear. - * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time. - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/expireat - * @example - *
-     * $redisCluster->set('x', '42');
-     * $now = time();               // current timestamp
-     * $redisCluster->expireAt('x', $now + 3); // x will disappear in 3 seconds.
-     * sleep(5);                        // wait 5 seconds
-     * $redisCluster->get('x');                // will return `FALSE`, as 'x' has expired.
-     * 
- */ - public function expireAt($key, $timestamp) { } - - /** - * Sets an expiration date (a timestamp) on an item. Requires a timestamp in milliseconds - * - * @param string $key The key that will disappear. - * @param int $timestamp Unix timestamp. The key's date of death, in seconds from Epoch time. - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/pexpireat - * @example - *
-     * $redisCluster->set('x', '42');
-     * $redisCluster->pExpireAt('x', 1555555555005);
-     * $redisCluster->ttl('x');                       // 218270121
-     * $redisCluster->pttl('x');                      // 218270120575
-     * 
- */ - public function pExpireAt($key, $timestamp) { } - - /** - * Append specified string to the string stored in specified key. - * - * @param string $key - * @param string $value - * - * @return int Size of the value after the append - * @link https://redis.io/commands/append - * @example - *
-     * $redisCluster->set('key', 'value1');
-     * $redisCluster->append('key', 'value2'); // 12
-     * $redisCluster->get('key');              // 'value1value2'
-     * 
- */ - public function append($key, $value) { } - - /** - * Return a single bit out of a larger string - * - * @param string $key - * @param int $offset - * - * @return int the bit value (0 or 1) - * @link https://redis.io/commands/getbit - * @example - *
-     * $redisCluster->set('key', "\x7f");  // this is 0111 1111
-     * $redisCluster->getBit('key', 0);    // 0
-     * $redisCluster->getBit('key', 1);    // 1
-     * 
- */ - public function getBit($key, $offset) { } - - /** - * Changes a single bit of a string. - * - * @param string $key - * @param int $offset - * @param bool|int $value bool or int (1 or 0) - * - * @return int 0 or 1, the value of the bit before it was set. - * @link https://redis.io/commands/setbit - * @example - *
-     * $redisCluster->set('key', "*");     // ord("*") = 42 = 0x2f = "0010 1010"
-     * $redisCluster->setBit('key', 5, 1); // returns 0
-     * $redisCluster->setBit('key', 7, 1); // returns 0
-     * $redisCluster->get('key');          // chr(0x2f) = "/" = b("0010 1111")
-     * 
- */ - public function setBit($key, $offset, $value) { } - - /** - * Bitwise operation on multiple keys. - * - * @param string $operation either "AND", "OR", "NOT", "XOR" - * @param string $retKey return key - * @param string $key1 - * @param string $key2 - * @param string $key3 - * - * @return int The size of the string stored in the destination key. - * @link https://redis.io/commands/bitop - * @example - *
-     * $redisCluster->set('bit1', '1'); // 11 0001
-     * $redisCluster->set('bit2', '2'); // 11 0010
-     *
-     * $redisCluster->bitOp('AND', 'bit', 'bit1', 'bit2'); // bit = 110000
-     * $redisCluster->bitOp('OR',  'bit', 'bit1', 'bit2'); // bit = 110011
-     * $redisCluster->bitOp('NOT', 'bit', 'bit1', 'bit2'); // bit = 110011
-     * $redisCluster->bitOp('XOR', 'bit', 'bit1', 'bit2'); // bit = 11
-     * 
- */ - public function bitOp($operation, $retKey, $key1, $key2, $key3 = null) { } - - /** - * Return the position of the first bit set to 1 or 0 in a string. The position is returned, thinking of the - * string as an array of bits from left to right, where the first byte's most significant bit is at position 0, - * the second byte's most significant bit is at position 8, and so forth. - * - * @param string $key - * @param int $bit - * @param int $start - * @param int $end - * - * @return int The command returns the position of the first bit set to 1 or 0 according to the request. - * If we look for set bits (the bit argument is 1) and the string is empty or composed of just - * zero bytes, -1 is returned. If we look for clear bits (the bit argument is 0) and the string - * only contains bit set to 1, the function returns the first bit not part of the string on the - * right. So if the string is three bytes set to the value 0xff the command BITPOS key 0 will - * return 24, since up to bit 23 all the bits are 1. Basically, the function considers the right - * of the string as padded with zeros if you look for clear bits and specify no range or the - * start argument only. However, this behavior changes if you are looking for clear bits and - * specify a range with both start and end. If no clear bit is found in the specified range, the - * function returns -1 as the user specified a clear range and there are no 0 bits in that range. - * @link https://redis.io/commands/bitpos - * @example - *
-     * $redisCluster->set('key', '\xff\xff');
-     * $redisCluster->bitpos('key', 1); // int(0)
-     * $redisCluster->bitpos('key', 1, 1); // int(8)
-     * $redisCluster->bitpos('key', 1, 3); // int(-1)
-     * $redisCluster->bitpos('key', 0); // int(16)
-     * $redisCluster->bitpos('key', 0, 1); // int(16)
-     * $redisCluster->bitpos('key', 0, 1, 5); // int(-1)
-     * 
- */ - public function bitpos($key, $bit, $start = 0, $end = null) { } - - /** - * Count bits in a string. - * - * @param string $key - * - * @return int The number of bits set to 1 in the value behind the input key. - * @link https://redis.io/commands/bitcount - * @example - *
-     * $redisCluster->set('bit', '345'); // // 11 0011  0011 0100  0011 0101
-     * var_dump( $redisCluster->bitCount('bit', 0, 0) ); // int(4)
-     * var_dump( $redisCluster->bitCount('bit', 1, 1) ); // int(3)
-     * var_dump( $redisCluster->bitCount('bit', 2, 2) ); // int(4)
-     * var_dump( $redisCluster->bitCount('bit', 0, 2) ); // int(11)
-     * 
- */ - public function bitCount($key) { } - - /** - * @see lIndex() - * - * @param string $key - * @param int $index - * - * @link https://redis.io/commands/lindex - */ - public function lGet($key, $index) { } - - /** - * Return a substring of a larger string - * - * @param string $key - * @param int $start - * @param int $end - * - * @return string the substring - * @link https://redis.io/commands/getrange - * @example - *
-     * $redisCluster->set('key', 'string value');
-     * $redisCluster->getRange('key', 0, 5);   // 'string'
-     * $redisCluster->getRange('key', -5, -1); // 'value'
-     * 
- */ - public function getRange($key, $start, $end) { } - - /** - * Trims an existing list so that it will contain only a specified range of elements. - * - * @param string $key - * @param int $start - * @param int $stop - * - * @return array|false Bool return FALSE if the key identify a non-list value. - * @link https://redis.io/commands/ltrim - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');
-     * $redisCluster->lRange('key1', 0, -1); // array('A', 'B', 'C')
-     * $redisCluster->lTrim('key1', 0, 1);
-     * $redisCluster->lRange('key1', 0, -1); // array('A', 'B')
-     * 
- */ - public function lTrim($key, $start, $stop) { } - - /** - * Returns the specified elements of the list stored at the specified key in - * the range [start, end]. start and stop are interpretated as indices: 0 the first element, - * 1 the second ... -1 the last element, -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * - * @return array containing the values in specified range. - * @link https://redis.io/commands/lrange - * @example - *
-     * $redisCluster->rPush('key1', 'A');
-     * $redisCluster->rPush('key1', 'B');
-     * $redisCluster->rPush('key1', 'C');
-     * $redisCluster->lRange('key1', 0, -1); // array('A', 'B', 'C')
-     * 
- */ - public function lRange($key, $start, $end) { } - - /** - * Deletes the elements of the sorted set stored at the specified key which have rank in the range [start,end]. - * - * @param string $key - * @param int $start - * @param int $end - * - * @return int The number of values deleted from the sorted set - * @link https://redis.io/commands/zremrangebyrank - * @example - *
-     * $redisCluster->zAdd('key', 1, 'one');
-     * $redisCluster->zAdd('key', 2, 'two');
-     * $redisCluster->zAdd('key', 3, 'three');
-     * $redisCluster->zRemRangeByRank('key', 0, 1); // 2
-     * $redisCluster->zRange('key', 0, -1, true); // array('three' => 3)
-     * 
- */ - public function zRemRangeByRank($key, $start, $end) { } - - /** - * Publish messages to channels. Warning: this function will probably change in the future. - * - * @param string $channel a channel to publish to - * @param string $message string - * - * @link https://redis.io/commands/publish - * @return int Number of clients that received the message - * @example $redisCluster->publish('chan-1', 'hello, world!'); // send message. - */ - public function publish($channel, $message) { } - - /** - * Renames a key. - * - * @param string $srcKey - * @param string $dstKey - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/rename - * @example - *
-     * $redisCluster->set('x', '42');
-     * $redisCluster->rename('x', 'y');
-     * $redisCluster->get('y');   // → 42
-     * $redisCluster->get('x');   // → `FALSE`
-     * 
- */ - public function rename($srcKey, $dstKey) { } - - /** - * Renames a key. - * - * Same as rename, but will not replace a key if the destination already exists. - * This is the same behaviour as setNx. - * - * @param string $srcKey - * @param string $dstKey - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/renamenx - * @example - *
-     * $redisCluster->set('x', '42');
-     * $redisCluster->renameNx('x', 'y');
-     * $redisCluster->get('y');   // → 42
-     * $redisCluster->get('x');   // → `FALSE`
-     * 
- */ - public function renameNx($srcKey, $dstKey) { } - - /** - * When called with a single key, returns the approximated cardinality computed by the HyperLogLog data - * structure stored at the specified variable, which is 0 if the variable does not exist. - * - * @param string|array $key - * - * @return int - * @link https://redis.io/commands/pfcount - * @example - *
-     * $redisCluster->pfAdd('key1', array('elem1', 'elem2'));
-     * $redisCluster->pfAdd('key2', array('elem3', 'elem2'));
-     * $redisCluster->pfCount('key1'); // int(2)
-     * $redisCluster->pfCount(array('key1', 'key2')); // int(3)
-     * 
- */ - public function pfCount($key) { } - - /** - * Adds all the element arguments to the HyperLogLog data structure stored at the key. - * - * @param string $key - * @param array $elements - * - * @return bool - * @link https://redis.io/commands/pfadd - * @example $redisCluster->pfAdd('key', array('elem1', 'elem2')) - */ - public function pfAdd($key, array $elements) { } - - /** - * Merge multiple HyperLogLog values into an unique value that will approximate the cardinality - * of the union of the observed Sets of the source HyperLogLog structures. - * - * @param string $destKey - * @param array $sourceKeys - * - * @return bool - * @link https://redis.io/commands/pfmerge - * @example - *
-     * $redisCluster->pfAdd('key1', array('elem1', 'elem2'));
-     * $redisCluster->pfAdd('key2', array('elem3', 'elem2'));
-     * $redisCluster->pfMerge('key3', array('key1', 'key2'));
-     * $redisCluster->pfCount('key3'); // int(3)
-     * 
- */ - public function pfMerge($destKey, array $sourceKeys) { } - - /** - * Changes a substring of a larger string. - * - * @param string $key - * @param int $offset - * @param string $value - * - * @return string the length of the string after it was modified. - * @link https://redis.io/commands/setrange - * @example - *
-     * $redisCluster->set('key', 'Hello world');
-     * $redisCluster->setRange('key', 6, "redis"); // returns 11
-     * $redisCluster->get('key');                  // "Hello redis"
-     * 
- */ - public function setRange($key, $offset, $value) { } - - /** - * Restore a key from the result of a DUMP operation. - * - * @param string $key The key name - * @param int $ttl How long the key should live (if zero, no expire will be set on the key) - * @param string $value (binary). The Redis encoded key value (from DUMP) - * - * @return bool - * @link https://redis.io/commands/restore - * @example - *
-     * $redisCluster->set('foo', 'bar');
-     * $val = $redisCluster->dump('foo');
-     * $redisCluster->restore('bar', 0, $val); // The key 'bar', will now be equal to the key 'foo'
-     * 
- */ - public function restore($key, $ttl, $value) { } - - /** - * Moves the specified member from the set at srcKey to the set at dstKey. - * - * @param string $srcKey - * @param string $dstKey - * @param string $member - * - * @return bool If the operation is successful, return TRUE. - * If the srcKey and/or dstKey didn't exist, and/or the member didn't exist in srcKey, FALSE is returned. - * @link https://redis.io/commands/smove - * @example - *
-     * $redisCluster->sAdd('key1' , 'set11');
-     * $redisCluster->sAdd('key1' , 'set12');
-     * $redisCluster->sAdd('key1' , 'set13');          // 'key1' => {'set11', 'set12', 'set13'}
-     * $redisCluster->sAdd('key2' , 'set21');
-     * $redisCluster->sAdd('key2' , 'set22');          // 'key2' => {'set21', 'set22'}
-     * $redisCluster->sMove('key1', 'key2', 'set13');  // 'key1' =>  {'set11', 'set12'}
-     *                                          // 'key2' =>  {'set21', 'set22', 'set13'}
-     * 
- */ - public function sMove($srcKey, $dstKey, $member) { } - - /** - * Returns a range of elements from the ordered set stored at the specified key, - * with values in the range [start, end]. start and stop are interpreted as zero-based indices: - * 0 the first element, - * 1 the second ... - * -1 the last element, - * -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * @param bool $withscores - * - * @return array Array containing the values in specified range. - * @link https://redis.io/commands/zrange - * @example - *
-     * $redisCluster->zAdd('key1', 0, 'val0');
-     * $redisCluster->zAdd('key1', 2, 'val2');
-     * $redisCluster->zAdd('key1', 10, 'val10');
-     * $redisCluster->zRange('key1', 0, -1); // array('val0', 'val2', 'val10')
-     * // with scores
-     * $redisCluster->zRange('key1', 0, -1, true); // array('val0' => 0, 'val2' => 2, 'val10' => 10)
-     * 
- */ - public function zRange($key, $start, $end, $withscores = null) { } - - /** - * Returns the elements of the sorted set stored at the specified key in the range [start, end] - * in reverse order. start and stop are interpretated as zero-based indices: - * 0 the first element, - * 1 the second ... - * -1 the last element, - * -2 the penultimate ... - * - * @param string $key - * @param int $start - * @param int $end - * @param bool $withscore - * - * @return array Array containing the values in specified range. - * @link https://redis.io/commands/zrevrange - * @example - *
-     * $redisCluster->zAdd('key', 0, 'val0');
-     * $redisCluster->zAdd('key', 2, 'val2');
-     * $redisCluster->zAdd('key', 10, 'val10');
-     * $redisCluster->zRevRange('key', 0, -1); // array('val10', 'val2', 'val0')
-     *
-     * // with scores
-     * $redisCluster->zRevRange('key', 0, -1, true); // array('val10' => 10, 'val2' => 2, 'val0' => 0)
-     * 
- */ - public function zRevRange($key, $start, $end, $withscore = null) { } - - /** - * Returns the elements of the sorted set stored at the specified key which have scores in the - * range [start,end]. Adding a parenthesis before start or end excludes it from the range. - * +inf and -inf are also valid limits. - * - * zRevRangeByScore returns the same items in reverse order, when the start and end parameters are swapped. - * - * @param string $key - * @param int $start - * @param int $end - * @param array $options Two options are available: - * - withscores => TRUE, - * - and limit => array($offset, $count) - * - * @return array Array containing the values in specified range. - * @link https://redis.io/commands/zrangebyscore - * @example - *
-     * $redisCluster->zAdd('key', 0, 'val0');
-     * $redisCluster->zAdd('key', 2, 'val2');
-     * $redisCluster->zAdd('key', 10, 'val10');
-     * $redisCluster->zRangeByScore('key', 0, 3);
-     * // array('val0', 'val2')
-     * $redisCluster->zRangeByScore('key', 0, 3, array('withscores' => TRUE);
-     * // array('val0' => 0, 'val2' => 2)
-     * $redisCluster->zRangeByScore('key', 0, 3, array('limit' => array(1, 1));
-     * // array('val2' => 2)
-     * $redisCluster->zRangeByScore('key', 0, 3, array('limit' => array(1, 1));
-     * // array('val2')
-     * $redisCluster->zRangeByScore('key', 0, 3, array('withscores' => TRUE, 'limit' => array(1, 1));
-     * // array('val2'=> 2)
-     * 
- */ - public function zRangeByScore($key, $start, $end, array $options = array()) { } - - /** - * @see zRangeByScore() - * - * @param string $key - * @param int $start - * @param int $end - * @param array $options - * - * @return array - */ - public function zRevRangeByScore($key, $start, $end, array $options = array()) { } - - /** - * Returns a range of members in a sorted set, by lexicographical range - * - * @param string $key The ZSET you wish to run against. - * @param int $min The minimum alphanumeric value you wish to get. - * @param int $max The maximum alphanumeric value you wish to get. - * @param int $offset Optional argument if you wish to start somewhere other than the first element. - * @param int $limit Optional argument if you wish to limit the number of elements returned. - * - * @return array Array containing the values in the specified range. - * @link https://redis.io/commands/zrangebylex - * @example - *
-     * foreach (array('a', 'b', 'c', 'd', 'e', 'f', 'g') as $k => $char) {
-     *     $redisCluster->zAdd('key', $k, $char);
-     * }
-     *
-     * $redisCluster->zRangeByLex('key', '-', '[c'); // array('a', 'b', 'c')
-     * $redisCluster->zRangeByLex('key', '-', '(c'); // array('a', 'b')
-     * $redisCluster->zRevRangeByLex('key', '(c','-'); // array('b', 'a')
-     * 
- */ - public function zRangeByLex($key, $min, $max, $offset = null, $limit = null) { } - - /** - * @see zRangeByLex() - * - * @param string $key - * @param int $min - * @param int $max - * @param int $offset - * @param int $limit - * - * @return array - * @link https://redis.io/commands/zrevrangebylex - */ - public function zRevRangeByLex($key, $min, $max, $offset = null, $limit = null) { } - - /** - * Count the number of members in a sorted set between a given lexicographical range. - * - * @param string $key - * @param int $min - * @param int $max - * - * @return int The number of elements in the specified score range. - * @link https://redis.io/commands/zlexcount - * @example - *
-     * foreach (array('a', 'b', 'c', 'd', 'e', 'f', 'g') as $k => $char) {
-     *     $redisCluster->zAdd('key', $k, $char);
-     * }
-     * $redisCluster->zLexCount('key', '[b', '[f'); // 5
-     * 
- */ - public function zLexCount($key, $min, $max) { } - - /** - * Remove all members in a sorted set between the given lexicographical range. - * - * @param string $key The ZSET you wish to run against. - * @param int $min The minimum alphanumeric value you wish to get. - * @param int $max The maximum alphanumeric value you wish to get. - * - * @return array the number of elements removed. - * @link https://redis.io/commands/zremrangebylex - * @example - *
-     * foreach (array('a', 'b', 'c', 'd', 'e', 'f', 'g') as $k => $char) {
-     *     $redisCluster->zAdd('key', $k, $char);
-     * }
-     * $redisCluster->zRemRangeByLex('key', '(b','[d'); // 2 , remove element 'c' and 'd'
-     * $redisCluster->zRange('key',0,-1);// array('a','b','e','f','g')
-     * 
- */ - public function zRemRangeByLex($key, $min, $max) { - } - - /** - * Add multiple sorted sets and store the resulting sorted set in a new key - * - * @param string $Output - * @param array $ZSetKeys - * @param null|array $Weights - * @param string $aggregateFunction Either "SUM", "MIN", or "MAX": defines the behaviour to use on - * duplicate entries during the zUnion. - * - * @return int The number of values in the new sorted set. - * @link https://redis.io/commands/zunionstore - * @example - *
-     * $redisCluster->del('k1');
-     * $redisCluster->del('k2');
-     * $redisCluster->del('k3');
-     * $redisCluster->del('ko1');
-     * $redisCluster->del('ko2');
-     * $redisCluster->del('ko3');
-     *
-     * $redisCluster->zAdd('k1', 0, 'val0');
-     * $redisCluster->zAdd('k1', 1, 'val1');
-     *
-     * $redisCluster->zAdd('k2', 2, 'val2');
-     * $redisCluster->zAdd('k2', 3, 'val3');
-     *
-     * $redisCluster->zUnionStore('ko1', array('k1', 'k2')); // 4, 'ko1' => array('val0', 'val1', 'val2', 'val3')
-     *
-     * // Weighted zUnionStore
-     * $redisCluster->zUnionStore('ko2', array('k1', 'k2'), array(1, 1)); // 4, 'ko2' => array('val0', 'val1', 'val2','val3')
-     * $redisCluster->zUnionStore('ko3', array('k1', 'k2'), array(5, 1)); // 4, 'ko3' => array('val0', 'val2', 'val3','val1')
-     * 
- */ - public function zUnionStore($Output, $ZSetKeys, ?array $Weights = null, $aggregateFunction = 'SUM') { } - - /** - * Intersect multiple sorted sets and store the resulting sorted set in a new key - * - * @param string $Output - * @param array $ZSetKeys - * @param null|array $Weights - * @param string $aggregateFunction Either "SUM", "MIN", or "MAX": - * defines the behaviour to use on duplicate entries during the zInterStore. - * - * @return int The number of values in the new sorted set. - * @link https://redis.io/commands/zinterstore - * @example - *
-     * $redisCluster->del('k1');
-     * $redisCluster->del('k2');
-     * $redisCluster->del('k3');
-     *
-     * $redisCluster->del('ko1');
-     * $redisCluster->del('ko2');
-     * $redisCluster->del('ko3');
-     * $redisCluster->del('ko4');
-     *
-     * $redisCluster->zAdd('k1', 0, 'val0');
-     * $redisCluster->zAdd('k1', 1, 'val1');
-     * $redisCluster->zAdd('k1', 3, 'val3');
-     *
-     * $redisCluster->zAdd('k2', 2, 'val1');
-     * $redisCluster->zAdd('k2', 3, 'val3');
-     *
-     * $redisCluster->zInterStore('ko1', array('k1', 'k2'));               // 2, 'ko1' => array('val1', 'val3')
-     * $redisCluster->zInterStore('ko2', array('k1', 'k2'), array(1, 1));  // 2, 'ko2' => array('val1', 'val3')
-     *
-     * // Weighted zInterStore
-     * $redisCluster->zInterStore('ko3', array('k1', 'k2'), array(1, 5), 'min'); // 2, 'ko3' => array('val1', 'val3')
-     * $redisCluster->zInterStore('ko4', array('k1', 'k2'), array(1, 5), 'max'); // 2, 'ko4' => array('val3', 'val1')
-     * 
- */ - public function zInterStore($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM') { } - - /** - * Deletes a specified member from the ordered set. - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string $memberN - * - * @return int Number of deleted values - * @link https://redis.io/commands/zrem - * @example - *
-     * $redisCluster->zAdd('z', 1, 'v1', 2, 'v2', 3, 'v3', 4, 'v4' );  // int(2)
-     * $redisCluster->zRem('z', 'v2', 'v3');                           // int(2)
-     * var_dump( $redisCluster->zRange('z', 0, -1) );
-     * //// Output:
-     * //
-     * // array(2) {
-     * //   [0]=> string(2) "v1"
-     * //   [1]=> string(2) "v4"
-     * // }
-     * 
- */ - public function zRem($key, $member1, $member2 = null, $memberN = null) { } - - /** - * Sort - * - * @param string $key - * @param array $option array(key => value, ...) - optional, with the following keys and values: - * - 'by' => 'some_pattern_*', - * - 'limit' => array(0, 1), - * - 'get' => 'some_other_pattern_*' or an array of patterns, - * - 'sort' => 'asc' or 'desc', - * - 'alpha' => TRUE, - * - 'store' => 'external-key' - * - * @return array - * An array of values, or a number corresponding to the number of elements stored if that was used. - * @link https://redis.io/commands/sort - * @example - *
-     * $redisCluster->del('s');
-     * $redisCluster->sadd('s', 5);
-     * $redisCluster->sadd('s', 4);
-     * $redisCluster->sadd('s', 2);
-     * $redisCluster->sadd('s', 1);
-     * $redisCluster->sadd('s', 3);
-     *
-     * var_dump($redisCluster->sort('s')); // 1,2,3,4,5
-     * var_dump($redisCluster->sort('s', array('sort' => 'desc'))); // 5,4,3,2,1
-     * var_dump($redisCluster->sort('s', array('sort' => 'desc', 'store' => 'out'))); // (int)5
-     * 
- */ - public function sort($key, $option = null) { } - - /** - * Describes the object pointed to by a key. - * The information to retrieve (string) and the key (string). - * Info can be one of the following: - * - "encoding" - * - "refcount" - * - "idletime" - * - * @param string $string - * @param string $key - * - * @return string|false for "encoding", int for "refcount" and "idletime", FALSE if the key doesn't exist. - * @link https://redis.io/commands/object - * @example - *
-     * $redisCluster->object("encoding", "l"); // → ziplist
-     * $redisCluster->object("refcount", "l"); // → 1
-     * $redisCluster->object("idletime", "l"); // → 400 (in seconds, with a precision of 10 seconds).
-     * 
- */ - public function object($string = '', $key = '') { } - - /** - * Subscribe to channels. Warning: this function will probably change in the future. - * - * @param array $channels an array of channels to subscribe to - * @param string|array $callback either a string or an array($instance, 'method_name'). - * The callback function receives 3 parameters: the redis instance, the channel - * name, and the message. - * - * @return mixed Any non-null return value in the callback will be returned to the caller. - * @link https://redis.io/commands/subscribe - * @example - *
-     * function f($redisCluster, $chan, $msg) {
-     *  switch($chan) {
-     *      case 'chan-1':
-     *          ...
-     *          break;
-     *
-     *      case 'chan-2':
-     *                     ...
-     *          break;
-     *
-     *      case 'chan-2':
-     *          ...
-     *          break;
-     *      }
-     * }
-     *
-     * $redisCluster->subscribe(array('chan-1', 'chan-2', 'chan-3'), 'f'); // subscribe to 3 chans
-     * 
- */ - public function subscribe($channels, $callback) { } - - /** - * Subscribe to channels by pattern - * - * @param array $patterns The number of elements removed from the set. - * @param string|array $callback Either a string or an array with an object and method. - * The callback will get four arguments ($redis, $pattern, $channel, $message) - * - * @return mixed Any non-null return value in the callback will be returned to the caller. - * - * @link https://redis.io/commands/psubscribe - * @example - *
-     * function psubscribe($redisCluster, $pattern, $chan, $msg) {
-     *  echo "Pattern: $pattern\n";
-     *  echo "Channel: $chan\n";
-     *  echo "Payload: $msg\n";
-     * }
-     * 
- */ - public function psubscribe($patterns, $callback) { } - - /** - * Unsubscribes the client from the given channels, or from all of them if none is given. - * - * @param $channels - * @param $callback - */ - public function unSubscribe($channels, $callback) { } - - /** - * Unsubscribes the client from the given patterns, or from all of them if none is given. - * - * @param $channels - * @param $callback - */ - public function punSubscribe($channels, $callback) { } - - /** - * Evaluate a LUA script serverside, from the SHA1 hash of the script instead of the script itself. - * In order to run this command Redis will have to have already loaded the script, either by running it or via - * the SCRIPT LOAD command. - * - * @param string $scriptSha - * @param array $args - * @param int $numKeys - * - * @return mixed @see eval() - * @see eval() - * @link https://redis.io/commands/evalsha - * @example - *
-     * $script = 'return 1';
-     * $sha = $redisCluster->script('load', $script);
-     * $redisCluster->evalSha($sha); // Returns 1
-     * 
- */ - public function evalSha($scriptSha, $args = array(), $numKeys = 0) { } - - /** - * Scan the keyspace for keys. - * - * @param int &$iterator Iterator, initialized to NULL. - * @param string|array $node Node identified by key or host/port array - * @param string $pattern Pattern to match. - * @param int $count Count of keys per iteration (only a suggestion to Redis). - * - * @return array|false This function will return an array of keys or FALSE if there are no more keys. - * @link https://redis.io/commands/scan - * @example - *
-     * $iterator = null;
-     * while($keys = $redisCluster->scan($iterator)) {
-     *     foreach($keys as $key) {
-     *         echo $key . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function scan(&$iterator, $node, $pattern = null, $count = 0) { } - - /** - * Scan a set for members. - * - * @param string $key The set to search. - * @param int &$iterator LONG (reference) to the iterator as we go. - * @param null $pattern String, optional pattern to match against. - * @param int $count How many members to return at a time (Redis might return a different amount). - * - * @return array|false PHPRedis will return an array of keys or FALSE when we're done iterating. - * @link https://redis.io/commands/sscan - * @example - *
-     * $iterator = null;
-     * while ($members = $redisCluster->sScan('set', $iterator)) {
-     *     foreach ($members as $member) {
-     *         echo $member . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function sScan($key, &$iterator, $pattern = null, $count = 0) { } - - /** - * Scan a sorted set for members, with optional pattern and count. - * - * @param string $key String, the set to scan. - * @param int &$iterator Long (reference), initialized to NULL. - * @param string $pattern String (optional), the pattern to match. - * @param int $count How many keys to return per iteration (Redis might return a different number). - * - * @return array|false PHPRedis will return matching keys from Redis, or FALSE when iteration is complete. - * @link https://redis.io/commands/zscan - * @example - *
-     * $iterator = null;
-     * while ($members = $redis-zscan('zset', $iterator)) {
-     *     foreach ($members as $member => $score) {
-     *         echo $member . ' => ' . $score . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function zScan($key, &$iterator, $pattern = null, $count = 0) { } - - /** - * Scan a HASH value for members, with an optional pattern and count. - * - * @param string $key - * @param int &$iterator - * @param string $pattern Optional pattern to match against. - * @param int $count How many keys to return in a go (only a sugestion to Redis). - * - * @return array An array of members that match our pattern. - * @link https://redis.io/commands/hscan - * @example - *
-     * $iterator = null;
-     * while($elements = $redisCluster->hscan('hash', $iterator)) {
-     *    foreach($elements as $key => $value) {
-     *         echo $key . ' => ' . $value . PHP_EOL;
-     *     }
-     * }
-     * 
- */ - public function hScan($key, &$iterator, $pattern = null, $count = 0) { } - - /** - * Detect whether we're in ATOMIC/MULTI/PIPELINE mode. - * - * @return int Either RedisCluster::ATOMIC, RedisCluster::MULTI or RedisCluster::PIPELINE - * @example $redisCluster->getMode(); - */ - public function getMode() { } - - /** - * The last error message (if any) - * - * @return string|null A string with the last returned script based error message, or NULL if there is no error - * @example - *
-     * $redisCluster->eval('this-is-not-lua');
-     * $err = $redisCluster->getLastError();
-     * // "ERR Error compiling script (new function): user_script:1: '=' expected near '-'"
-     * 
- */ - public function getLastError() { } - - /** - * Clear the last error message - * - * @return bool true - * @example - *
-     * $redisCluster->set('x', 'a');
-     * $redisCluster->incr('x');
-     * $err = $redisCluster->getLastError();
-     * // "ERR value is not an integer or out of range"
-     * $redisCluster->clearLastError();
-     * $err = $redisCluster->getLastError();
-     * // NULL
-     * 
- */ - public function clearLastError() { } - - /** - * Get client option - * - * @param string $name parameter name - * - * @return int Parameter value. - * @example - * // return RedisCluster::SERIALIZER_NONE, RedisCluster::SERIALIZER_PHP, or RedisCluster::SERIALIZER_IGBINARY. - * $redisCluster->getOption(RedisCluster::OPT_SERIALIZER); - */ - public function getOption($name) { } - - /** - * Set client option. - * - * @param string $name parameter name - * @param string $value parameter value - * - * @return bool TRUE on success, FALSE on error. - * @example - *
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_NONE);        // don't serialize data
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_PHP);         // use built-in serialize/unserialize
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_IGBINARY);    // use igBinary serialize/unserialize
-     * $redisCluster->setOption(RedisCluster::OPT_PREFIX, 'myAppName:');                             // use custom prefix on all keys
-     * 
- */ - public function setOption($name, $value) { } - - /** - * A utility method to prefix the value with the prefix setting for phpredis. - * - * @param mixed $value The value you wish to prefix - * - * @return string If a prefix is set up, the value now prefixed. If there is no prefix, the value will be returned unchanged. - * @example - *
-     * $redisCluster->setOption(RedisCluster::OPT_PREFIX, 'my-prefix:');
-     * $redisCluster->_prefix('my-value'); // Will return 'my-prefix:my-value'
-     * 
- */ - public function _prefix($value) { } - - /** - * A utility method to serialize values manually. This method allows you to serialize a value with whatever - * serializer is configured, manually. This can be useful for serialization/unserialization of data going in - * and out of EVAL commands as phpredis can't automatically do this itself. Note that if no serializer is - * set, phpredis will change Array values to 'Array', and Objects to 'Object'. - * - * @param mixed $value The value to be serialized. - * - * @return mixed - * @example - *
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_NONE);
-     * $redisCluster->_serialize("foo"); // returns "foo"
-     * $redisCluster->_serialize(Array()); // Returns "Array"
-     * $redisCluster->_serialize(new stdClass()); // Returns "Object"
-     *
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_PHP);
-     * $redisCluster->_serialize("foo"); // Returns 's:3:"foo";'
-     * 
- */ - public function _serialize($value) { } - - /** - * A utility method to unserialize data with whatever serializer is set up. If there is no serializer set, the - * value will be returned unchanged. If there is a serializer set up, and the data passed in is malformed, an - * exception will be thrown. This can be useful if phpredis is serializing values, and you return something from - * redis in a LUA script that is serialized. - * - * @param string $value The value to be unserialized - * - * @return mixed - * @example - *
-     * $redisCluster->setOption(RedisCluster::OPT_SERIALIZER, RedisCluster::SERIALIZER_PHP);
-     * $redisCluster->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)
-     * 
- */ - public function _unserialize($value) { } - - /** - * Return all redis master nodes - * - * @return array - * @example - *
-     * $redisCluster->_masters(); // Will return [[0=>'127.0.0.1','6379'],[0=>'127.0.0.1','6380']]
-     * 
- */ - public function _masters() { } - - /** - * Enter and exit transactional mode. - * - * @param int $mode RedisCluster::MULTI|RedisCluster::PIPELINE - * Defaults to RedisCluster::MULTI. - * A RedisCluster::MULTI block of commands runs as a single transaction; - * a RedisCluster::PIPELINE block is simply transmitted faster to the server, but without any guarantee - * of atomicity. discard cancels a transaction. - * - * @return Redis returns the Redis instance and enters multi-mode. - * Once in multi-mode, all subsequent method calls return the same object until exec() is called. - * @link https://redis.io/commands/multi - * @example - *
-     * $ret = $redisCluster->multi()
-     *      ->set('key1', 'val1')
-     *      ->get('key1')
-     *      ->set('key2', 'val2')
-     *      ->get('key2')
-     *      ->exec();
-     *
-     * //$ret == array (
-     * //    0 => TRUE,
-     * //    1 => 'val1',
-     * //    2 => TRUE,
-     * //    3 => 'val2');
-     * 
- */ - public function multi($mode = RedisCluster::MULTI) { } - - /** - * @see multi() - * @return void|array - * @link https://redis.io/commands/exec - */ - public function exec() { } - - /** - * @see multi() - * @link https://redis.io/commands/discard - */ - public function discard() { } - - /** - * Watches a key for modifications by another client. If the key is modified between WATCH and EXEC, - * the MULTI/EXEC transaction will fail (return FALSE). unwatch cancels all the watching of all keys by this client. - * - * @param string|array $key : a list of keys - * - * @return void - * @link https://redis.io/commands/watch - * @example - *
-     * $redisCluster->watch('x');
-     * // long code here during the execution of which other clients could well modify `x`
-     * $ret = $redisCluster->multi()
-     *          ->incr('x')
-     *          ->exec();
-     * // $ret = FALSE if x has been modified between the call to WATCH and the call to EXEC.
-     * 
- */ - public function watch($key) { } - - /** - * @see watch() - * @link https://redis.io/commands/unwatch - */ - public function unwatch() { } - - /** - * Performs a synchronous save at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return bool TRUE in case of success, FALSE in case of failure. - * If a save is already running, this command will fail and return FALSE. - * @link https://redis.io/commands/save - * @example - * $redisCluster->save('x'); //key - * $redisCluster->save(['127.0.0.1',6379]); //[host,port] - */ - public function save($nodeParams) { } - - /** - * Performs a background save at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return bool TRUE in case of success, FALSE in case of failure. - * If a save is already running, this command will fail and return FALSE. - * @link https://redis.io/commands/bgsave - */ - public function bgsave($nodeParams) { } - - /** - * Removes all entries from the current database at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return bool Always TRUE. - * @link https://redis.io/commands/flushdb - */ - public function flushDB($nodeParams) { } - - /** - * Removes all entries from all databases at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return bool Always TRUE. - * @link https://redis.io/commands/flushall - */ - public function flushAll($nodeParams) { } - - /** - * Returns the current database's size at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return int DB size, in number of keys. - * @link https://redis.io/commands/dbsize - * @example - *
-     * $count = $redisCluster->dbSize('x');
-     * echo "Redis has $count keys\n";
-     * 
- */ - public function dbSize($nodeParams) { } - - /** - * Starts the background rewrite of AOF (Append-Only File) at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return bool TRUE in case of success, FALSE in case of failure. - * @link https://redis.io/commands/bgrewriteaof - * @example $redisCluster->bgrewriteaof('x'); - */ - public function bgrewriteaof($nodeParams) { } - - /** - * Returns the timestamp of the last disk save at a specific node. - * - * @param string|array $nodeParams key or [host,port] - * - * @return int timestamp. - * @link https://redis.io/commands/lastsave - * @example $redisCluster->lastSave('x'); - */ - public function lastSave($nodeParams) { } - - /** - * Returns an associative array of strings and integers - * - * @param string $option Optional. The option to provide redis. - * SERVER | CLIENTS | MEMORY | PERSISTENCE | STATS | REPLICATION | CPU | CLASTER | KEYSPACE - * | COMANDSTATS - * - * Returns an associative array of strings and integers, with the following keys: - * - redis_version - * - redis_git_sha1 - * - redis_git_dirty - * - redis_build_id - * - redis_mode - * - os - * - arch_bits - * - multiplexing_api - * - atomicvar_api - * - gcc_version - * - process_id - * - run_id - * - tcp_port - * - uptime_in_seconds - * - uptime_in_days - * - hz - * - lru_clock - * - executable - * - config_file - * - connected_clients - * - client_longest_output_list - * - client_biggest_input_buf - * - blocked_clients - * - used_memory - * - used_memory_human - * - used_memory_rss - * - used_memory_rss_human - * - used_memory_peak - * - used_memory_peak_human - * - used_memory_peak_perc - * - used_memory_peak - * - used_memory_overhead - * - used_memory_startup - * - used_memory_dataset - * - used_memory_dataset_perc - * - total_system_memory - * - total_system_memory_human - * - used_memory_lua - * - used_memory_lua_human - * - maxmemory - * - maxmemory_human - * - maxmemory_policy - * - mem_fragmentation_ratio - * - mem_allocator - * - active_defrag_running - * - lazyfree_pending_objects - * - mem_fragmentation_ratio - * - loading - * - rdb_changes_since_last_save - * - rdb_bgsave_in_progress - * - rdb_last_save_time - * - rdb_last_bgsave_status - * - rdb_last_bgsave_time_sec - * - rdb_current_bgsave_time_sec - * - rdb_last_cow_size - * - aof_enabled - * - aof_rewrite_in_progress - * - aof_rewrite_scheduled - * - aof_last_rewrite_time_sec - * - aof_current_rewrite_time_sec - * - aof_last_bgrewrite_status - * - aof_last_write_status - * - aof_last_cow_size - * - changes_since_last_save - * - aof_current_size - * - aof_base_size - * - aof_pending_rewrite - * - aof_buffer_length - * - aof_rewrite_buffer_length - * - aof_pending_bio_fsync - * - aof_delayed_fsync - * - loading_start_time - * - loading_total_bytes - * - loading_loaded_bytes - * - loading_loaded_perc - * - loading_eta_seconds - * - total_connections_received - * - total_commands_processed - * - instantaneous_ops_per_sec - * - total_net_input_bytes - * - total_net_output_bytes - * - instantaneous_input_kbps - * - instantaneous_output_kbps - * - rejected_connections - * - maxclients - * - sync_full - * - sync_partial_ok - * - sync_partial_err - * - expired_keys - * - evicted_keys - * - keyspace_hits - * - keyspace_misses - * - pubsub_channels - * - pubsub_patterns - * - latest_fork_usec - * - migrate_cached_sockets - * - slave_expires_tracked_keys - * - active_defrag_hits - * - active_defrag_misses - * - active_defrag_key_hits - * - active_defrag_key_misses - * - role - * - master_replid - * - master_replid2 - * - master_repl_offset - * - second_repl_offset - * - repl_backlog_active - * - repl_backlog_size - * - repl_backlog_first_byte_offset - * - repl_backlog_histlen - * - master_host - * - master_port - * - master_link_status - * - master_last_io_seconds_ago - * - master_sync_in_progress - * - slave_repl_offset - * - slave_priority - * - slave_read_only - * - master_sync_left_bytes - * - master_sync_last_io_seconds_ago - * - master_link_down_since_seconds - * - connected_slaves - * - min-slaves-to-write - * - min-replicas-to-write - * - min_slaves_good_slaves - * - used_cpu_sys - * - used_cpu_user - * - used_cpu_sys_children - * - used_cpu_user_children - * - cluster_enabled - * - * @link https://redis.io/commands/info - * @return array - * @example - *
-     * $redisCluster->info();
-     *
-     * or
-     *
-     * $redisCluster->info("COMMANDSTATS"); //Information on the commands that have been run (>=2.6 only)
-     * $redisCluster->info("CPU"); // just CPU information from Redis INFO
-     * 
- */ - public function info($option = null) { } - - /** - * @since redis >= 2.8.12. - * Returns the role of the instance in the context of replication - * - * @param string|array $nodeParams key or [host,port] - * - * @return array - * @link https://redis.io/commands/role - * @example - *
-     * $redisCluster->role(['127.0.0.1',6379]);
-     * // [ 0=>'master',1 => 3129659, 2 => [ ['127.0.0.1','9001','3129242'], ['127.0.0.1','9002','3129543'] ] ]
-     * 
- */ - public function role($nodeParams) { } - - /** - * Returns a random key at the specified node - * - * @param string|array $nodeParams key or [host,port] - * - * @return string an existing key in redis. - * @link https://redis.io/commands/randomkey - * @example - *
-     * $key = $redisCluster->randomKey('x');
-     * $surprise = $redisCluster->get($key);  // who knows what's in there.
-     * 
- */ - public function randomKey($nodeParams) { } - - /** - * Return the specified node server time. - * - * @param string|array $nodeParams key or [host,port] - * - * @return array If successfully, the time will come back as an associative array with element zero being the - * unix timestamp, and element one being microseconds. - * @link https://redis.io/commands/time - * @example - *
-     * var_dump( $redisCluster->time('x') );
-     * //// Output:
-     * //
-     * // array(2) {
-     * //   [0] => string(10) "1342364352"
-     * //   [1] => string(6) "253002"
-     * // }
-     * 
- */ - public function time($nodeParams) { } - - /** - * Check the specified node status - * - * @param string|array $nodeParams key or [host,port] - * - * @return string STRING: +PONG on success. Throws a RedisException object on connectivity error, as described - * above. - * @link https://redis.io/commands/ping - */ - public function ping($nodeParams) { } - - /** - * Returns message. - * - * @param string|array $nodeParams key or [host,port] - * @param string $msg - * - * @return mixed - */ - public function echo ($nodeParams, $msg) { } - - /** - * Returns Array reply of details about all Redis Cluster commands. - * - * @return mixed array | bool - */ - public function command() { } - - /** - * Send arbitrary things to the redis server at the specified node - * - * @param string|array $nodeParams key or [host,port] - * @param string $command Required command to send to the server. - * @param mixed $arguments Optional variable amount of arguments to send to the server. - * - * @return mixed - */ - public function rawCommand($nodeParams, $command, $arguments) { } - - /** - * @since redis >= 3.0 - * Executes cluster command - * - * @param string|array $nodeParams key or [host,port] - * @param string $command Required command to send to the server. - * @param mixed $arguments Optional variable amount of arguments to send to the server. - * - * @return mixed - * @link https://redis.io/commands#cluster - * @example - *
-     * $redisCluster->cluster(['127.0.0.1',6379],'INFO');
-     * 
- */ - public function cluster($nodeParams, $command, $arguments) { } - - /** - * Allows you to get information of the cluster client - * - * @param string|array $nodeParams key or [host,port] - * @param string $subCmd can be: 'LIST', 'KILL', 'GETNAME', or 'SETNAME' - * @param string $args optional arguments - */ - public function client($nodeParams, $subCmd, $args) { } - - /** - * Get or Set the redis config keys. - * - * @param string|array $nodeParams key or [host,port] - * @param string $operation either `GET` or `SET` - * @param string $key for `SET`, glob-pattern for `GET`. See https://redis.io/commands/config-get for examples. - * @param string $value optional string (only for `SET`) - * - * @return array Associative array for `GET`, key -> value - * @link https://redis.io/commands/config-get - * @link https://redis.io/commands/config-set - * @example - *
-     * $redisCluster->config(['127.0.0.1',6379], "GET", "*max-*-entries*");
-     * $redisCluster->config(['127.0.0.1',6379], "SET", "dir", "/var/run/redis/dumps/");
-     * 
- */ - public function config($nodeParams, $operation, $key, $value) { } - - /** - * A command allowing you to get information on the Redis pub/sub system. - * - * @param string|array $nodeParams key or [host,port] - * - * @param string $keyword String, which can be: "channels", "numsub", or "numpat" - * @param string|array $argument Optional, variant. - * For the "channels" subcommand, you can pass a string pattern. - * For "numsub" an array of channel names - * - * @return array|int Either an integer or an array. - * - channels Returns an array where the members are the matching channels. - * - numsub Returns a key/value array where the keys are channel names and - * values are their counts. - * - numpat Integer return containing the number active pattern subscriptions. - * @link https://redis.io/commands/pubsub - * @example - *
-     * $redisCluster->pubsub(['127.0.0.1',6379], 'channels'); // All channels
-     * $redisCluster->pubsub(['127.0.0.1',6379], 'channels', '*pattern*'); // Just channels matching your pattern
-     * $redisCluster->pubsub(['127.0.0.1',6379], 'numsub', array('chan1', 'chan2')); // Get subscriber counts for
-     * 'chan1' and 'chan2'
-     * $redisCluster->pubsub(['127.0.0.1',6379], 'numpat'); // Get the number of pattern subscribers
-     * 
- */ - public function pubsub($nodeParams, $keyword, $argument) { } - - /** - * Execute the Redis SCRIPT command to perform various operations on the scripting subsystem. - * - * @param string|array $nodeParams key or [host,port] - * @param string $command load | flush | kill | exists - * @param string $script - * - * @return mixed - * @link https://redis.io/commands/script-load - * @link https://redis.io/commands/script-kill - * @link https://redis.io/commands/script-flush - * @link https://redis.io/commands/script-exists - * @example - *
-     * $redisCluster->script(['127.0.0.1',6379], 'load', $script);
-     * $redisCluster->script(['127.0.0.1',6379], 'flush');
-     * $redisCluster->script(['127.0.0.1',6379], 'kill');
-     * $redisCluster->script(['127.0.0.1',6379], 'exists', $script1, [$script2, $script3, ...]);
-     * 
- * - * SCRIPT LOAD will return the SHA1 hash of the passed script on success, and FALSE on failure. - * SCRIPT FLUSH should always return TRUE - * SCRIPT KILL will return true if a script was able to be killed and false if not - * SCRIPT EXISTS will return an array with TRUE or FALSE for each passed script - */ - public function script($nodeParams, $command, $script) { } - - /** - * This function is used in order to read and reset the Redis slow queries log. - * - * @param string|array $nodeParams key or [host,port] - * @param string $command - * @param mixed $argument - * - * @link https://redis.io/commands/slowlog - * @example - *
-     * $redisCluster->slowLog(['127.0.0.1',6379],'get','2');
-     * 
- */ - public function slowLog($nodeParams, $command, $argument) { } - - /** - * Add one or more geospatial items in the geospatial index represented using a sorted set - * - * @param string $key - * @param float $longitude - * @param float $latitude - * @param string $member - * - * @link https://redis.io/commands/geoadd - * @example - *
-     * $redisCluster->geoAdd('Sicily', 13.361389, 38.115556, 'Palermo'); // int(1)
-     * $redisCluster->geoAdd('Sicily', 15.087269, 37.502669, "Catania"); // int(1)
-     * 
- */ - public function geoAdd($key, $longitude, $latitude, $member) { } - - /** - * Returns members of a geospatial index as standard geohash strings - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string $memberN - * - * @example - *
-     * $redisCluster->geoAdd('Sicily', 13.361389, 38.115556, 'Palermo'); // int(1)
-     * $redisCluster->geoAdd('Sicily', 15.087269, 37.502669, "Catania"); // int(1)
-     * $redisCluster->geohash('Sicily','Palermo','Catania');//['sqc8b49rny0','sqdtr74hyu0']
-     * 
- */ - public function geohash($key, $member1, $member2 = null, $memberN = null) { } - - /** - * Returns longitude and latitude of members of a geospatial index - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string $memberN - * @example - *
-     * $redisCluster->geoAdd('Sicily', 15.087269, 37.502669, "Catania"); // int(1)
-     * $redisCluster->geopos('Sicily','Palermo');//[['13.36138933897018433','38.11555639549629859']]
-     * 
- */ - public function geopos($key, $member1, $member2 = null, $memberN = null) { } - - /** - * - * Returns the distance between two members of a geospatial index - * - * @param string $key - * @param string $member1 - * @param string $member2 - * @param string $unit The unit must be one of the following, and defaults to meters: - * m for meters. - * km for kilometers. - * mi for miles. - * ft for feet. - * - * @link https://redis.io/commands/geoadd - * @example - *
-     * $redisCluster->geoAdd('Sicily', 13.361389, 38.115556, 'Palermo'); // int(1)
-     * $redisCluster->geoAdd('Sicily', 15.087269, 37.502669, "Catania"); // int(1)
-     * $redisCluster->geoDist('Sicily', 'Palermo' ,'Catania'); // float(166274.1516)
-     * $redisCluster->geoDist('Sicily', 'Palermo','Catania', 'km'); // float(166.2742)
-     * 
- */ - public function geoDist($key, $member1, $member2, $unit = 'm') { } - - /** - * Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point - * - * @param string $key - * @param float $longitude - * @param float $latitude - * @param float $radius - * @param string $radiusUnit String can be: "m" for meters; "km" for kilometers , "mi" for miles, or "ft" for feet. - * @param array $options - * - * @link https://redis.io/commands/georadius - * @example - *
-     * $redisCluster->del('Sicily');
-     * $redisCluster->geoAdd('Sicily', 12.361389, 35.115556, 'Palermo'); // int(1)
-     * $redisCluster->geoAdd('Sicily', 15.087269, 37.502669, "Catania"); // int(1)
-     * $redisCluster->geoAdd('Sicily', 13.3585, 35.330022, "Agrigento"); // int(1)
-     *
-     * var_dump( $redisCluster->geoRadius('Sicily',13.3585, 35.330022, 300, 'km', ['WITHDIST' ,'DESC']) );
-     *
-     * array(3) {
-     *    [0]=>
-     *   array(2) {
-     *        [0]=>
-     *     string(7) "Catania"
-     *        [1]=>
-     *     string(8) "286.9362"
-     *   }
-     *   [1]=>
-     *   array(2) {
-     *        [0]=>
-     *     string(7) "Palermo"
-     *        [1]=>
-     *     string(7) "93.6874"
-     *   }
-     *   [2]=>
-     *   array(2) {
-     *        [0]=>
-     *     string(9) "Agrigento"
-     *        [1]=>
-     *     string(6) "0.0002"
-     *   }
-     * }
-     * var_dump( $redisCluster->geoRadiusByMember('Sicily','Agrigento', 100, 'km', ['WITHDIST' ,'DESC']) );
-     *
-     * * array(2) {
-     *    [0]=>
-     *   array(2) {
-     *        [0]=>
-     *     string(7) "Palermo"
-     *        [1]=>
-     *     string(7) "93.6872"
-     *   }
-     *   [1]=>
-     *   array(2) {
-     *        [0]=>
-     *     string(9) "Agrigento"
-     *        [1]=>
-     *     string(6) "0.0000"
-     *   }
-     * }
-     *
-     * 
-     */
-    public function geoRadius($key, $longitude, $latitude, $radius, $radiusUnit, array $options) { }
-
-    /**
-     * Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member
-     *
-     * @see geoRadius
-     *
-     * @param string $key
-     * @param string $member
-     * @param float  $radius
-     * @param string $radiusUnit
-     * @param array  $options
-     */
-    public function geoRadiusByMember($key, $member, $radius, $radiusUnit, array $options) { }
-
-}
-
-class RedisClusterException extends Exception {}
--- a/build/stubs/sftp.php
+++ /dev/null
@@ -1,83 +0,0 @@
-
-	 * The imported style sheet as a DOMDocument or
-	 * SimpleXMLElement object.
-	 * 

- * @return bool TRUE on success or FALSE on failure. - */ - public function importStylesheet ($stylesheet) {} - - /** - * Transform to a DOMDocument - * @link https://php.net/manual/en/xsltprocessor.transformtodoc.php - * @param DOMNode $doc

- * The node to be transformed. - *

- * @return DOMDocument|false The resulting DOMDocument or FALSE on error. - */ - public function transformToDoc (DOMNode $doc) {} - - /** - * Transform to URI - * @link https://php.net/manual/en/xsltprocessor.transformtouri.php - * @param DOMDocument|SimpleXMLElement $doc

- * The document to transform. - *

- * @param string $uri

- * The target URI for the transformation. - *

- * @return int|false the number of bytes written or FALSE if an error occurred. - */ - public function transformToUri ($doc, $uri) {} - - /** - * Transform to XML - * @link https://php.net/manual/en/xsltprocessor.transformtoxml.php - * @param DOMDocument|SimpleXMLElement $doc

- * The transformed document. - *

- * @return string|false The result of the transformation as a string or FALSE on error. - */ - public function transformToXml ($doc) {} - - /** - * Set value for a parameter - * @link https://php.net/manual/en/xsltprocessor.setparameter.php - * @param string $namespace

- * The namespace URI of the XSLT parameter. - *

- * @param string $name

- * The local name of the XSLT parameter. - *

- * @param string $value

- * The new value of the XSLT parameter. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setParameter ($namespace, $name, $value) {} - - /** - * Get value of a parameter - * @link https://php.net/manual/en/xsltprocessor.getparameter.php - * @param string $namespaceURI

- * The namespace URI of the XSLT parameter. - *

- * @param string $localName

- * The local name of the XSLT parameter. - *

- * @return string|false The value of the parameter (as a string), or FALSE if it's not set. - */ - public function getParameter ($namespaceURI, $localName) {} - - /** - * Remove parameter - * @link https://php.net/manual/en/xsltprocessor.removeparameter.php - * @param string $namespaceURI

- * The namespace URI of the XSLT parameter. - *

- * @param string $localName

- * The local name of the XSLT parameter. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function removeParameter ($namespaceURI, $localName) {} - - /** - * Determine if PHP has EXSLT support - * @link https://php.net/manual/en/xsltprocessor.hasexsltsupport.php - * @return bool TRUE on success or FALSE on failure. - * @since 5.0.4 - */ - public function hasExsltSupport () {} - - /** - * Enables the ability to use PHP functions as XSLT functions - * @link https://php.net/manual/en/xsltprocessor.registerphpfunctions.php - * @param mixed $restrict [optional]

- * Use this parameter to only allow certain functions to be called from - * XSLT. - *

- *

- * This parameter can be either a string (a function name) or an array of - * functions. - *

- * @return void No value is returned. - * @since 5.0.4 - */ - public function registerPHPFunctions ($restrict = null) {} - - /** - * Sets profiling output file - * @link https://php.net/manual/en/xsltprocessor.setprofiling.php - * @param string $filename

- * Path to the file to dump profiling information. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setProfiling ($filename) {} - - /** - * Set security preferences - * @link https://php.net/manual/en/xsltprocessor.setsecurityprefs.php - * @param int $securityPrefs - * @return int - * @since 5.4 - */ - public function setSecurityPrefs ($securityPrefs) {} - - /** - * Get security preferences - * @link https://php.net/manual/en/xsltprocessor.getsecurityprefs.php - * @return int - * @since 5.4 - */ - public function getSecurityPrefs () {} - -} -define ('XSL_CLONE_AUTO', 0); -define ('XSL_CLONE_NEVER', -1); -define ('XSL_CLONE_ALWAYS', 1); - -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_NONE', 0); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_READ_FILE', 2); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_WRITE_FILE', 4); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_CREATE_DIRECTORY', 8); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_READ_NETWORK', 16); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_WRITE_NETWORK', 32); -/** @link https://php.net/manual/en/xsl.constants.php */ -define ('XSL_SECPREF_DEFAULT', 44); - -/** - * libxslt version like 10117. Available as of PHP 5.1.2. - * @link https://php.net/manual/en/xsl.constants.php - */ -define ('LIBXSLT_VERSION', 10128); - -/** - * libxslt version like 1.1.17. Available as of PHP 5.1.2. - * @link https://php.net/manual/en/xsl.constants.php - */ -define ('LIBXSLT_DOTTED_VERSION', "1.1.28"); - -/** - * libexslt version like 813. Available as of PHP 5.1.2. - * @link https://php.net/manual/en/xsl.constants.php - */ -define ('LIBEXSLT_VERSION', 817); - -/** - * libexslt version like 1.1.17. Available as of PHP 5.1.2. - * @link https://php.net/manual/en/xsl.constants.php - */ -define ('LIBEXSLT_DOTTED_VERSION', "1.1.28"); - -// End of xsl v.0.1 -?> --- a/build/stubs/zip.php +++ /dev/null @@ -1,1482 +0,0 @@ - - * Open a ZIP file archive - * - * @link https://php.net/manual/en/ziparchive.open.php - * - * @param string $filename

- * The file name of the ZIP archive to open. - *

- * @param int $flags [optional]

- * The mode to use to open the archive. - *

- *

- * ZipArchive::OVERWRITE - *

- * - * @return mixed Error codes - *

- * Returns TRUE on success or the error code. - *

- *

- * ZipArchive::ER_EXISTS - *

- *

- * File already exists. - *

- *

- * ZipArchive::ER_INCONS - *

- *

- * Zip archive inconsistent. - *

- *

- * ZipArchive::ER_INVAL - *

- *

- * Invalid argument. - *

- *

- * ZipArchive::ER_MEMORY - *

- *

- * Malloc failure. - *

- *

- * ZipArchive::ER_NOENT - *

- *

- * No such file. - *

- *

- * ZipArchive::ER_NOZIP - *

- *

- * Not a zip archive. - *

- *

- * ZipArchive::ER_OPEN - *

- *

- * Can't open file. - *

- *

- * ZipArchive::ER_READ - *

- *

- * Read error. - *

- *

- * ZipArchive::ER_SEEK - *

- *

- * Seek error. - *

- */ - public function open( - $filename, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Close the active archive (opened or newly created) - * @link https://php.net/manual/en/ziparchive.close.php - * @return bool TRUE on success or FALSE on failure. - */ - public function close() {} - - /** - * (PHP 7 >= 7.2.0, PECL zip >= 1.15.0)
- * Counts the number of files in the archive. - * @link https://www.php.net/manual/en/ziparchive.count.php - * @return int - * @since 7.2 - */ - public function count() {} - - /** - * Returns the status error message, system and/or zip messages - * @link https://php.net/manual/en/ziparchive.getstatusstring.php - * @return string|false a string with the status message on success or FALSE on failure. - * @since 5.2.7 - */ - public function getStatusString() {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.8.0)
- * Add a new directory - * @link https://php.net/manual/en/ziparchive.addemptydir.php - * @param string $dirname

- * The directory to add. - *

- * @param int $flags [optional] Set how to manage name encoding (ZipArchive::FL_ENC_*) and entry replacement (ZipArchive::FL_OVERWRITE) - * @return bool TRUE on success or FALSE on failure. - */ - public function addEmptyDir( - string $dirname, - int $flags = 0 - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Add a file to a ZIP archive using its contents - * @link https://php.net/manual/en/ziparchive.addfromstring.php - * @param string $name

- * The name of the entry to create. - *

- * @param string $content

- * The contents to use to create the entry. It is used in a binary - * safe mode. - *

- * @param int $flags [optional] Set how to manage name encoding (ZipArchive::FL_ENC_*) and entry replacement (ZipArchive::FL_OVERWRITE) - * @return bool TRUE on success or FALSE on failure. - */ - public function addFromString( - $name, - $content, - $flags = 8192 - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Adds a file to a ZIP archive from the given path - * @link https://php.net/manual/en/ziparchive.addfile.php - * @param string $filepath

- * The path to the file to add. - *

- * @param string $entryname [optional]

- * If supplied, this is the local name inside the ZIP archive that will override the filename. - *

- * @param int $start [optional]

- * This parameter is not used but is required to extend ZipArchive. - *

- * @param int $length [optional]

- * This parameter is not used but is required to extend ZipArchive. - *

- * @param int $flags [optional] Set how to manage name encoding (ZipArchive::FL_ENC_*) and entry replacement (ZipArchive::FL_OVERWRITE) - * @return bool TRUE on success or FALSE on failure. - */ - public function addFile( - $filepath, - $entryname = null, - $start = 0, - $length = 0, - $flags = 8192 - ) {} - - /** - * (PHP 5 >= 5.3.0, PECL zip >= 1.9.0)
- * Add files from a directory by glob pattern - * @link https://php.net/manual/en/ziparchive.addglob.php - * @param string $pattern

- * A glob pattern against which files will be matched. - *

- * @param int $flags [optional]

- * A bit mask of glob() flags. - *

- * @param array $options [optional]

- * An associative array of options. Available options are: - *

- *

- * "add_path" - *

- *

- * Prefix to prepend when translating to the local path of the file within - * the archive. This is applied after any remove operations defined by the - * "remove_path" or "remove_all_path" - * options. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function addGlob( - $pattern, - $flags = 0, - array $options = [] - ) {} - - /** - * (PHP 5 >= 5.3.0, PECL zip >= 1.9.0)
- * Add files from a directory by PCRE pattern - * @link https://php.net/manual/en/ziparchive.addpattern.php - * @param string $pattern

- * A PCRE pattern against which files will be matched. - *

- * @param string $path [optional]

- * The directory that will be scanned. Defaults to the current working directory. - *

- * @param array $options [optional]

- * An associative array of options accepted by ZipArchive::addGlob. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function addPattern( - $pattern, - $path = '.', - array $options = [] - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Renames an entry defined by its index - * @link https://php.net/manual/en/ziparchive.renameindex.php - * @param int $index

- * Index of the entry to rename. - *

- * @param string $new_name

- * New name. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function renameIndex( - $index, - $new_name - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Renames an entry defined by its name - * @link https://php.net/manual/en/ziparchive.renamename.php - * @param string $name

- * Name of the entry to rename. - *

- * @param string $new_name

- * New name. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function renameName( - $name, - $new_name - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.4.0)
- * Set the comment of a ZIP archive - * @link https://php.net/manual/en/ziparchive.setarchivecomment.php - * @param string $comment

- * The contents of the comment. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setArchiveComment( $comment) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Returns the Zip archive comment - * @link https://php.net/manual/en/ziparchive.getarchivecomment.php - * @param int $flags [optional]

- * If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged - * comment is returned. - *

- * @return string|false the Zip archive comment or FALSE on failure. - */ - public function getArchiveComment( $flags = null) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.4.0)
- * Set the comment of an entry defined by its index - * @link https://php.net/manual/en/ziparchive.setcommentindex.php - * @param int $index

- * Index of the entry. - *

- * @param string $comment

- * The contents of the comment. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setCommentIndex( - $index, - $comment - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.4.0)
- * Set the comment of an entry defined by its name - * @link https://php.net/manual/en/ziparchive.setcommentname.php - * @param string $name

- * Name of the entry. - *

- * @param string $comment

- * The contents of the comment. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function setCommentName( - $name, - $comment - ) {} - - /** - * Set the compression method of an entry defined by its index - * @link https://php.net/manual/en/ziparchive.setcompressionindex.php - * @param int $index Index of the entry. - * @param int $method The compression method. Either ZipArchive::CM_DEFAULT, ZipArchive::CM_STORE or ZipArchive::CM_DEFLATE. - * @param int $compflags [optional] Compression flags. Currently unused. - * @return bool Returns TRUE on success or FALSE on failure. - * @since 7.0 - */ - public function setCompressionIndex(int $index, int $method, int $compflags = 0) {} - - /** - * Set the compression method of an entry defined by its name - * https://secure.php.net/manual/en/ziparchive.setcompressionname.php - * @param string $name Name of the entry. - * @param int $method The compression method. Either ZipArchive::CM_DEFAULT, ZipArchive::CM_STORE or ZipArchive::CM_DEFLATE. - * @param int $compflags [optional] Compression flags. Currently unused. - * @return bool Returns TRUE on success or FALSE on failure. - * @since 7.0 - */ - public function setCompressionName(string $name, int $method, int $compflags = 0) {} - - /** - * Set the encryption method of an entry defined by its index - * @link https://php.net/manual/en/ziparchive.setencryptionindex.php - * @param int $index Index of the entry. - * @param int $method The encryption method defined by one of the ZipArchive::EM_ constants. - * @param string|null $password [optional] Optional password, default used when missing. - * @return bool Returns TRUE on success or FALSE on failure. - * @since 7.2 - */ - public function setEncryptionIndex(int $index, int $method, ?string $password = null) {} - - /** - * Set the encryption method of an entry defined by its name - * @link https://php.net/manual/en/ziparchive.setencryptionname.php - * @param string $name Name of the entry. - * @param int $method The encryption method defined by one of the ZipArchive::EM_ constants. - * @param string|null $password [optional] Optional password, default used when missing. - * @return bool Returns TRUE on success or FALSE on failure. - * @since 7.2 - */ - public function setEncryptionName(string $name, int $method, ?string $password = null) {} - - /** - * (PHP 5 >= 5.6.0, PECL zip >= 1.12.0)
- * @param string $password - * @return bool - */ - public function setPassword( $password) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.4.0)
- * Returns the comment of an entry using the entry index - * @link https://php.net/manual/en/ziparchive.getcommentindex.php - * @param int $index

- * Index of the entry - *

- * @param int $flags [optional]

- * If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged - * comment is returned. - *

- * @return string|false the comment on success or FALSE on failure. - */ - public function getCommentIndex( - $index, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.4.0)
- * Returns the comment of an entry using the entry name - * @link https://php.net/manual/en/ziparchive.getcommentname.php - * @param string $name

- * Name of the entry - *

- * @param int $flags [optional]

- * If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged - * comment is returned. - *

- * @return string|false the comment on success or FALSE on failure. - */ - public function getCommentName( - $name, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * delete an entry in the archive using its index - * @link https://php.net/manual/en/ziparchive.deleteindex.php - * @param int $index

- * Index of the entry to delete. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function deleteIndex( $index) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * delete an entry in the archive using its name - * @link https://php.net/manual/en/ziparchive.deletename.php - * @param string $name

- * Name of the entry to delete. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function deleteName( $name) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Get the details of an entry defined by its name. - * @link https://php.net/manual/en/ziparchive.statname.php - * @param string $name

- * Name of the entry - *

- * @param int $flags [optional]

- * The flags argument specifies how the name lookup should be done. - * Also, ZipArchive::FL_UNCHANGED may be ORed to it to request - * information about the original file in the archive, - * ignoring any changes made. - * ZipArchive::FL_NOCASE - *

- * @return array|false an array containing the entry details or FALSE on failure. - */ - public function statName( - $name, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Get the details of an entry defined by its index. - * @link https://php.net/manual/en/ziparchive.statindex.php - * @param int $index

- * Index of the entry - *

- * @param int $flags [optional]

- * ZipArchive::FL_UNCHANGED may be ORed to it to request - * information about the original file in the archive, - * ignoring any changes made. - *

- * @return array|false an array containing the entry details or FALSE on failure. - */ - public function statIndex( - $index, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Returns the index of the entry in the archive - * @link https://php.net/manual/en/ziparchive.locatename.php - * @param string $name

- * The name of the entry to look up - *

- * @param int $flags [optional]

- * The flags are specified by ORing the following values, - * or 0 for none of them. - * ZipArchive::FL_NOCASE - *

- * @return int|false the index of the entry on success or FALSE on failure. - */ - public function locateName( - $name, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Returns the name of an entry using its index - * @link https://php.net/manual/en/ziparchive.getnameindex.php - * @param int $index

- * Index of the entry. - *

- * @param int $flags [optional]

- * If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged - * name is returned. - *

- * @return string|false the name on success or FALSE on failure. - */ - public function getNameIndex( - $index, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Revert all global changes done in the archive. - * @link https://php.net/manual/en/ziparchive.unchangearchive.php - * @return bool TRUE on success or FALSE on failure. - */ - public function unchangeArchive() {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Undo all changes done in the archive - * @link https://php.net/manual/en/ziparchive.unchangeall.php - * @return bool TRUE on success or FALSE on failure. - */ - public function unchangeAll() {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Revert all changes done to an entry at the given index - * @link https://php.net/manual/en/ziparchive.unchangeindex.php - * @param int $index

- * Index of the entry. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function unchangeIndex( $index) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.5.0)
- * Revert all changes done to an entry with the given name. - * @link https://php.net/manual/en/ziparchive.unchangename.php - * @param string $name

- * Name of the entry. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function unchangeName( $name) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Extract the archive contents - * @link https://php.net/manual/en/ziparchive.extractto.php - * @param string $pathto

- * Location where to extract the files. - *

- * @param mixed $files [optional]

- * The entries to extract. It accepts either a single entry name or - * an array of names. - *

- * @return bool TRUE on success or FALSE on failure. - */ - public function extractTo( - $pathto, - $files = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Returns the entry contents using its name - * @link https://php.net/manual/en/ziparchive.getfromname.php - * @param string $name

- * Name of the entry - *

- * @param int $len [optional]

- * The length to be read from the entry. If 0, then the - * entire entry is read. - *

- * @param int $flags [optional]

- * The flags to use to open the archive. the following values may - * be ORed to it. - * ZipArchive::FL_UNCHANGED - *

- * @return string|false the contents of the entry on success or FALSE on failure. - */ - public function getFromName( - $name, - $len = 0, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.3.0)
- * Returns the entry contents using its index - * @link https://php.net/manual/en/ziparchive.getfromindex.php - * @param int $index

- * Index of the entry - *

- * @param int $len [optional]

- * The length to be read from the entry. If 0, then the - * entire entry is read. - *

- * @param int $flags [optional]

- * The flags to use to open the archive. the following values may - * be ORed to it. - *

- *

- * ZipArchive::FL_UNCHANGED - *

- * @return string|false the contents of the entry on success or FALSE on failure. - */ - public function getFromIndex( - $index, - $len = 0, - $flags = null - ) {} - - /** - * (PHP 5 >= 5.2.0, PECL zip >= 1.1.0)
- * Get a file handler to the entry defined by its name (read only). - * @link https://php.net/manual/en/ziparchive.getstream.php - * @param string $name

- * The name of the entry to use. - *

- * @return resource|false a file pointer (resource) on success or FALSE on failure. - */ - public function getStream( $name) {} - - /** - * Set the external attributes of an entry defined by its name - * @link https://www.php.net/manual/en/ziparchive.setexternalattributesname.php - * @param string $name Name of the entry - * @param int $opsys The operating system code defined by one of the ZipArchive::OPSYS_ constants. - * @param int $attr The external attributes. Value depends on operating system. - * @param int $flags [optional] Optional flags. Currently unused. - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function setExternalAttributesName( - $name, - $opsys, - $attr, - $flags = null - ) {} - - /** - * Retrieve the external attributes of an entry defined by its name - * @link https://www.php.net/manual/en/ziparchive.getexternalattributesname.php - * @param string $name Name of the entry - * @param int &$opsys On success, receive the operating system code defined by one of the ZipArchive::OPSYS_ constants. - * @param int &$attr On success, receive the external attributes. Value depends on operating system. - * @param int $flags [optional] If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged attributes are returned. - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function getExternalAttributesName( - $name, - &$opsys, - &$attr, - $flags = null - ) {} - - /** - * Set the external attributes of an entry defined by its index - * @link https://www.php.net/manual/en/ziparchive.setexternalattributesindex.php - * @param int $index Index of the entry. - * @param int $opsys The operating system code defined by one of the ZipArchive::OPSYS_ constants. - * @param int $attr The external attributes. Value depends on operating system. - * @param int $flags [optional] Optional flags. Currently unused. - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function setExternalAttributesIndex( - $index, - $opsys, - $attr, - $flags = null - ) {} - - /** - * Retrieve the external attributes of an entry defined by its index - * @link https://www.php.net/manual/en/ziparchive.getexternalattributesindex.php - * @param int $index Index of the entry. - * @param int &$opsys On success, receive the operating system code defined by one of the ZipArchive::OPSYS_ constants. - * @param int &$attr On success, receive the external attributes. Value depends on operating system. - * @param int $flags [optional] If flags is set to ZipArchive::FL_UNCHANGED, the original unchanged attributes are returned. - * @return bool Returns TRUE on success or FALSE on failure. - */ - public function getExternalAttributesIndex( - $index, - &$opsys, - &$attr, - $flags = null - ) {} - - public static function isEncryptionMethodSupported( - $method, - $enc = true - ) {} - - public static function isCompressionMethodSupported( - $method, - $enc = true - ) {} - - public function registerCancelCallback( $callback) {} - - public function registerProgressCallback( - $rate, - $callback - ) {} - - public function setMtimeName( - $name, - $timestamp, - $flags = null - ) {} - - public function setMtimeIndex( - $index, - $timestamp, - $flags = null - ) {} - - public function replaceFile( - $filepath, - $index, - $start = null, - $length = null, - $flags = null - ) {} -} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Open a ZIP file archive - * @link https://php.net/manual/en/function.zip-open.php - * @param string $filename

- * The file name of the ZIP archive to open. - *

- * @return resource|int|false a resource handle for later use with - * zip_read and zip_close - * or returns the number of error if filename does not - * exist or in case of other error. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_open(string $filename) {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Close a ZIP file archive - * @link https://php.net/manual/en/function.zip-close.php - * @param resource $zip

- * A ZIP file previously opened with zip_open. - *

- * @return void No value is returned. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_close($zip): void {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Read next entry in a ZIP file archive - * @link https://php.net/manual/en/function.zip-read.php - * @param resource $zip

- * A ZIP file previously opened with zip_open. - *

- * @return resource|false a directory entry resource for later use with the - * zip_entry_... functions, or FALSE if - * there are no more entries to read, or an error code if an error - * occurred. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_read($zip) {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Open a directory entry for reading - * @link https://php.net/manual/en/function.zip-entry-open.php - * @param resource $zip_dp

- * A valid resource handle returned by zip_open. - *

- * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @param string $mode [optional]

- * Any of the modes specified in the documentation of - * fopen. - *

- *

- * Currently, mode is ignored and is always - * "rb". This is due to the fact that zip support - * in PHP is read only access. - *

- * @return bool TRUE on success or FALSE on failure. - *

- * Unlike fopen and other similar functions, - * the return value of zip_entry_open only - * indicates the result of the operation and is not needed for - * reading or closing the directory entry. - *

- */ -function zip_entry_open($zip_dp, $zip_entry, string $mode = 'rb'): bool {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Close a directory entry - * @link https://php.net/manual/en/function.zip-entry-close.php - * @param resource $zip_entry

- * A directory entry previously opened zip_entry_open. - *

- * @return bool TRUE on success or FALSE on failure. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_close($zip_entry): bool {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Read from an open directory entry - * @link https://php.net/manual/en/function.zip-entry-read.php - * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @param int $len [optional]

- * The number of bytes to return. - *

- *

- * This should be the uncompressed length you wish to read. - *

- * @return string|false the data read, empty string on end of a file, or FALSE on error. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_read($zip_entry, int $len = 1024): string|false {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Retrieve the actual file size of a directory entry - * @link https://php.net/manual/en/function.zip-entry-filesize.php - * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @return int|false The size of the directory entry. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_filesize($zip_entry): int|false {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Retrieve the name of a directory entry - * @link https://php.net/manual/en/function.zip-entry-name.php - * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @return string|false The name of the directory entry. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_name($zip_entry): string|false {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Retrieve the compressed size of a directory entry - * @link https://php.net/manual/en/function.zip-entry-compressedsize.php - * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @return int|false The compressed size. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_compressedsize($zip_entry): int|false {} - -/** - * (PHP 4 >= 4.1.0, PHP 5 >= 5.2.0, PECL zip >= 1.0.0)
- * Retrieve the compression method of a directory entry - * @link https://php.net/manual/en/function.zip-entry-compressionmethod.php - * @param resource $zip_entry

- * A directory entry returned by zip_read. - *

- * @return string|false The compression method. - * @deprecated 8.0 Use {@link ZipArchive} instead. - */ -function zip_entry_compressionmethod($zip_entry): string|false {} - -// End of zip v.1.11.0 --- a/build/translation-checker.php +++ /dev/null @@ -1,123 +0,0 @@ - 'appstore-shipped-with-server', - 'dashboard' => 'dashboard-shipped-with-server', - 'encryption' => 'files_encryption', - 'settings' => 'settings-1', -]; - -// Next line only looks messed up, but it works. Don't touch it! -$rtlCharacters = [ - '\x{061C}', // ARABIC LETTER MARK - '\x{0623}', // ARABIC LETTER ALEF WITH HAMZA ABOVE - '\x{200E}', // LEFT-TO-RIGHT MARK - '\x{200F}', // RIGHT-TO-LEFT MARK - '\x{202A}', // LEFT-TO-RIGHT EMBEDDING - '\x{202B}', // RIGHT-TO-LEFT EMBEDDING - '\x{202C}', // POP DIRECTIONAL FORMATTING - '\x{202D}', // LEFT-TO-RIGHT OVERRIDE - '\x{202E}', // RIGHT-TO-LEFT OVERRIDE - '\x{2066}', // LEFT-TO-RIGHT ISOLATE - '\x{2067}', // RIGHT-TO-LEFT ISOLATE - '\x{2068}', // FIRST STRONG ISOLATE - '\x{2069}', // POP DIRECTIONAL ISOLATE - '\x{206C}', // INHIBIT ARABIC FORM SHAPING - '\x{206D}', // ACTIVATE ARABIC FORM SHAPING -]; - -$rtlLanguages = [ - 'ar', // Arabic - 'fa', // Persian - 'he', // Hebrew - 'ps', // Pashto, - 'ug', // 'Uyghurche / Uyghur - 'ur_PK', // Urdu -]; - -$valid = 0; -$errors = []; -$apps = new \DirectoryIterator(__DIR__ . '/../apps'); -foreach ($apps as $app) { - if ($app->isDot() || in_array($app->getBasename(), $untranslatedApps, true)) { - continue; - } - - $resourceName = $txConfigAppMap[$app->getBasename()] ?? $app->getBasename(); - if (!file_exists($app->getPathname() . '/l10n') || !str_contains($txConfig, '[o:nextcloud:p:nextcloud:r:' . $resourceName . ']')) { - $errors[] = $app->getBasename() . "\n" . ' App is not correctly configured for translation sync via transifex and also not marked as untranslated' . "\n"; - continue; - } - - $directories[] = $app->getPathname() . '/l10n'; -} - -foreach ($directories as $dir) { - if (!file_exists($dir)) { - continue; - } - $directory = new \DirectoryIterator($dir); - - foreach ($directory as $file) { - if ($file->getExtension() !== 'json') { - continue; - } - - $content = file_get_contents($file->getPathname()); - - $language = pathinfo($file->getFilename(), PATHINFO_FILENAME); - if (!in_array($language, $rtlLanguages, true) && preg_match_all('/^(.+[' . implode('', $rtlCharacters) . '].+)$/mu', $content, $matches)) { - $errors[] = $file->getPathname() . "\n" . ' ' . 'Contains a RTL limited characters in the translations. Offending strings:' . "\n" . implode("\n", $matches[0]) . "\n"; - } - - $json = json_decode($content, true); - - $translations = json_encode($json['translations']); - if (strpos($translations, '|') !== false) { - $errors[] = $file->getPathname() . "\n" . ' ' . 'Contains a | in the translations.' . "\n"; - } - - if (json_last_error() !== JSON_ERROR_NONE) { - $errors[] = $file->getPathname() . "\n" . ' ' . json_last_error_msg() . "\n"; - } else { - $valid++; - } - - if ($isDebug && $file->getFilename() === 'en_GB.json') { - $sourceStrings = json_encode(array_keys($json['translations'])); - - if (strpos($sourceStrings, '\u2019') !== false) { - $errors[] = $file->getPathname() . "\n" - . ' ' . 'Contains a unicode single quote "’" in the english source string, please replace with normal single quotes.' . "\n" - . ' ' . 'Please note that this only updates after a sync to transifex.' . "\n"; - } - } - } -} - -if (count($errors) > 0) { - echo "\033[0;31m" . sprintf('ERROR: There were %d errors:', count($errors)) . "\033[0m\n\n"; - echo implode("\n", $errors); - exit(1); -} - -echo "\033[0;32m" . 'OK: ' . $valid . ' files parse' . "\033[0m\n"; -exit(0); --- a/build/triple-dot-checker.php +++ /dev/null @@ -1,47 +0,0 @@ -getExtension() === 'map') || $file->isDir()) { - continue; - } - $content = file_get_contents($file->getPathname()); - $matches = preg_grep('/[^A-Za-z][tn]\([\'"][^\'"]*?[\'"],( ?)[\'"][^\'"]*?(\.\.\.)[^\'"]*?[\'"]/sU', explode(PHP_EOL, $content)); - - foreach ($matches as $ln => $match) { - if (file_exists($file->getPathname() . '.map')) { - $errors[] = sprintf('At line %d in file %s (file may be transpiled)', $ln, $file); - } else { - $errors[] = sprintf('At line %d in file %s', $ln, $file); - } - } - } -} - -echo PHP_EOL . PHP_EOL; -if (count($errors) > 0) { - if (count($errors) > 1) { - echo sprintf('ERROR: There are %d uses of triple dot instead of ellipsis:', count($errors)) . PHP_EOL; - } else { - echo 'ERROR: There is 1 use of triple dot instead of ellipsis:' . PHP_EOL; - } - echo implode(PHP_EOL, $errors) . PHP_EOL; - exit(1); -} - -echo 'OK: all good! No use of triple dot instead of ellipsis found.' . PHP_EOL; -exit(0); --- a/build/update-apps.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# -# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Update Nextcloud apps from latest git stable35 -# For local development environment -# Use from Nextcloud server folder with `./build/update-apps.sh` -# -# It automatically: -# - goes through all apps which are not shipped via server -# - shows the app name in bold and uses whitespace for separation -# - changes to stable35 and pulls quietly -# - shows the 3 most recent commits for context -# - removes branches merged into stable35 -# - … could even do the build steps if they are consistent for the apps (like `make`) - -set -euo pipefail - -for path in apps*/*/.git; do - ( - path="$(dirname "$path")" - cd "$path" - printf "\n\033[1m${PWD##*/}\033[0m\n" - branch="$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')" - git checkout "$branch" - git pull --quiet -p - git --no-pager log -3 --pretty=format:"%h %Cblue%ar%x09%an %Creset%s" - printf "\n" - git branch --merged "$branch" | grep -v "$branch$" | xargs git branch -d || true - ) -done --- a/build/update.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# -# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Update Nextcloud server and apps from latest git stable35 -# For local development environment -# Use from Nextcloud server folder with `./build/update.sh` - -# Update server -printf "\n\033[1m${PWD##*/}\033[0m\n" -git checkout stable35 -git pull --quiet -p -git --no-pager log -3 --pretty=format:"%h %Cblue%ar%x09%an %Creset%s" -printf "\n" -git branch --merged stable35 | grep -v "stable35$" | xargs git branch -d -git submodule update --init - -# Update apps -source ./build/update-apps.sh --- a/contribute/HowToApplyALicense.md +++ /dev/null @@ -1,87 +0,0 @@ - -# How to apply a license - -Originally Nextcloud was licensed under the GNU AGPLv3 only. From -June, 16 2016 on we switched to "GNU AGPLv3 or any later version" for -better long-term maintainability and to make it more secure from a -legal point of view. - -Additionally Nextcloud doesn't require a CLA (Contributor License -Agreement). The copyright belongs to all the individual -contributors. - -## Apply a license to a new file - -If you create a new file please use a SPDX license header. -The year should then be the creation time and the email address is optional. - -#### Frontend source (`.js`, `.ts`, `.css` and etc) - -```js -/** - * SPDX-FileCopyrightText: [year] [your name] [] - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -```` - -or `.vue` files - -```html - -``` - -#### Backend source (`.php`) - -```php -/** - * SPDX-FileCopyrightText: [year] [your name] [] - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -``` - -## Apply a licence to an existing file - -If you modify an existing file, please keep the existing license header as -it is and just add your copyright notice. -In order to do so there are two options: - -* If a generic header is already present, please just add yourself to the AUTHORS.md file -* If no generic header is present, you can add yourself with a copyright line as described below - -````diff -/** - * SPDX-FileCopyrightText: 2022 Alice - * SPDX-FileCopyrightText: 2023 Bob -+* SPDX-FileCopyrightText: [year] [your name] [] - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -```` - -An example of a generic license header where adding yourself to the AUTHORS.md -file is prefered please see the example below - -``` -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -``` - -For more information on SPDX headers, please see - -* https://reuse.software/ -* https://spdx.dev/ - -## DCO - -Additionally we require a Developer Certificate of Origin (DCO), look -at [CONTRIBUTING.md][contributing] to learn more how to sign your commits. - -[contributing]: ../.github/CONTRIBUTING.md#sign-your-work --- a/contribute/developer-certificate-of-origin +++ /dev/null @@ -1,35 +0,0 @@ -Developer Certificate of Origin -Version 1.1 - -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -660 York Street, Suite 102, -San Francisco, CA 94110 USA - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. --- a/contribute/developer-certificate-of-origin.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2004, 2006 The Linux Foundation and its contributors. -SPDX-License-Identifier: LicenseRef-DCO --- a/tests/Core/BackgroundJobs/CheckCodeIntegrityJobTest.php +++ /dev/null @@ -1,296 +0,0 @@ - [ - 'EXTRA_FILE' => [ - 'core/extra.php' => ['expected' => '', 'current' => 'abc'], - ], - ], - ]; - - private Checker&MockObject $checker; - private IConfig&MockObject $config; - private IAppConfig&MockObject $appConfig; - private IGroupManager&MockObject $groupManager; - private INotificationManager&MockObject $notificationManager; - private IMailer&MockObject $mailer; - private IL10N&MockObject $l10n; - /** @var list recipients collected by expectAdminNotifications() */ - private array $users = []; - private CheckCodeIntegrityJob $job; - - protected function setUp(): void { - parent::setUp(); - - $this->checker = $this->createMock(Checker::class); - $this->config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->notificationManager = $this->createMock(INotificationManager::class); - $this->mailer = $this->createMock(IMailer::class); - - $this->config->method('getSystemValueBool') - ->with('integrity.check.scheduled', true) - ->willReturn(true); - $this->checker->method('isCodeCheckEnforced')->willReturn(true); - - $this->l10n = $this->createMock(IL10N::class); - $this->l10n->method('t')->willReturnArgument(0); - $this->l10n->method('n')->willReturnCallback( - fn (string $singular, string $plural, int $count, array $parameters = []): string - => vsprintf(str_replace('%n', (string)$count, $count === 1 ? $singular : $plural), $parameters) - ); - - $this->mailer->method('createEMailTemplate')->willReturn($this->createMock(IEMailTemplate::class)); - $this->mailer->method('createMessage')->willReturn($this->createMock(IMessage::class)); - - $this->notificationManager->method('createNotification')->willReturnCallback( - fn (): INotification => new Notification($this->createMock(IValidator::class), $this->createMock(IRichTextFormatter::class)) - ); - - $this->job = $this->createJob($this->checker, $this->config); - } - - private function createJob(Checker $checker, IConfig $config): CheckCodeIntegrityJob { - $time = $this->createMock(ITimeFactory::class); - $time->method('getDateTime')->willReturn(new \DateTime('2026-09-25 12:00:00')); - $l10nFactory = $this->createMock(IFactory::class); - $l10nFactory->method('get')->willReturn($this->l10n); - - return new CheckCodeIntegrityJob( - $time, - $checker, - $config, - $this->appConfig, - $this->groupManager, - $this->notificationManager, - $this->mailer, - $l10nFactory, - $this->createMock(IURLGenerator::class), - $this->createMock(LoggerInterface::class), - ); - } - - private function mockAdmins(): void { - $withEmail = $this->createMock(IUser::class); - $withEmail->method('getUID')->willReturn('admin1'); - $withEmail->method('getEMailAddress')->willReturn('admin1@example.com'); - $withoutEmail = $this->createMock(IUser::class); - $withoutEmail->method('getUID')->willReturn('admin2'); - $withoutEmail->method('getEMailAddress')->willReturn(null); - - $group = $this->createMock(IGroup::class); - $group->method('getUsers')->willReturn([$withEmail, $withoutEmail]); - $this->groupManager->method('get')->with('admin')->willReturn($group); - } - - private function fingerprint(array $results): string { - return self::invokePrivate($this->job, 'fingerprint', [$results]); - } - - /** - * Expects one notification per admin for the given fingerprint and summary - */ - private function expectAdminNotifications(string $fingerprint, array $summary): void { - $this->notificationManager->expects($this->exactly(2))->method('notify') - ->willReturnCallback(function (INotification $notification) use ($fingerprint, $summary): void { - $this->assertSame('core', $notification->getApp()); - $this->assertSame(CheckCodeIntegrityJob::NOTIFICATION_OBJECT_TYPE, $notification->getObjectType()); - $this->assertSame($fingerprint, $notification->getObjectId()); - $this->assertSame('code_integrity_changed', $notification->getSubject()); - $this->assertSame($summary, $notification->getSubjectParameters()); - $this->users[] = $notification->getUser(); - }); - } - - private function expectRemovedNotification(string $fingerprint): void { - $this->notificationManager->expects($this->once())->method('markProcessed') - ->with($this->callback(fn (INotification $notification): bool - => $notification->getApp() === 'core' - && $notification->getObjectType() === CheckCodeIntegrityJob::NOTIFICATION_OBJECT_TYPE - && $notification->getObjectId() === $fingerprint)); - } - - private function expectStoredFingerprint(string $fingerprint): void { - $this->appConfig->expects($this->once())->method('setValueString') - ->with('core', ConfigLexicon::INTEGRITY_CHECK_NOTIFIED_RESULT, $fingerprint); - } - - public function testSkipsWhenDisabledByConfig(): void { - $config = $this->createMock(IConfig::class); - $config->method('getSystemValueBool') - ->with('integrity.check.scheduled', true) - ->willReturn(false); - - $this->checker->expects($this->never())->method('runInstanceVerification'); - - self::invokePrivate($this->createJob($this->checker, $config), 'run', [null]); - } - - public function testSkipsWhenCodeCheckIsNotEnforced(): void { - $checker = $this->createMock(Checker::class); - $checker->method('isCodeCheckEnforced')->willReturn(false); - $checker->expects($this->never())->method('runInstanceVerification'); - - self::invokePrivate($this->createJob($checker, $this->config), 'run', [null]); - } - - public function testNotifiesAndMailsAdminsWhenCheckStartsFailing(): void { - $this->mockAdmins(); - $this->checker->expects($this->once())->method('runInstanceVerification'); - $this->checker->method('getResults')->willReturn(self::FAILED_RESULT); - $this->appConfig->method('getValueString') - ->with('core', ConfigLexicon::INTEGRITY_CHECK_NOTIFIED_RESULT) - ->willReturn(''); - - $fingerprint = $this->fingerprint(self::FAILED_RESULT); - $this->notificationManager->expects($this->never())->method('markProcessed'); - $this->expectAdminNotifications($fingerprint, ['files' => 1, 'unverified' => []]); - $this->mailer->expects($this->once())->method('send'); - $this->expectStoredFingerprint($fingerprint); - - self::invokePrivate($this->job, 'run', [null]); - - $this->assertSame(['admin1', 'admin2'], $this->users); - } - - public function testDoesNothingWhenResultIsUnchanged(): void { - $this->checker->method('getResults')->willReturn(self::FAILED_RESULT); - $this->appConfig->method('getValueString')->willReturn($this->fingerprint(self::FAILED_RESULT)); - - $this->notificationManager->expects($this->never())->method('markProcessed'); - $this->notificationManager->expects($this->never())->method('notify'); - $this->mailer->expects($this->never())->method('send'); - $this->appConfig->expects($this->never())->method('setValueString'); - - self::invokePrivate($this->job, 'run', [null]); - } - - public function testReplacesNotificationWhenFailureChanges(): void { - $this->mockAdmins(); - $changed = self::FAILED_RESULT; - $changed['core']['EXTRA_FILE']['core/another.php'] = ['expected' => '', 'current' => 'def']; - $this->checker->method('getResults')->willReturn($changed); - $previous = $this->fingerprint(self::FAILED_RESULT); - $this->appConfig->method('getValueString')->willReturn($previous); - - $fingerprint = $this->fingerprint($changed); - $this->expectRemovedNotification($previous); - $this->expectAdminNotifications($fingerprint, ['files' => 2, 'unverified' => []]); - $this->mailer->expects($this->once())->method('send'); - $this->expectStoredFingerprint($fingerprint); - - self::invokePrivate($this->job, 'run', [null]); - } - - public function testClearsNotificationWhenCheckPassesAgain(): void { - $this->checker->method('getResults')->willReturn([]); - $previous = $this->fingerprint(self::FAILED_RESULT); - $this->appConfig->method('getValueString')->willReturn($previous); - - $this->expectRemovedNotification($previous); - $this->notificationManager->expects($this->never())->method('notify'); - $this->mailer->expects($this->never())->method('send'); - $this->expectStoredFingerprint(''); - - self::invokePrivate($this->job, 'run', [null]); - } - - public function testMailFailureDoesNotPreventStoringTheResult(): void { - $this->mockAdmins(); - $this->checker->method('getResults')->willReturn(self::FAILED_RESULT); - $this->appConfig->method('getValueString')->willReturn(''); - $this->mailer->method('send')->willThrowException(new \Exception('SMTP down')); - - $fingerprint = $this->fingerprint(self::FAILED_RESULT); - $this->expectAdminNotifications($fingerprint, ['files' => 1, 'unverified' => []]); - $this->expectStoredFingerprint($fingerprint); - - self::invokePrivate($this->job, 'run', [null]); - } - - public function testFingerprintIgnoresOrder(): void { - $result = [ - 'core' => ['EXTRA_FILE' => ['b.php' => ['current' => '2'], 'a.php' => ['current' => '1']]], - 'myapp' => ['INVALID_HASH' => ['x.php' => ['current' => 'c']]], - ]; - $reordered = [ - 'myapp' => ['INVALID_HASH' => ['x.php' => ['current' => 'c']]], - 'core' => ['EXTRA_FILE' => ['a.php' => ['current' => '1'], 'b.php' => ['current' => '2']]], - ]; - - $this->assertSame($this->fingerprint($result), $this->fingerprint($reordered)); - } - - public function testFingerprintChangesWhenFailingFileChangesAgain(): void { - $changedAgain = self::FAILED_RESULT; - $changedAgain['core']['EXTRA_FILE']['core/extra.php']['current'] = 'def'; - - $this->assertNotSame($this->fingerprint(self::FAILED_RESULT), $this->fingerprint($changedAgain)); - } - - public function testSummarizeSeparatesFilesFromUnverifiedScopes(): void { - $result = [ - 'myapp' => ['EXTRA_FILE' => ['extra1.php' => [], 'extra2.php' => []], 'INVALID_HASH' => ['lib/Changed.php' => []]], - 'files' => ['EXCEPTION' => ['class' => 'X', 'message' => 'Signature data not found.']], - 'core' => ['EXCEPTION' => ['class' => 'X', 'message' => 'Certificate is not valid.']], - ]; - - $this->assertSame( - ['files' => 3, 'unverified' => ['core', 'files']], - self::invokePrivate($this->job, 'summarize', [$result]), - ); - } - - public static function dataFormatSummary(): array { - return [ - 'files only' => [['files' => 2, 'unverified' => []], ['2 files do not match the signed release. They may have been modified or added without authorization.']], - 'unverified only' => [['files' => 0, 'unverified' => ['core']], ['The signature of core is missing or invalid, so it could not be verified.']], - 'both' => [['files' => 1, 'unverified' => ['core', 'files']], [ - '1 file does not match the signed release. It may have been modified or added without authorization.', - 'The signatures of core, files are missing or invalid, so they could not be verified.', - ]], - 'missing parameters' => [[], []], - ]; - } - - #[DataProvider('dataFormatSummary')] - public function testFormatSummary(array $summary, array $expected): void { - $this->assertSame($expected, CheckCodeIntegrityJob::formatSummary($this->l10n, $summary)); - } -} --- a/tests/Core/Command/Apps/AppsDisableTest.php +++ /dev/null @@ -1,73 +0,0 @@ -commandTester = new CommandTester($command); - - Server::get(IAppManager::class)->enableApp('admin_audit'); - Server::get(IAppManager::class)->enableApp('comments'); - } - - /** - * @param $appId - * @param $groups - * @param $statusCode - * @param $pattern - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataCommandInput')] - public function testCommandInput($appId, $statusCode, $pattern): void { - $input = ['app-id' => $appId]; - - $this->commandTester->execute($input); - - $this->assertMatchesRegularExpression('/' . $pattern . '/', $this->commandTester->getDisplay()); - $this->assertSame($statusCode, $this->commandTester->getStatusCode()); - } - - public const VERSION_REGEX = '([\d\.\-dev]*)'; - - public static function dataCommandInput(): array { - return [ - [['admin_audit'], 0, 'admin_audit ' . self::VERSION_REGEX . ' disabled'], - [['comments'], 0, 'comments ' . self::VERSION_REGEX . ' disabled'], - [['invalid_app'], 0, 'No such app enabled: invalid_app'], - - [['admin_audit', 'comments'], 0, 'admin_audit ' . self::VERSION_REGEX . " disabled\ncomments " . self::VERSION_REGEX . ' disabled'], - [['admin_audit', 'comments', 'invalid_app'], 0, 'admin_audit ' . self::VERSION_REGEX . " disabled\ncomments " . self::VERSION_REGEX . " disabled\nNo such app enabled: invalid_app"], - - [['files'], 2, "files can't be disabled"], - [['provisioning_api'], 2, "provisioning_api can't be disabled"], - - [['files', 'admin_audit'], 2, "files can't be disabled.\nadmin_audit " . self::VERSION_REGEX . ' disabled'], - [['provisioning_api', 'comments'], 2, "provisioning_api can't be disabled.\ncomments " . self::VERSION_REGEX . ' disabled'], - ]; - } -} --- a/tests/Core/Command/Apps/AppsEnableTest.php +++ /dev/null @@ -1,90 +0,0 @@ -commandTester = new CommandTester($command); - - // Not every setup disables the app store, and enabling an unknown app pulls - // the whole catalogue (~30 MB, cached for an hour) to find it is not there. - $this->overwriteSystemConfig('appstoreenabled', false); - - Server::get(IAppManager::class)->disableApp('admin_audit'); - Server::get(IAppManager::class)->disableApp('comments'); - } - - /** - * @param $appId - * @param $groups - * @param $statusCode - * @param $pattern - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataCommandInput')] - public function testCommandInput($appId, $groups, $statusCode, $pattern): void { - $input = ['app-id' => $appId]; - - if (is_array($groups)) { - $input['--groups'] = $groups; - } - - $this->commandTester->execute($input); - - $this->assertMatchesRegularExpression('/' . $pattern . '/', $this->commandTester->getDisplay()); - $this->assertSame($statusCode, $this->commandTester->getStatusCode()); - } - - public const VERSION_REGEX = '([\d\.\-dev]*)'; - - public static function dataCommandInput(): array { - return [ - [['admin_audit'], null, 0, 'admin_audit ' . self::VERSION_REGEX . ' enabled'], - [['comments'], null, 0, 'comments ' . self::VERSION_REGEX . ' enabled'], - [['comments', 'comments'], null, 0, 'comments ' . self::VERSION_REGEX . " enabled\ncomments already enabled"], - [['invalid_app'], null, 1, 'Could not download app invalid_app'], - - [['admin_audit', 'comments'], null, 0, 'admin_audit ' . self::VERSION_REGEX . " enabled\ncomments " . self::VERSION_REGEX . ' enabled'], - [['admin_audit', 'comments', 'invalid_app'], null, 1, 'admin_audit ' . self::VERSION_REGEX . " enabled\ncomments " . self::VERSION_REGEX . " enabled\nCould not download app invalid_app"], - - [['admin_audit'], ['admin'], 1, "admin_audit can't be enabled for groups"], - [['comments'], ['admin'], 1, "comments can't be enabled for groups"], - - [['updatenotification'], ['admin'], 0, 'updatenotification ' . self::VERSION_REGEX . ' enabled for groups: admin'], - [['updatenotification', 'dashboard'], ['admin'], 0, 'updatenotification ' . self::VERSION_REGEX . " enabled for groups: admin\ndashboard " . self::VERSION_REGEX . ' enabled for groups: admin'], - - [['updatenotification'], ['admin', 'invalid_group'], 0, 'updatenotification ' . self::VERSION_REGEX . ' enabled for groups: admin'], - [['updatenotification', 'dashboard'], ['admin', 'invalid_group'], 0, 'updatenotification ' . self::VERSION_REGEX . " enabled for groups: admin\ndashboard " . self::VERSION_REGEX . ' enabled for groups: admin'], - [['updatenotification', 'dashboard', 'invalid_app'], ['admin', 'invalid_group'], 1, 'updatenotification ' . self::VERSION_REGEX . " enabled for groups: admin\ndashboard " . self::VERSION_REGEX . " enabled for groups: admin\nCould not download app invalid_app"], - ]; - } -} --- a/tests/Core/Command/Config/App/DeleteConfigTest.php +++ /dev/null @@ -1,99 +0,0 @@ -appConfig = $this->createMock(IAppConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new DeleteConfig($this->appConfig, $this->configManager); - } - - public static function dataDelete(): array { - return [ - [ - 'name', - true, - true, - 0, - 'info', - ], - [ - 'name', - true, - false, - 0, - 'info', - ], - [ - 'name', - false, - false, - 0, - 'info', - ], - [ - 'name', - false, - true, - 1, - 'error', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataDelete')] - public function testDelete(string $configName, bool $configExists, bool $checkIfExists, int $expectedReturn, string $expectedMessage): void { - $this->appConfig->expects(($checkIfExists) ? $this->once() : $this->never()) - ->method('getKeys') - ->with('app-name') - ->willReturn($configExists ? [$configName] : []); - - $this->appConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never()) - ->method('deleteKey') - ->with('app-name', $configName); - - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['app', 'app-name'], - ['name', $configName], - ]); - $this->consoleInput->method('hasParameterOption') - ->with('--error-if-not-exists') - ->willReturn($checkIfExists); - - $this->consoleOutput->method('writeln') - ->with($this->stringContains($expectedMessage)); - - $this->assertSame($expectedReturn, self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } -} --- a/tests/Core/Command/Config/App/GetConfigTest.php +++ /dev/null @@ -1,137 +0,0 @@ -appConfig = $this->createMock(IAppConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new GetConfig($this->appConfig, $this->configManager); - } - - public static function dataGet(): array { - return [ - // String output as json - ['name', 'newvalue', true, null, false, 'json', 0, json_encode('newvalue')], - // String output as plain text - ['name', 'newvalue', true, null, false, 'plain', 0, 'newvalue'], - // String falling back to default output as json - ['name', null, false, 'newvalue', true, 'json', 0, json_encode('newvalue')], - // String falling back without default: error - ['name', null, false, null, false, 'json', 1, null], - - // Int "0" output as json/plain - ['name', 0, true, null, false, 'json', 0, json_encode(0)], - ['name', 0, true, null, false, 'plain', 0, '0'], - // Int "1" output as json/plain - ['name', 1, true, null, false, 'json', 0, json_encode(1)], - ['name', 1, true, null, false, 'plain', 0, '1'], - - // Bool "true" output as json/plain - ['name', true, true, null, false, 'json', 0, json_encode(true)], - ['name', true, true, null, false, 'plain', 0, 'true'], - // Bool "false" output as json/plain - ['name', false, true, null, false, 'json', 0, json_encode(false)], - ['name', false, true, null, false, 'plain', 0, 'false'], - - // Null output as json/plain - ['name', null, true, null, false, 'json', 0, json_encode(null)], - ['name', null, true, null, false, 'plain', 0, 'null'], - - // Array output as json/plain - ['name', ['a', 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])], - ['name', ['a', 'b'], true, null, false, 'plain', 0, "a\nb"], - // Key array output as json/plain - ['name', [0 => 'a', 1 => 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])], - ['name', [0 => 'a', 1 => 'b'], true, null, false, 'plain', 0, "a\nb"], - // Associative array output as json/plain - ['name', ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(['a' => 1, 'b' => 2])], - ['name', ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, "a: 1\nb: 2"], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGet')] - public function testGet(string $configName, mixed $value, bool $configExists, mixed $defaultValue, bool $hasDefault, string $outputFormat, int $expectedReturn, ?string $expectedMessage): void { - if (!$expectedReturn) { - if ($configExists) { - $this->appConfig->expects($this->once()) - ->method('getDetails') - ->with('app-name', $configName) - ->willReturn(['value' => $value]); - } - } - - if (!$configExists) { - $this->appConfig->expects($this->once()) - ->method('getDetails') - ->with('app-name', $configName) - ->willThrowException(new AppConfigUnknownKeyException()); - } - - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['app', 'app-name'], - ['name', $configName], - ]); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['default-value', $defaultValue], - ['output', $outputFormat], - ]); - $this->consoleInput->method('hasParameterOption') - ->willReturnMap([ - ['--output', false, true], - ['--default-value', false, $hasDefault], - ]); - - if ($expectedMessage !== null) { - global $output; - - $output = ''; - $this->consoleOutput->method('writeln') - ->willReturnCallback(function ($value) { - global $output; - $output .= $value . "\n"; - return $output; - }); - } - - $this->assertSame($expectedReturn, self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); - - if ($expectedMessage !== null) { - global $output; - // Remove the trailing newline - $this->assertSame($expectedMessage, substr($output, 0, -1)); - } - } -} --- a/tests/Core/Command/Config/App/SetConfigTest.php +++ /dev/null @@ -1,107 +0,0 @@ -appConfig = $this->createMock(AppConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new SetConfig($this->appConfig, $this->configManager); - } - - public static function dataSet(): array { - return [ - [ - 'name', - 'newvalue', - true, - true, - true, - 'info', - ], - [ - 'name', - 'newvalue', - false, - true, - false, - 'comment', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSet')] - public function testSet(string $configName, mixed $newValue, bool $configExists, bool $updateOnly, bool $updated, string $expectedMessage): void { - $this->appConfig->method('hasKey') - ->with('app-name', $configName) - ->willReturn($configExists); - - if (!$configExists) { - $this->appConfig->method('getValueType') - ->willThrowException(new AppConfigUnknownKeyException()); - } else { - $this->appConfig->method('getValueType') - ->willReturn(IAppConfig::VALUE_MIXED); - } - - if ($updated) { - $this->appConfig->expects($this->once()) - ->method('setValueMixed') - ->with('app-name', $configName, $newValue); - } - - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['app', 'app-name'], - ['name', $configName], - ]); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['value', $newValue], - ['lazy', null], - ['sensitive', null], - ['no-interaction', true], - ]); - $this->consoleInput->method('hasParameterOption') - ->willReturnMap([ - ['--type', false, false], - ['--value', false, true], - ['--update-only', false, $updateOnly] - ]); - $this->consoleOutput->method('writeln') - ->with($this->stringContains($expectedMessage)); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Config/ImportTest.php +++ /dev/null @@ -1,170 +0,0 @@ -config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var IConfig $config */ - $this->command = new Import($config); - } - - public static function validateAppsArrayData(): array { - return [ - [0], - [1], - [null], - ['new \Exception()'], - [json_encode([])], - ]; - } - - /** - * @param mixed $configValue - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validateAppsArrayData')] - public function testValidateAppsArray($configValue): void { - $this->invokePrivate($this->command, 'validateAppsArray', [['app' => ['name' => $configValue]]]); - $this->assertTrue(true, 'Asserting that no exception is thrown'); - } - - public static function validateAppsArrayThrowsData(): array { - return [ - [false], - [true], - [[]], - [new \Exception()], - ]; - } - - /** - * @param mixed $configValue - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validateAppsArrayThrowsData')] - public function testValidateAppsArrayThrows($configValue): void { - try { - $this->invokePrivate($this->command, 'validateAppsArray', [['app' => ['name' => $configValue]]]); - $this->fail('Did not throw expected UnexpectedValueException'); - } catch (\UnexpectedValueException $e) { - $this->assertStringStartsWith('Invalid app config value for "app":"name".', $e->getMessage()); - } - } - - public static function checkTypeRecursivelyData(): array { - return [ - [0], - [1], - [null], - ['new \Exception()'], - [json_encode([])], - [false], - [true], - [[]], - [['string']], - [['test' => 'string']], - [['test' => ['sub' => 'string']]], - ]; - } - - /** - * @param mixed $configValue - */ - #[\PHPUnit\Framework\Attributes\DataProvider('checkTypeRecursivelyData')] - public function testCheckTypeRecursively($configValue): void { - $this->invokePrivate($this->command, 'checkTypeRecursively', [$configValue, 'name']); - $this->assertTrue(true, 'Asserting that no exception is thrown'); - } - - public static function checkTypeRecursivelyThrowsData(): array { - return [ - [new \Exception()], - [[new \Exception()]], - [['test' => new \Exception()]], - [['test' => ['sub' => new \Exception()]]], - ]; - } - - /** - * @param mixed $configValue - */ - #[\PHPUnit\Framework\Attributes\DataProvider('checkTypeRecursivelyThrowsData')] - public function testCheckTypeRecursivelyThrows($configValue): void { - try { - $this->invokePrivate($this->command, 'checkTypeRecursively', [$configValue, 'name']); - $this->fail('Did not throw expected UnexpectedValueException'); - } catch (\UnexpectedValueException $e) { - $this->assertStringStartsWith('Invalid system config value for "name"', $e->getMessage()); - } - } - - public static function validateArrayData(): array { - return [ - [['system' => []]], - [['apps' => []]], - [['system' => [], 'apps' => []]], - ]; - } - - /** - * @param array $configArray - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validateArrayData')] - public function testValidateArray($configArray): void { - $this->invokePrivate($this->command, 'validateArray', [$configArray]); - $this->assertTrue(true, 'Asserting that no exception is thrown'); - } - - public static function validateArrayThrowsData(): array { - return [ - [[], 'At least one key of the following is expected:'], - [[0 => []], 'Found invalid entries in root'], - [['string' => []], 'Found invalid entries in root'], - ]; - } - - /** - * - * @param mixed $configArray - * @param string $expectedException - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validateArrayThrowsData')] - public function testValidateArrayThrows($configArray, $expectedException): void { - try { - $this->invokePrivate($this->command, 'validateArray', [$configArray]); - $this->fail('Did not throw expected UnexpectedValueException'); - } catch (\UnexpectedValueException $e) { - $this->assertStringStartsWith($expectedException, $e->getMessage()); - } - } -} --- a/tests/Core/Command/Config/ListConfigsTest.php +++ /dev/null @@ -1,328 +0,0 @@ -systemConfig = $this->getMockBuilder(SystemConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->appConfig = $this->getMockBuilder(IAppConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->configManager = $this->getMockBuilder(ConfigManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->appManager = $this->createMock(IAppManager::class); - - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new ListConfigs( - $this->systemConfig, - $this->appConfig, - $this->configManager, - $this->appManager, - ); - } - - public static function listData(): array { - return [ - [ - 'all', - // config.php - [ - 'secret', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', IConfig::SENSITIVE_VALUE], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - false, - json_encode([ - 'system' => [ - 'secret' => IConfig::SENSITIVE_VALUE, - 'overwrite.cli.url' => 'http://localhost', - ], - 'apps' => [ - 'core' => [ - 'global_cache_gc_lastrun' => '1430388388', - ], - 'files' => [ - 'enabled' => 'yes', - ], - ], - ]), - ], - [ - 'all', - // config.php - [ - 'secret', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', 'my secret'], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - true, - json_encode([ - 'system' => [ - 'secret' => 'my secret', - 'overwrite.cli.url' => 'http://localhost', - ], - 'apps' => [ - 'core' => [ - 'global_cache_gc_lastrun' => '1430388388', - ], - 'files' => [ - 'enabled' => 'yes', - ], - ], - ]), - ], - [ - 'system', - // config.php - [ - 'secret', - 'objectstore', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', IConfig::SENSITIVE_VALUE], - ['objectstore', 'N;', [ - 'class' => 'OC\\Files\\ObjectStore\\Swift', - 'arguments' => [ - 'username' => 'facebook100000123456789', - 'password' => IConfig::SENSITIVE_VALUE, - ], - ]], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - false, - json_encode([ - 'system' => [ - 'secret' => IConfig::SENSITIVE_VALUE, - 'objectstore' => [ - 'class' => 'OC\\Files\\ObjectStore\\Swift', - 'arguments' => [ - 'username' => 'facebook100000123456789', - 'password' => IConfig::SENSITIVE_VALUE, - ], - ], - 'overwrite.cli.url' => 'http://localhost', - ], - ]), - ], - [ - 'system', - // config.php - [ - 'secret', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', 'my secret'], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - true, - json_encode([ - 'system' => [ - 'secret' => 'my secret', - 'overwrite.cli.url' => 'http://localhost', - ], - ]), - ], - [ - 'files', - // config.php - [ - 'secret', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', 'my secret'], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - false, - json_encode([ - 'apps' => [ - 'files' => [ - 'enabled' => 'yes', - ], - ], - ]), - ], - [ - 'files', - // config.php - [ - 'secret', - 'overwrite.cli.url', - ], - [ - ['secret', 'N;', 'my secret'], - ['overwrite.cli.url', 'N;', 'http://localhost'], - ], - // app config - [ - ['files', [ - 'enabled' => 'yes', - ]], - ['core', [ - 'global_cache_gc_lastrun' => '1430388388', - ]], - ], - true, - json_encode([ - 'apps' => [ - 'files' => [ - 'enabled' => 'yes', - ], - ], - ]), - ], - ]; - } - - /** - * - * @param string $app - * @param array $systemConfigs - * @param array $systemConfigMap - * @param array $appConfig - * @param bool $private - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('listData')] - public function testList($app, $systemConfigs, $systemConfigMap, $appConfig, $private, $expected): void { - $this->systemConfig->expects($this->any()) - ->method('getKeys') - ->willReturn($systemConfigs); - if ($private) { - $this->systemConfig->expects($this->any()) - ->method('getValue') - ->willReturnMap($systemConfigMap); - $this->appConfig->expects($this->any()) - ->method('getAllValues') - ->willReturnMap($appConfig); - } else { - $this->systemConfig->expects($this->any()) - ->method('getFilteredValue') - ->willReturnMap($systemConfigMap); - $this->appConfig->expects($this->any()) - ->method('getFilteredValues') - ->willReturnMap($appConfig); - } - - $this->appConfig->expects($this->any()) - ->method('getApps') - ->willReturn(['core', 'files']); - $this->appConfig->expects($this->any()) - ->method('getAllValues') - ->willReturnMap($appConfig); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('app') - ->willReturn($app); - - $this->consoleInput->expects($this->any()) - ->method('getOption') - ->willReturnMap([ - ['output', 'json'], - ['private', $private], - ]); - - global $output; - - $output = ''; - $this->consoleOutput->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function ($value) use (&$output) { - $output .= $value . "\n"; - return $output; - }); - - $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - - $this->assertEquals($expected, trim($output, "\n")); - } -} --- a/tests/Core/Command/Config/System/CastHelperTest.php +++ /dev/null @@ -1,67 +0,0 @@ -castHelper = new CastHelper(); - } - - public static function castValueProvider(): array { - return [ - [null, 'string', ['value' => '', 'readable-value' => 'empty string']], - - ['abc', 'string', ['value' => 'abc', 'readable-value' => 'string abc']], - - ['123', 'integer', ['value' => 123, 'readable-value' => 'integer 123']], - ['456', 'int', ['value' => 456, 'readable-value' => 'integer 456']], - - ['2.25', 'double', ['value' => 2.25, 'readable-value' => 'double 2.25']], - ['0.5', 'float', ['value' => 0.5, 'readable-value' => 'double 0.5']], - - ['', 'null', ['value' => null, 'readable-value' => 'null']], - - ['true', 'boolean', ['value' => true, 'readable-value' => 'boolean true']], - ['false', 'bool', ['value' => false, 'readable-value' => 'boolean false']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('castValueProvider')] - public function testCastValue($value, $type, $expectedValue): void { - $this->assertSame( - $expectedValue, - $this->castHelper->castValue($value, $type) - ); - } - - public static function castValueInvalidProvider(): array { - return [ - ['123', 'foobar'], - - [null, 'integer'], - ['abc', 'integer'], - ['76ggg', 'double'], - ['true', 'float'], - ['foobar', 'boolean'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('castValueInvalidProvider')] - public function testCastValueInvalid($value, $type): void { - $this->expectException(\InvalidArgumentException::class); - - $this->castHelper->castValue($value, $type); - } -} --- a/tests/Core/Command/Config/System/DeleteConfigTest.php +++ /dev/null @@ -1,208 +0,0 @@ -systemConfig = $this->getMockBuilder(SystemConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var SystemConfig $systemConfig */ - $this->command = new DeleteConfig($systemConfig); - } - - public static function deleteData(): array { - return [ - [ - 'name1', - true, - true, - 0, - 'info', - ], - [ - 'name2', - true, - false, - 0, - 'info', - ], - [ - 'name3', - false, - false, - 0, - 'info', - ], - [ - 'name4', - false, - true, - 1, - 'error', - ], - ]; - } - - /** - * - * @param string $configName - * @param bool $configExists - * @param bool $checkIfExists - * @param int $expectedReturn - * @param string $expectedMessage - */ - #[\PHPUnit\Framework\Attributes\DataProvider('deleteData')] - public function testDelete($configName, $configExists, $checkIfExists, $expectedReturn, $expectedMessage): void { - $this->systemConfig->expects(($checkIfExists) ? $this->once() : $this->never()) - ->method('getKeys') - ->willReturn($configExists ? [$configName] : []); - - $this->systemConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never()) - ->method('deleteValue') - ->with($configName); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('name') - ->willReturn([$configName]); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->with('--error-if-not-exists') - ->willReturn($checkIfExists); - - $this->consoleOutput->expects($this->any()) - ->method('writeln') - ->with($this->stringContains($expectedMessage)); - - $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function deleteArrayData(): array { - return [ - [ - ['name', 'sub'], - true, - false, - true, - true, - 0, - 'info', - ], - [ - ['name', 'sub', '2sub'], - true, - false, - ['sub' => ['2sub' => 1], 'sub2' => false], - ['sub' => [], 'sub2' => false], - 0, - 'info', - ], - [ - ['name', 'sub3'], - true, - false, - ['sub' => ['2sub' => 1], 'sub2' => false], - ['sub' => ['2sub' => 1], 'sub2' => false], - 0, - 'info', - ], - [ - ['name', 'sub'], - false, - true, - true, - true, - 1, - 'error', - ], - [ - ['name', 'sub'], - true, - true, - true, - true, - 1, - 'error', - ], - [ - ['name', 'sub3'], - true, - true, - ['sub' => ['2sub' => 1], 'sub2' => false], - ['sub' => ['2sub' => 1], 'sub2' => false], - 1, - 'error', - ], - ]; - } - - /** - * - * @param string[] $configNames - * @param bool $configKeyExists - * @param bool $checkIfKeyExists - * @param mixed $configValue - * @param mixed $updateValue - * @param int $expectedReturn - * @param string $expectedMessage - */ - #[\PHPUnit\Framework\Attributes\DataProvider('deleteArrayData')] - public function testArrayDelete(array $configNames, $configKeyExists, $checkIfKeyExists, $configValue, $updateValue, $expectedReturn, $expectedMessage): void { - $this->systemConfig->expects(($checkIfKeyExists) ? $this->once() : $this->never()) - ->method('getKeys') - ->willReturn($configKeyExists ? [$configNames[0]] : []); - - $this->systemConfig->expects(($configKeyExists) ? $this->once() : $this->never()) - ->method('getValue') - ->willReturn($configValue); - - $this->systemConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never()) - ->method('setValue') - ->with($configNames[0], $updateValue); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('name') - ->willReturn($configNames); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->with('--error-if-not-exists') - ->willReturn($checkIfKeyExists); - - $this->consoleOutput->expects($this->any()) - ->method('writeln') - ->with($this->stringContains($expectedMessage)); - - $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } -} --- a/tests/Core/Command/Config/System/GetConfigTest.php +++ /dev/null @@ -1,160 +0,0 @@ -systemConfig = $this->getMockBuilder(SystemConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var SystemConfig $systemConfig */ - $this->command = new GetConfig($systemConfig); - } - - public static function getData(): array { - return [ - // String output as json - ['name', 'newvalue', true, null, false, 'json', 0, json_encode('newvalue')], - // String output as plain text - ['name', 'newvalue', true, null, false, 'plain', 0, 'newvalue'], - // String falling back to default output as json - ['name', null, false, 'newvalue', true, 'json', 0, json_encode('newvalue')], - // String falling back without default: error - ['name', null, false, null, false, 'json', 1, null], - - // Int "0" output as json/plain - ['name', 0, true, null, false, 'json', 0, json_encode(0)], - ['name', 0, true, null, false, 'plain', 0, '0'], - // Int "1" output as json/plain - ['name', 1, true, null, false, 'json', 0, json_encode(1)], - ['name', 1, true, null, false, 'plain', 0, '1'], - - // Bool "true" output as json/plain - ['name', true, true, null, false, 'json', 0, json_encode(true)], - ['name', true, true, null, false, 'plain', 0, 'true'], - // Bool "false" output as json/plain - ['name', false, true, null, false, 'json', 0, json_encode(false)], - ['name', false, true, null, false, 'plain', 0, 'false'], - - // Null output as json/plain - ['name', null, true, null, false, 'json', 0, json_encode(null)], - ['name', null, true, null, false, 'plain', 0, 'null'], - - // Array output as json/plain - ['name', ['a', 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])], - ['name', ['a', 'b'], true, null, false, 'plain', 0, "a\nb"], - // Key array output as json/plain - ['name', [0 => 'a', 1 => 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])], - ['name', [0 => 'a', 1 => 'b'], true, null, false, 'plain', 0, "a\nb"], - // Associative array output as json/plain - ['name', ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(['a' => 1, 'b' => 2])], - ['name', ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, "a: 1\nb: 2"], - - // Nested depth - [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(1)], - [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, '1'], - [['name', 'c'], ['a' => 1, 'b' => 2], true, true, true, 'json', 0, json_encode(true)], - [['name', 'c'], ['a' => 1, 'b' => 2], true, true, false, 'json', 1, null], - ]; - } - - /** - * - * @param string[] $configNames - * @param mixed $value - * @param bool $configExists - * @param mixed $defaultValue - * @param bool $hasDefault - * @param string $outputFormat - * @param int $expectedReturn - * @param string $expectedMessage - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getData')] - public function testGet($configNames, $value, $configExists, $defaultValue, $hasDefault, $outputFormat, $expectedReturn, $expectedMessage): void { - if (is_array($configNames)) { - $configName = $configNames[0]; - } else { - $configName = $configNames; - $configNames = [$configName]; - } - $this->systemConfig->expects($this->atLeastOnce()) - ->method('getKeys') - ->willReturn($configExists ? [$configName] : []); - - if (!$expectedReturn) { - if ($configExists) { - $this->systemConfig->expects($this->once()) - ->method('getValue') - ->with($configName) - ->willReturn($value); - } - } - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('name') - ->willReturn($configNames); - $this->consoleInput->expects($this->any()) - ->method('getOption') - ->willReturnMap([ - ['default-value', $defaultValue], - ['output', $outputFormat], - ]); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->willReturnMap([ - ['--output', false, true], - ['--default-value', false,$hasDefault], - ]); - - if ($expectedMessage !== null) { - global $output; - - $output = ''; - $this->consoleOutput->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function ($value) { - global $output; - $output .= $value . "\n"; - return $output; - }); - } - - $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput])); - - if ($expectedMessage !== null) { - global $output; - // Remove the trailing newline - $this->assertSame($expectedMessage, substr($output, 0, -1)); - } - } -} --- a/tests/Core/Command/Config/System/SetConfigTest.php +++ /dev/null @@ -1,115 +0,0 @@ -systemConfig = $this->getMockBuilder(SystemConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var SystemConfig $systemConfig */ - $this->command = new SetConfig($systemConfig, new CastHelper()); - } - - public static function dataTest() { - return [ - [['name'], 'newvalue', null, 'newvalue'], - [['a', 'b', 'c'], 'foobar', null, ['b' => ['c' => 'foobar']]], - [['a', 'b', 'c'], 'foobar', ['b' => ['d' => 'barfoo']], ['b' => ['d' => 'barfoo', 'c' => 'foobar']]], - ]; - } - - /** - * - * @param array $configNames - * @param string $newValue - * @param mixed $existingData - * @param mixed $expectedValue - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')] - public function testSet($configNames, $newValue, $existingData, $expectedValue): void { - $this->systemConfig->expects($this->once()) - ->method('setValue') - ->with($configNames[0], $expectedValue); - $this->systemConfig->method('getValue') - ->with($configNames[0]) - ->willReturn($existingData); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('name') - ->willReturn($configNames); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['value', $newValue], - ['type', 'string'], - ]); - - $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public static function setUpdateOnlyProvider(): array { - return [ - [['name'], null], - [['a', 'b', 'c'], null], - [['a', 'b', 'c'], ['b' => 'foobar']], - [['a', 'b', 'c'], ['b' => ['d' => 'foobar']]], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('setUpdateOnlyProvider')] - public function testSetUpdateOnly($configNames, $existingData): void { - $this->expectException(\UnexpectedValueException::class); - - $this->systemConfig->expects($this->never()) - ->method('setValue'); - $this->systemConfig->method('getValue') - ->with($configNames[0]) - ->willReturn($existingData); - $this->systemConfig->method('getKeys') - ->willReturn($existingData ? $configNames[0] : []); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('name') - ->willReturn($configNames); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['value', 'foobar'], - ['type', 'string'], - ['update-only', true], - ]); - - $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Db/DbIndexUsageTest.php +++ /dev/null @@ -1,169 +0,0 @@ -connection = $this->createMock(Connection::class); - $this->input = $this->createMock(InputInterface::class); - $this->command = new DbIndexUsage($this->connection); - } - - private function mockMySQLRows(): array { - return [ - ['table' => 'oc_filecache', 'index' => 'idx_fc_name', 'reads' => 0, 'writes' => 150], - ['table' => 'oc_share', 'index' => 'idx_sh_par', 'reads' => 0, 'writes' => 42], - ]; - } - - private function mockPostgreSQLRows(): array { - return [ - ['table' => 'oc_filecache', 'index' => 'idx_fc_name', 'reads' => 0, 'tuples_read' => 0, 'tuples_fetched' => 0], - ]; - } - - private function mockResult(array $rows): Result&MockObject { - $result = $this->createMock(Result::class); - $result->method('fetchAllAssociative')->willReturn($rows); - return $result; - } - - public function testNoUnusedIndexesPrintsSuccessMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult([])); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('No unused indexes found', $output->fetch()); - } - - public function testMySQLUnusedIndexesRendersTable(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLRows())); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $content = $output->fetch(); - $this->assertStringContainsString('Reads', $content); - $this->assertStringContainsString('Writes', $content); - $this->assertStringContainsString('idx_fc_name', $content); - $this->assertStringContainsString('Found 2 unused index(es)', $content); - } - - public function testPostgreSQLUnusedIndexesRendersTable(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(PostgreSQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockPostgreSQLRows())); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $content = $output->fetch(); - $this->assertStringContainsString('Tuples Read', $content); - $this->assertStringContainsString('Tuples Fetched', $content); - } - - public function testAllFlagSuppressesCountMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLRows())); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', true]]); - - $output = new BufferedOutput(); - self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertStringNotContainsString('Found', $output->fetch()); - } - - public function testDefaultFilterIncludedInQuery(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->expects($this->once()) - ->method('executeQuery') - ->with($this->stringContains('count_read = 0')) - ->willReturn($this->mockResult([])); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]); - - self::invokePrivate($this->command, 'execute', [$this->input, new BufferedOutput()]); - } - - public function testAllFlagRemovesFilterFromQuery(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->expects($this->once()) - ->method('executeQuery') - ->with($this->logicalNot($this->stringContains('count_read = 0'))) - ->willReturn($this->mockResult([])); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', true]]); - - self::invokePrivate($this->command, 'execute', [$this->input, new BufferedOutput()]); - } - - public function testJsonOutputWhenRowsExist(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLRows())); - $this->input->method('getOption')->willReturnMap([['json', true], ['all', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $data = json_decode($output->fetch(), true); - $this->assertIsArray($data); - $this->assertCount(2, $data); - $this->assertArrayHasKey('table', $data[0]); - $this->assertArrayHasKey('index', $data[0]); - } - - public function testSQLiteReturnsSuccessWithMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(SqlitePlatform::class)); - $this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('not supported for SQLite', $output->fetch()); - } -} --- a/tests/Core/Command/Db/DbInfoTest.php +++ /dev/null @@ -1,166 +0,0 @@ -connection = $this->createMock(Connection::class); - $this->input = $this->createMock(InputInterface::class); - $this->command = new DbInfo($this->connection); - } - - private function mockMySQLResult(array $overrides = []): Result&MockObject { - $result = $this->createMock(Result::class); - $result->method('fetchAssociative')->willReturn(array_merge([ - 'version' => '8.0.30', - 'buffer_pool' => 1073741824, // 1 GB - 'max_conn' => '200', - 'charset' => 'utf8mb4', - 'tx_isolation' => 'READ-COMMITTED', - ], $overrides)); - return $result; - } - - private function mockPostgreSQLResult(): Result&MockObject { - $result = $this->createMock(Result::class); - $result->method('fetchAssociative')->willReturn([ - 'version' => 'PostgreSQL 15.2 on x86_64', - 'max_conn' => '100', - 'shared_buffers' => '128MB', - 'work_mem' => '4MB', - ]); - return $result; - } - - public function testMySQLTableOutput(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockMySQLResult()); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $content = $output->fetch(); - $this->assertStringContainsString('Setting', $content); - $this->assertStringContainsString('MySQL/MariaDB', $content); - } - - public function testPostgreSQLTableOutput(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(PostgreSQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockPostgreSQLResult()); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('PostgreSQL', $output->fetch()); - } - - public function testSQLiteTableOutput(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(SqlitePlatform::class)); - $result = $this->createMock(Result::class); - $result->method('fetchAssociative')->willReturn(['version' => '3.43.0']); - $this->connection->method('executeQuery')->willReturn($result); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('SQLite', $output->fetch()); - } - - public function testUnsupportedPlatformReturnsFailure(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(AbstractPlatform::class)); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(1, $exit); - $this->assertStringContainsString('Unsupported', $output->fetch()); - } - - public function testJsonOutputContainsSettingKeys(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockMySQLResult()); - $this->input->method('getOption')->willReturnMap([['json', true]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $data = json_decode($output->fetch(), true); - $this->assertIsArray($data); - $this->assertArrayHasKey('setting', $data[0]); - $this->assertArrayHasKey('value', $data[0]); - } - - public static function dataMySQLHealthChecks(): array { - return [ - 'charset utf8mb4 → OK' => ['charset', 'utf8mb4', true, 'Character Set'], - 'charset latin1 → CHECK' => ['charset', 'latin1', false, 'Character Set'], - 'max_conn 200 → OK' => ['max_conn', '200', true, 'Max Connections'], - 'max_conn 50 → CHECK' => ['max_conn', '50', false, 'Max Connections'], - 'tx_isolation READ-COMMITTED → OK' => ['tx_isolation', 'READ-COMMITTED', true, 'Transaction Isolation'], - 'tx_isolation REPEATABLE-READ → CHECK' => ['tx_isolation', 'REPEATABLE-READ', false, 'Transaction Isolation'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataMySQLHealthChecks')] - public function testMySQLHealthCheckStatus( - string $field, - string $value, - bool $expectedOk, - string $settingLabel, - ): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockMySQLResult([$field => $value])); - $this->input->method('getOption')->willReturnMap([['json', true]]); - - $output = new BufferedOutput(); - self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $data = json_decode($output->fetch(), true); - $rows = array_values(array_filter($data, fn ($r) => $r['setting'] === $settingLabel)); - $this->assertNotEmpty($rows, "Setting '{$settingLabel}' not found in JSON output"); - $this->assertSame($expectedOk, $rows[0]['ok']); - } -} --- a/tests/Core/Command/Db/DbLocksTest.php +++ /dev/null @@ -1,161 +0,0 @@ -connection = $this->createMock(Connection::class); - $this->input = $this->createMock(InputInterface::class); - $this->command = new DbLocks($this->connection); - } - - private function mockMySQLLocks(): array { - return [[ - 'waiting_trx_id' => '12345', - 'waiting_thread' => '42', - 'waiting_query' => 'UPDATE oc_filecache SET path_hash = ?', - 'blocking_trx_id' => '12344', - 'blocking_thread' => '41', - 'blocking_query' => null, // NULL — deve ser renderizado como '—' - ]]; - } - - private function mockPostgreSQLLocks(): array { - return [[ - 'blocked_pid' => 1234, - 'blocked_user' => 'nextcloud', - 'blocking_pid' => 1233, - 'blocking_user' => 'nextcloud', - 'blocked_query' => 'SELECT * FROM oc_filecache WHERE parent = ?', - 'blocked_duration' => '00:00:05.123456', - ]]; - } - - private function mockResult(array $rows): Result&MockObject { - $result = $this->createMock(Result::class); - $result->method('fetchAllAssociative')->willReturn($rows); - return $result; - } - - public function testMySQLNoLocksShowsInfoMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult([])); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('No active locks', $output->fetch()); - } - - public function testPostgreSQLNoLocksShowsInfoMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(PostgreSQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult([])); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('No active locks', $output->fetch()); - } - - public function testMySQLLocksFoundShowsErrorMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLLocks())); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('Found 1 blocking transaction(s)', $output->fetch()); - } - - public function testPostgreSQLLocksFoundShowsErrorMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(PostgreSQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockPostgreSQLLocks())); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('Found 1 blocking transaction(s)', $output->fetch()); - } - - public function testJsonOutputWhenLocksExist(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLLocks())); - $this->input->method('getOption')->willReturnMap([['json', true]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $data = json_decode($output->fetch(), true); - $this->assertIsArray($data); - $this->assertCount(1, $data); - $this->assertArrayHasKey('waiting_trx_id', $data[0]); - } - - public function testSQLiteReturnsSuccessWithMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(SqlitePlatform::class)); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('file-level locking', $output->fetch()); - } - - public function testNullColumnRenderedAsDash(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockMySQLLocks())); // blocking_query = null - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertStringContainsString('—', $output->fetch()); - } -} --- a/tests/Core/Command/Db/DbSizeTest.php +++ /dev/null @@ -1,122 +0,0 @@ -connection = $this->createMock(Connection::class); - $this->input = $this->createMock(InputInterface::class); - $this->command = new DbSize($this->connection); - } - - private function mockRows(): array { - return [ - ['table' => 'oc_filecache', 'total_mb' => 12.50, 'data_mb' => 10.00, 'index_mb' => 2.50, 'rows' => 5000, 'avg_row_bytes' => 2560], - ['table' => 'oc_share', 'total_mb' => 3.25, 'data_mb' => 2.00, 'index_mb' => 1.25, 'rows' => 200, 'avg_row_bytes' => 16384], - ]; - } - - private function mockResult(array $rows): Result&MockObject { - $result = $this->createMock(Result::class); - $result->method('fetchAllAssociative')->willReturn($rows); - return $result; - } - - public function testMySQLOutputContainsTableAndTotal(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockRows())); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $content = $output->fetch(); - $this->assertStringContainsString('oc_filecache', $content); - $this->assertStringContainsString('Total database size', $content); - } - - public function testPostgreSQLOutputContainsTableAndTotal(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(PostgreSQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockRows())); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('Total database size', $output->fetch()); - } - - public function testSQLiteReturnsSuccessWithMessage(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(SqlitePlatform::class)); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $this->assertStringContainsString('not supported for SQLite', $output->fetch()); - } - - public function testJsonOutputIsValidArray(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockRows())); - $this->input->method('getOption')->willReturnMap([['json', true]]); - - $output = new BufferedOutput(); - $exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - $this->assertSame(0, $exit); - $data = json_decode($output->fetch(), true); - $this->assertIsArray($data); - $this->assertCount(2, $data); - $this->assertArrayHasKey('table', $data[0]); - $this->assertArrayHasKey('total_mb', $data[0]); - } - - public function testTotalSizeCalculation(): void { - $this->connection->method('getDatabasePlatform') - ->willReturn($this->createMock(MySQLPlatform::class)); - $this->connection->method('executeQuery') - ->willReturn($this->mockResult($this->mockRows())); - $this->input->method('getOption')->willReturnMap([['json', false]]); - - $output = new BufferedOutput(); - self::invokePrivate($this->command, 'execute', [$this->input, $output]); - - // 12.50 + 3.25 = 15.75 - $this->assertStringContainsString('15.75', $output->fetch()); - } -} --- a/tests/Core/Command/Encryption/ChangeKeyStorageRootTest.php +++ /dev/null @@ -1,358 +0,0 @@ -view = $this->getMockBuilder(View::class)->getMock(); - $this->userManager = $this->getMockBuilder(IUserManager::class)->getMock(); - $this->config = $this->getMockBuilder(IConfig::class)->getMock(); - $this->util = $this->getMockBuilder('OC\Encryption\Util')->disableOriginalConstructor()->getMock(); - $this->questionHelper = $this->getMockBuilder(QuestionHelper::class)->getMock(); - $this->inputInterface = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->outputInterface = $this->getMockBuilder(OutputInterface::class)->getMock(); - $this->userInterface = $this->getMockBuilder(UserInterface::class)->getMock(); - - /* We need format method to return a string */ - $outputFormatter = $this->createMock(OutputFormatterInterface::class); - $outputFormatter->method('isDecorated')->willReturn(false); - $outputFormatter->method('format')->willReturnArgument(0); - - $this->outputInterface->expects($this->any())->method('getFormatter') - ->willReturn($outputFormatter); - - $this->changeKeyStorageRoot = new ChangeKeyStorageRoot( - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')] - public function testExecute($newRoot, $answer, $successMoveKey): void { - $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') - ->setConstructorArgs( - [ - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ] - )->onlyMethods(['moveAllKeys'])->getMock(); - - $this->util->expects($this->once())->method('getKeyStorageRoot') - ->willReturn(''); - $this->inputInterface->expects($this->once())->method('getArgument') - ->with('newRoot')->willReturn($newRoot); - - if ($answer === true || $newRoot !== null) { - $changeKeyStorageRoot->expects($this->once())->method('moveAllKeys') - ->willReturn($successMoveKey); - } else { - $changeKeyStorageRoot->expects($this->never())->method('moveAllKeys'); - } - - if ($successMoveKey === true) { - $this->util->expects($this->once())->method('setKeyStorageRoot'); - } else { - $this->util->expects($this->never())->method('setKeyStorageRoot'); - } - - if ($newRoot === null) { - $this->questionHelper->expects($this->once())->method('ask')->willReturn($answer); - } else { - $this->questionHelper->expects($this->never())->method('ask'); - } - - $this->invokePrivate( - $changeKeyStorageRoot, - 'execute', - [$this->inputInterface, $this->outputInterface] - ); - } - - public static function dataTestExecute(): array { - return [ - [null, true, true], - [null, true, false], - [null, false, null], - ['/newRoot', null, true], - ['/newRoot', null, false] - ]; - } - - public function testMoveAllKeys(): void { - /** @var ChangeKeyStorageRoot $changeKeyStorageRoot */ - $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') - ->setConstructorArgs( - [ - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ] - )->onlyMethods(['prepareNewRoot', 'moveSystemKeys', 'moveUserKeys'])->getMock(); - - $changeKeyStorageRoot->expects($this->once())->method('prepareNewRoot')->with('newRoot'); - $changeKeyStorageRoot->expects($this->once())->method('moveSystemKeys')->with('oldRoot', 'newRoot'); - $changeKeyStorageRoot->expects($this->once())->method('moveUserKeys')->with('oldRoot', 'newRoot', $this->outputInterface); - - $this->invokePrivate($changeKeyStorageRoot, 'moveAllKeys', ['oldRoot', 'newRoot', $this->outputInterface]); - } - - public function testPrepareNewRoot(): void { - $this->view->expects($this->once())->method('is_dir')->with('newRoot') - ->willReturn(true); - - $this->view->expects($this->once())->method('file_put_contents') - ->with('newRoot/' . Storage::KEY_STORAGE_MARKER, - 'Nextcloud will detect this folder as key storage root only if this file exists')->willReturn(true); - - $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']); - } - - /** - * - * @param bool $dirExists - * @param bool $couldCreateFile - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPrepareNewRootException')] - public function testPrepareNewRootException($dirExists, $couldCreateFile): void { - $this->expectException(\Exception::class); - - $this->view->expects($this->once())->method('is_dir')->with('newRoot') - ->willReturn($dirExists); - $this->view->expects($this->any())->method('file_put_contents')->willReturn($couldCreateFile); - - $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']); - } - - public static function dataTestPrepareNewRootException(): array { - return [ - [true, false], - [true, null], - [false, true] - ]; - } - - /** - * - * @param bool $dirExists - * @param bool $targetExists - * @param bool $executeRename - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveSystemKeys')] - public function testMoveSystemKeys($dirExists, $targetExists, $executeRename): void { - $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') - ->setConstructorArgs( - [ - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ] - )->onlyMethods(['targetExists'])->getMock(); - - $this->view->expects($this->once())->method('is_dir') - ->with('oldRoot/files_encryption')->willReturn($dirExists); - $changeKeyStorageRoot->expects($this->any())->method('targetExists') - ->with('newRoot/files_encryption')->willReturn($targetExists); - - if ($executeRename) { - $this->view->expects($this->once())->method('rename') - ->with('oldRoot/files_encryption', 'newRoot/files_encryption'); - } else { - $this->view->expects($this->never())->method('rename'); - } - - $this->invokePrivate($changeKeyStorageRoot, 'moveSystemKeys', ['oldRoot', 'newRoot']); - } - - public static function dataTestMoveSystemKeys(): array { - return [ - [true, false, true], - [false, true, false], - [true, true, false], - [false, false, false] - ]; - } - - public function testMoveUserKeys(): void { - $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') - ->setConstructorArgs( - [ - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ] - )->onlyMethods(['setupUserFS', 'moveUserEncryptionFolder'])->getMock(); - - $this->userManager->expects($this->once())->method('getBackends') - ->willReturn([$this->userInterface]); - $this->userInterface->expects($this->once())->method('getUsers') - ->willReturn(['user1', 'user2']); - $changeKeyStorageRoot->expects($this->exactly(2))->method('setupUserFS'); - $changeKeyStorageRoot->expects($this->exactly(2))->method('moveUserEncryptionFolder'); - - $this->invokePrivate($changeKeyStorageRoot, 'moveUserKeys', ['oldRoot', 'newRoot', $this->outputInterface]); - } - - /** - * - * @param bool $userExists - * @param bool $isDir - * @param bool $targetExists - * @param bool $shouldRename - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveUserEncryptionFolder')] - public function testMoveUserEncryptionFolder($userExists, $isDir, $targetExists, $shouldRename): void { - $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot') - ->setConstructorArgs( - [ - $this->view, - $this->userManager, - $this->config, - $this->util, - $this->questionHelper - ] - )->onlyMethods(['targetExists', 'prepareParentFolder'])->getMock(); - - $this->userManager->expects($this->once())->method('userExists') - ->willReturn($userExists); - $this->view->expects($this->any())->method('is_dir') - ->willReturn($isDir); - $changeKeyStorageRoot->expects($this->any())->method('targetExists') - ->willReturn($targetExists); - - if ($shouldRename) { - $changeKeyStorageRoot->expects($this->once())->method('prepareParentFolder') - ->with('newRoot/user1'); - $this->view->expects($this->once())->method('rename') - ->with('oldRoot/user1/files_encryption', 'newRoot/user1/files_encryption'); - } else { - $changeKeyStorageRoot->expects($this->never())->method('prepareParentFolder'); - $this->view->expects($this->never())->method('rename'); - } - - $this->invokePrivate($changeKeyStorageRoot, 'moveUserEncryptionFolder', ['user1', 'oldRoot', 'newRoot']); - } - - public static function dataTestMoveUserEncryptionFolder(): array { - return [ - [true, true, false, true], - [true, false, true, false], - [false, true, true, false], - [false, false, true, false], - [false, true, false, false], - [false, true, true, false], - [false, false, false, false] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPrepareParentFolder')] - public function testPrepareParentFolder($path, $pathExists): void { - $this->view->expects($this->any())->method('file_exists') - ->willReturnCallback( - function ($fileExistsPath) use ($path, $pathExists) { - if ($path === $fileExistsPath) { - return $pathExists; - } - return false; - } - ); - - if ($pathExists === false) { - $subDirs = explode('/', ltrim($path, '/')); - $this->view->expects($this->exactly(count($subDirs)))->method('mkdir'); - } else { - $this->view->expects($this->never())->method('mkdir'); - } - - $this->invokePrivate( - $this->changeKeyStorageRoot, - 'prepareParentFolder', - [$path] - ); - } - - public static function dataTestPrepareParentFolder(): array { - return [ - ['/user/folder/sub_folder/keystorage', true], - ['/user/folder/sub_folder/keystorage', false] - ]; - } - - public function testTargetExists(): void { - $this->view->expects($this->once())->method('file_exists')->with('path') - ->willReturn(false); - - $this->assertFalse( - $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path']) - ); - } - - public function testTargetExistsException(): void { - $this->expectException(\Exception::class); - - $this->view->expects($this->once())->method('file_exists')->with('path') - ->willReturn(true); - - $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path']); - } -} --- a/tests/Core/Command/Encryption/DecryptAllTest.php +++ /dev/null @@ -1,199 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->questionHelper = $this->createMock(QuestionHelper::class); - $this->decryptAll = $this->createMock(\OC\Encryption\DecryptAll::class); - - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleInput->expects($this->any()) - ->method('isInteractive') - ->willReturn(true); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - $this->config->expects($this->any()) - ->method('getSystemValue') - ->with('maintenance', false) - ->willReturn(false); - $this->appManager->expects($this->any()) - ->method('isEnabledForUser') - ->with('files_trashbin')->willReturn(true); - } - - public function testMaintenanceAndTrashbin(): void { - // on construct we enable single-user-mode and disable the trash bin - // on destruct we disable single-user-mode again and enable the trash bin - $calls = [ - ['maintenance', true], - ['maintenance', false], - ]; - $this->config->expects($this->exactly(2)) - ->method('setSystemValue') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - $this->appManager->expects($this->once()) - ->method('disableApp') - ->with('files_trashbin'); - $this->appManager->expects($this->once()) - ->method('enableApp') - ->with('files_trashbin'); - - $instance = new DecryptAll( - $this->appManager, - $this->config, - $this->appConfig, - $this->decryptAll, - $this->questionHelper - ); - $this->invokePrivate($instance, 'forceMaintenanceAndTrashbin'); - - $this->assertTrue( - $this->invokePrivate($instance, 'wasTrashbinEnabled') - ); - - $this->assertFalse( - $this->invokePrivate($instance, 'wasMaintenanceModeEnabled') - ); - $this->invokePrivate($instance, 'resetMaintenanceAndTrashbin'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')] - public function testExecute($encryptionEnabled, $continue): void { - $instance = new DecryptAll( - $this->appManager, - $this->config, - $this->appConfig, - $this->decryptAll, - $this->questionHelper - ); - - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with('core', 'encryption_enabled', false) - ->willReturn($encryptionEnabled); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->with('user') - ->willReturn('user1'); - - if ($encryptionEnabled) { - $calls = [ - ['core', 'encryption_enabled', false, false], - ['core', 'encryption_enabled', true, false], - ]; - $this->appConfig->expects($this->exactly(count($calls))) - ->method('setValueBool') - ->willReturnCallback(function () use (&$calls): bool { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - return true; - }); - } else { - $this->appConfig->expects($this->never()) - ->method('setValueBool'); - } - $this->questionHelper->expects($this->once()) - ->method('ask') - ->willReturn($continue); - if ($continue) { - $this->decryptAll->expects($this->once()) - ->method('decryptAll') - ->with($this->consoleInput, $this->consoleOutput, 'user1'); - } else { - $this->decryptAll->expects($this->never())->method('decryptAll'); - } - - $this->invokePrivate($instance, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public static function dataTestExecute(): array { - return [ - [true, true], - [true, false], - [false, true], - [false, false] - ]; - } - - public function testExecuteFailure(): void { - $this->expectException(\Exception::class); - - $instance = new DecryptAll( - $this->appManager, - $this->config, - $this->appConfig, - $this->decryptAll, - $this->questionHelper - ); - - // make sure that we enable encryption again after a exception was thrown - $calls = [ - ['core', 'encryption_enabled', false, false], - ['core', 'encryption_enabled', true, false], - ]; - $this->appConfig->expects($this->exactly(2)) - ->method('setValueBool') - ->willReturnCallback(function () use (&$calls): bool { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - return true; - }); - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with('core', 'encryption_enabled', false) - ->willReturn(true); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->with('user') - ->willReturn('user1'); - - $this->questionHelper->expects($this->once()) - ->method('ask') - ->willReturn(true); - - $this->decryptAll->expects($this->once()) - ->method('decryptAll') - ->with($this->consoleInput, $this->consoleOutput, 'user1') - ->willReturnCallback(function (): void { - throw new \Exception(); - }); - - $this->invokePrivate($instance, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Encryption/DisableTest.php +++ /dev/null @@ -1,74 +0,0 @@ -appConfig = $this->getMockBuilder(IAppConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var IAppConfig $appConfig */ - $this->command = new Disable($appConfig); - } - - public static function dataDisable(): array { - return [ - [true, true, 'Encryption disabled'], - [false, false, 'Encryption is already disabled'], - ]; - } - - /** - * - * @param bool $oldStatus - * @param bool $isUpdating - * @param string $expectedString - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDisable')] - public function testDisable(bool $oldStatus, bool $isUpdating, string $expectedString): void { - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with('core', 'encryption_enabled', false) - ->willReturn($oldStatus); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains($expectedString)); - - if ($isUpdating) { - $this->appConfig->expects($this->once()) - ->method('setValueBool') - ->with('core', 'encryption_enabled', false); - } - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Encryption/EnableTest.php +++ /dev/null @@ -1,98 +0,0 @@ -appConfig = $this->getMockBuilder(IAppConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $manager = $this->manager = $this->getMockBuilder(IManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var \OCP\IAppConfig $appConfig */ - /** @var \OCP\Encryption\IManager $manager */ - $this->command = new Enable($appConfig, $manager); - } - - public static function dataEnable(): array { - return [ - [false, '', [], true, 'Encryption enabled', 'No encryption module is loaded'], - [true, '', [], false, 'Encryption is already enabled', 'No encryption module is loaded'], - [false, '', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'], - [false, 'OC_NO_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'The current default module does not exist: OC_NO_MODULE'], - [false, 'OC_TEST_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'Default module: OC_TEST_MODULE'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataEnable')] - public function testEnable(bool $oldStatus, ?string $defaultModule, array $availableModules, bool $isUpdating, string $expectedString, string $expectedDefaultModuleString): void { - if ($isUpdating) { - $this->appConfig->expects($this->once()) - ->method('setValueBool') - ->with('core', 'encryption_enabled', true); - } - - $this->manager->expects($this->atLeastOnce()) - ->method('getEncryptionModules') - ->willReturn($availableModules); - - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with('core', 'encryption_enabled', false) - ->willReturn($oldStatus); - - if (!empty($availableModules)) { - $this->appConfig->expects($this->once()) - ->method('getValueString') - ->with('core', 'default_encryption_module', '') - ->willReturn((string)$defaultModule); - } - - $calls = [ - [$expectedString, 0], - ['', 0], - [$expectedDefaultModuleString, 0], - ]; - $this->consoleOutput->expects($this->exactly(3)) - ->method('writeln') - ->willReturnCallback(function (string $message, int $level) use (&$calls): void { - $call = array_shift($calls); - $this->assertStringContainsString($call[0], $message); - $this->assertSame($call[1], $level); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Encryption/EncryptAllTest.php +++ /dev/null @@ -1,94 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->encryptionManager = $this->createMock(IManager::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->encryptionModule = $this->createMock(IEncryptionModule::class); - $this->questionHelper = $this->createMock(QuestionHelper::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleInput->expects($this->any()) - ->method('isInteractive') - ->willReturn(true); - $this->consoleOutput = $this->createMock(OutputInterface::class); - } - - public function testEncryptAll(): void { - // trash bin needs to be disabled in order to avoid adding dummy files to the users - // trash bin which gets deleted during the encryption process - $this->appManager->expects($this->once())->method('disableApp')->with('files_trashbin'); - - $instance = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper); - $this->invokePrivate($instance, 'forceMaintenanceAndTrashbin'); - $this->invokePrivate($instance, 'resetMaintenanceAndTrashbin'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')] - public function testExecute($answer, $askResult): void { - $command = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper); - - $this->encryptionManager->expects($this->once())->method('isEnabled')->willReturn(true); - $this->questionHelper->expects($this->once())->method('ask')->willReturn($askResult); - - if ($answer === 'Y' || $answer === 'y') { - $this->encryptionManager->expects($this->once()) - ->method('getEncryptionModule')->willReturn($this->encryptionModule); - $this->encryptionModule->expects($this->once()) - ->method('encryptAll')->with($this->consoleInput, $this->consoleOutput); - } else { - $this->encryptionManager->expects($this->never())->method('getEncryptionModule'); - $this->encryptionModule->expects($this->never())->method('encryptAll'); - } - - $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public static function dataTestExecute(): array { - return [ - ['y', true], ['Y', true], ['n', false], ['N', false], ['', false] - ]; - } - - public function testExecuteException(): void { - $this->expectException(\Exception::class); - - $command = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper); - $this->encryptionManager->expects($this->once())->method('isEnabled')->willReturn(false); - $this->encryptionManager->expects($this->never())->method('getEncryptionModule'); - $this->encryptionModule->expects($this->never())->method('encryptAll'); - $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Encryption/SetDefaultModuleTest.php +++ /dev/null @@ -1,130 +0,0 @@ -manager = $this->getMockBuilder(IManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->config = $this->getMockBuilder(IConfig::class) - ->getMock(); - - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - $this->command = new SetDefaultModule($this->manager, $this->config); - } - - public static function dataSetDefaultModule(): array { - return [ - ['ID0', 'ID0', null, null, 'already'], - ['ID0', 'ID1', 'ID1', true, 'info'], - ['ID0', 'ID1', 'ID1', false, 'error'], - ]; - } - - /** - * - * @param string $oldModule - * @param string $newModule - * @param string $updateModule - * @param bool $updateSuccess - * @param string $expectedString - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultModule')] - public function testSetDefaultModule($oldModule, $newModule, $updateModule, $updateSuccess, $expectedString): void { - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('module') - ->willReturn($newModule); - - $this->manager->expects($this->once()) - ->method('getDefaultEncryptionModuleId') - ->willReturn($oldModule); - - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with('maintenance', false) - ->willReturn(false); - - if ($updateModule) { - $this->manager->expects($this->once()) - ->method('setDefaultEncryptionModule') - ->with($updateModule) - ->willReturn($updateSuccess); - } - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains($expectedString)); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - /** - * - * @param string $oldModule - * @param string $newModule - * @param string $updateModule - * @param bool $updateSuccess - * @param string $expectedString - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultModule')] - public function testMaintenanceMode($oldModule, $newModule, $updateModule, $updateSuccess, $expectedString): void { - $this->consoleInput->expects($this->never()) - ->method('getArgument') - ->with('module') - ->willReturn($newModule); - - $this->manager->expects($this->never()) - ->method('getDefaultEncryptionModuleId') - ->willReturn($oldModule); - - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with('maintenance', false) - ->willReturn(true); - - $calls = [ - 'Maintenance mode must be disabled when setting default module,', - 'in order to load the relevant encryption modules correctly.', - ]; - $this->consoleOutput->expects($this->exactly(2)) - ->method('writeln') - ->willReturnCallback(function ($message) use (&$calls): void { - $expected = array_shift($calls); - $this->assertStringContainsString($expected, $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Group/AddTest.php +++ /dev/null @@ -1,81 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->command = new Add($this->groupManager); - - $this->input = $this->createMock(InputInterface::class); - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) { - if ($arg === 'groupid') { - return 'myGroup'; - } - throw new \Exception(); - }); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testGroupExists(): void { - $gid = 'myGroup'; - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with($gid) - ->willReturn($group); - - $this->groupManager->expects($this->never()) - ->method('createGroup'); - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" already exists.')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAdd(): void { - $gid = 'myGroup'; - $group = $this->createMock(IGroup::class); - $group->method('getGID') - ->willReturn($gid); - $this->groupManager->method('createGroup') - ->willReturn($group); - - $this->groupManager->expects($this->once()) - ->method('createGroup') - ->with($this->equalTo($gid)); - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Created group "' . $group->getGID() . '"')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Group/AddUserTest.php +++ /dev/null @@ -1,173 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->command = new AddUser($this->userManager, $this->groupManager); - $this->output = $this->createMock(OutputInterface::class); - } - - protected function configureInput(array|string $returnGroup, array|string $returnUser): void { - $this->input = $this->createMock(InputInterface::class); - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($returnGroup, $returnUser) { - if ($arg === 'group') { - return $returnGroup; - } - if ($arg === 'user') { - return $returnUser; - } - throw new \Exception(); - }); - } - - public function testNoGroup(): void { - $this->configureInput('myGroup', 'myUser'); - - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn(null); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('group not found'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testNoUser(): void { - $this->configureInput('myGroup', 'myUser'); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $this->userManager->method('get') - ->with('myUser') - ->willReturn(null); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('user myUser not found'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAdd(): void { - $this->configureInput('myGroup', 'myUser'); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user = $this->createMock(IUser::class); - $this->userManager->method('get') - ->with('myUser') - ->willReturn($user); - - $group->expects($this->once()) - ->method('addUser') - ->with($user); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAddMultiple(): void { - $this->configureInput('myGroup', ['myUser', 'myOtherUser']); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $this->userManager->method('get') - ->willReturnMap([ - ['myUser', $user1], - ['myOtherUser', $user2], - ]); - - $group->expects($this->exactly(2)) - ->method('addUser') - ->with($this->callback(static fn (IUser $user): bool => in_array($user, [$user1, $user2], true))); - - $this->output->expects($this->exactly(2)) - ->method('writeln') - ->with($this->callback(static fn (string $message): bool => in_array($message, - [ - 'user myUser added', - 'user myOtherUser added', - ], true))); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAddMultiplePartialSuccess(): void { - $this->configureInput('myGroup', ['myUser', 'myOtherUser']); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user = $this->createMock(IUser::class); - $this->userManager->method('get') - ->willReturnMap([ - ['myUser', $user], - ['myOtherUser', null], - ]); - - $group->expects($this->once()) - ->method('addUser') - ->with($user); - - $this->output->expects($this->exactly(3)) - ->method('writeln') - ->with($this->callback(static fn (string $message): bool => in_array($message, - [ - 'user myUser added', - 'user myOtherUser not found', - 'Some users were not found, all others where added to the group.', - ], true))); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Group/DeleteTest.php +++ /dev/null @@ -1,133 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->command = new Delete($this->groupManager); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testDoesNotExists(): void { - $gid = 'myGroup'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - $this->groupManager->method('groupExists') - ->with($gid) - ->willReturn(false); - - $this->groupManager->expects($this->never()) - ->method('get'); - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" does not exist.')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testDeleteAdmin(): void { - $gid = 'admin'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - - $this->groupManager->expects($this->never()) - ->method($this->anything()); - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" could not be deleted.')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testDeleteFailed(): void { - $gid = 'myGroup'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - $group = $this->createMock(IGroup::class); - $group->method('delete') - ->willReturn(false); - $this->groupManager->method('groupExists') - ->with($gid) - ->willReturn(true); - $this->groupManager->method('get') - ->with($gid) - ->willReturn($group); - - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" could not be deleted. Please check the logs.')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testDelete(): void { - $gid = 'myGroup'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - $group = $this->createMock(IGroup::class); - $group->method('delete') - ->willReturn(true); - $this->groupManager->method('groupExists') - ->with($gid) - ->willReturn(true); - $this->groupManager->method('get') - ->with($gid) - ->willReturn($group); - - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" was removed')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Group/InfoTest.php +++ /dev/null @@ -1,99 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->command = $this->getMockBuilder(Info::class) - ->setConstructorArgs([$this->groupManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testDoesNotExists(): void { - $gid = 'myGroup'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - $this->groupManager->method('get') - ->with($gid) - ->willReturn(null); - - $this->output->expects($this->once()) - ->method('writeln') - ->with($this->equalTo('Group "' . $gid . '" does not exist.')); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testInfo(): void { - $gid = 'myGroup'; - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($gid) { - if ($arg === 'groupid') { - return $gid; - } - throw new \Exception(); - }); - - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn($gid); - $group->method('getDisplayName') - ->willReturn('My Group'); - $group->method('getBackendNames') - ->willReturn(['Database']); - - $this->groupManager->method('get') - ->with($gid) - ->willReturn($group); - - $this->command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with( - $this->equalTo($this->input), - $this->equalTo($this->output), - [ - 'groupID' => 'myGroup', - 'displayName' => 'My Group', - 'backends' => ['Database'], - ] - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Group/ListCommandTest.php +++ /dev/null @@ -1,205 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->command = $this->getMockBuilder(ListCommand::class) - ->setConstructorArgs([$this->groupManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testExecute(): void { - $group1 = $this->createMock(IGroup::class); - $group1->method('getGID')->willReturn('group1'); - $group2 = $this->createMock(IGroup::class); - $group2->method('getGID')->willReturn('group2'); - $group3 = $this->createMock(IGroup::class); - $group3->method('getGID')->willReturn('group3'); - - $user = $this->createMock(IUser::class); - - $this->groupManager->method('search') - ->with( - '', - 100, - 42, - )->willReturn([$group1, $group2, $group3]); - - $group1->method('getUsers') - ->willReturn([ - 'user1' => $user, - 'user2' => $user, - ]); - - $group2->method('getUsers') - ->willReturn([ - ]); - - $group3->method('getUsers') - ->willReturn([ - 'user1' => $user, - 'user3' => $user, - ]); - - $this->input->method('getOption') - ->willReturnCallback(function ($arg) { - if ($arg === 'limit') { - return '100'; - } elseif ($arg === 'offset') { - return '42'; - } elseif ($arg === 'info') { - return null; - } - throw new \Exception(); - }); - - $this->command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with( - $this->equalTo($this->input), - $this->equalTo($this->output), - $this->callback( - fn ($iterator) => iterator_to_array($iterator) === [ - 'group1' => [ - 'user1', - 'user2', - ], - 'group2' => [ - ], - 'group3' => [ - 'user1', - 'user3', - ] - ] - ) - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testInfo(): void { - $group1 = $this->createMock(IGroup::class); - $group1->method('getGID')->willReturn('group1'); - $group1->method('getDisplayName')->willReturn('Group 1'); - $group2 = $this->createMock(IGroup::class); - $group2->method('getGID')->willReturn('group2'); - $group2->method('getDisplayName')->willReturn('Group 2'); - $group3 = $this->createMock(IGroup::class); - $group3->method('getGID')->willReturn('group3'); - $group3->method('getDisplayName')->willReturn('Group 3'); - - $user = $this->createMock(IUser::class); - - $this->groupManager->method('search') - ->with( - '', - 100, - 42, - )->willReturn([$group1, $group2, $group3]); - - $group1->method('getUsers') - ->willReturn([ - 'user1' => $user, - 'user2' => $user, - ]); - - $group1->method('getBackendNames') - ->willReturn(['Database']); - - $group2->method('getUsers') - ->willReturn([ - ]); - - $group2->method('getBackendNames') - ->willReturn(['Database']); - - $group3->method('getUsers') - ->willReturn([ - 'user1' => $user, - 'user3' => $user, - ]); - - $group3->method('getBackendNames') - ->willReturn(['LDAP']); - - $this->input->method('getOption') - ->willReturnCallback(function ($arg) { - if ($arg === 'limit') { - return '100'; - } elseif ($arg === 'offset') { - return '42'; - } elseif ($arg === 'info') { - return true; - } - throw new \Exception(); - }); - - $this->command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with( - $this->equalTo($this->input), - $this->equalTo($this->output), - $this->callback( - fn ($iterator) => iterator_to_array($iterator) === [ - 'group1' => [ - 'displayName' => 'Group 1', - 'backends' => ['Database'], - 'users' => [ - 'user1', - 'user2', - ], - ], - 'group2' => [ - 'displayName' => 'Group 2', - 'backends' => ['Database'], - 'users' => [], - ], - 'group3' => [ - 'displayName' => 'Group 3', - 'backends' => ['LDAP'], - 'users' => [ - 'user1', - 'user3', - ], - ] - ] - ) - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Group/RemoveUserTest.php +++ /dev/null @@ -1,173 +0,0 @@ -groupManager = $this->createMock(IGroupManager::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->command = new RemoveUser($this->userManager, $this->groupManager); - $this->output = $this->createMock(OutputInterface::class); - } - - protected function configureInput(array|string $returnGroup, array|string $returnUser): void { - $this->input = $this->createMock(InputInterface::class); - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($returnGroup, $returnUser) { - if ($arg === 'group') { - return $returnGroup; - } - if ($arg === 'user') { - return $returnUser; - } - throw new \Exception(); - }); - } - - public function testNoGroup(): void { - $this->configureInput('myGroup', 'myUser'); - - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn(null); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('group not found'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testNoUser(): void { - $this->configureInput('myGroup', 'myUser'); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $this->userManager->method('get') - ->with('myUser') - ->willReturn(null); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('user myUser not found'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testRemove(): void { - $this->configureInput('myGroup', 'myUser'); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user = $this->createMock(IUser::class); - $this->userManager->method('get') - ->with('myUser') - ->willReturn($user); - - $group->expects($this->once()) - ->method('removeUser') - ->with($user); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testRemoveMultiple(): void { - $this->configureInput('myGroup', ['myUser', 'myOtherUser']); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $this->userManager->method('get') - ->willReturnMap([ - ['myUser', $user1], - ['myOtherUser', $user2], - ]); - - $group->expects($this->exactly(2)) - ->method('removeUser') - ->with($this->callback(static fn (IUser $user): bool => in_array($user, [$user1, $user2], true))); - - $this->output->expects($this->exactly(2)) - ->method('writeln') - ->with($this->callback(static fn (string $message): bool => in_array($message, - [ - 'user myUser removed', - 'user myOtherUser removed', - ], true))); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testRemoveMultiplePartialSuccess(): void { - $this->configureInput('myGroup', ['myUser', 'myOtherUser']); - - $group = $this->createMock(IGroup::class); - $this->groupManager->method('get') - ->with('myGroup') - ->willReturn($group); - - $user = $this->createMock(IUser::class); - $this->userManager->method('get') - ->willReturnMap([ - ['myUser', $user], - ['myOtherUser', null], - ]); - - $group->expects($this->once()) - ->method('removeUser') - ->with($user); - - $this->output->expects($this->exactly(3)) - ->method('writeln') - ->with($this->callback(static fn (string $message): bool => in_array($message, - [ - 'user myUser removed', - 'user myOtherUser not found', - 'Some users were not found, all others where removed from the group.', - ], true))); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/Log/FileTest.php +++ /dev/null @@ -1,113 +0,0 @@ -config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - $this->command = new File($config); - } - - public function testEnable(): void { - $this->config->method('getSystemValue')->willReturnArgument(1); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['enable', 'true'] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('log_type', 'file'); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testChangeFile(): void { - $this->config->method('getSystemValue')->willReturnArgument(1); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['file', '/foo/bar/file.log'] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('logfile', '/foo/bar/file.log'); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public static function changeRotateSizeProvider(): array { - return [ - ['42', 42], - ['0', 0], - ['1 kB', 1024], - ['5MB', 5 * 1024 * 1024], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('changeRotateSizeProvider')] - public function testChangeRotateSize($optionValue, $configValue): void { - $this->config->method('getSystemValue')->willReturnArgument(1); - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['rotate-size', $optionValue] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('log_rotate_size', $configValue); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testGetConfiguration(): void { - $this->config->method('getSystemValue') - ->willReturnMap([ - ['log_type', 'file', 'log_type_value'], - ['datadirectory', \OC::$SERVERROOT . '/data', '/data/directory/'], - ['logfile', '/data/directory/nextcloud.log', '/var/log/nextcloud.log'], - ['log_rotate_size', 100 * 1024 * 1024, 5 * 1024 * 1024], - ]); - - $calls = [ - ['Log backend file: disabled'], - ['Log file: /var/log/nextcloud.log'], - ['Rotate at: 5 MB'], - ]; - $this->consoleOutput->expects($this->exactly(3)) - ->method('writeln') - ->willReturnCallback(function (string $message) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected[0], $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Log/ManageTest.php +++ /dev/null @@ -1,161 +0,0 @@ -config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - $this->command = new Manage($config); - } - - public function testChangeBackend(): void { - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['backend', 'syslog'] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('log_type', 'syslog'); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testChangeLevel(): void { - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['level', 'debug'] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('loglevel', 0); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testChangeTimezone(): void { - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['timezone', 'UTC'] - ]); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('logtimezone', 'UTC'); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testValidateBackend(): void { - $this->expectException(\InvalidArgumentException::class); - - self::invokePrivate($this->command, 'validateBackend', ['notabackend']); - } - - public function testValidateTimezone(): void { - $this->expectException(\Exception::class); - - // this might need to be changed when humanity colonises Mars - self::invokePrivate($this->command, 'validateTimezone', ['Mars/OlympusMons']); - } - - public static function dataConvertLevelString(): array { - return [ - ['dEbug', 0], - ['inFO', 1], - ['Warning', 2], - ['wArn', 2], - ['error', 3], - ['eRr', 3], - ['fAtAl', 4], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataConvertLevelString')] - public function testConvertLevelString(string $levelString, int $expectedInt): void { - $this->assertEquals($expectedInt, - self::invokePrivate($this->command, 'convertLevelString', [$levelString]) - ); - } - - public function testConvertLevelStringInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - - self::invokePrivate($this->command, 'convertLevelString', ['abc']); - } - - public static function dataConvertLevelNumber(): array { - return [ - [0, 'Debug'], - [1, 'Info'], - [2, 'Warning'], - [3, 'Error'], - [4, 'Fatal'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataConvertLevelNumber')] - public function testConvertLevelNumber(int $levelNum, string $expectedString): void { - $this->assertEquals($expectedString, - self::invokePrivate($this->command, 'convertLevelNumber', [$levelNum]) - ); - } - - public function testConvertLevelNumberInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - - self::invokePrivate($this->command, 'convertLevelNumber', [11]); - } - - public function testGetConfiguration(): void { - $this->config->expects($this->exactly(3)) - ->method('getSystemValue') - ->willReturnMap([ - ['log_type', 'file', 'log_type_value'], - ['loglevel', 2, 0], - ['logtimezone', 'UTC', 'logtimezone_value'], - ]); - - $calls = [ - ['Enabled logging backend: log_type_value'], - ['Log level: Debug (0)'], - ['Log timezone: logtimezone_value'], - ]; - $this->consoleOutput->expects($this->exactly(3)) - ->method('writeln') - ->willReturnCallback(function (string $message) use (&$calls): void { - $call = array_shift($calls); - $this->assertStringContainsString($call[0], $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Maintenance/DataFingerprintTest.php +++ /dev/null @@ -1,54 +0,0 @@ -config = $this->getMockBuilder(IConfig::class)->getMock(); - $this->timeFactory = $this->getMockBuilder(ITimeFactory::class)->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var IConfig $config */ - $this->command = new DataFingerprint($this->config, $this->timeFactory); - } - - public function testSetFingerPrint(): void { - $this->timeFactory->expects($this->once()) - ->method('getTime') - ->willReturn(42); - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('data-fingerprint', md5(42)); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Maintenance/InstallTest.php +++ /dev/null @@ -1,91 +0,0 @@ -command = new Install( - $this->createMock(SystemConfig::class), - $this->createMock(IniGetWrapper::class), - ); - } - - /** - * @param array $parameters - * @return array The installer options built from the command line input - */ - private function validateInput(array $parameters): array { - $input = new ArrayInput(array_merge([ - '--database-name' => 'nextcloud', - '--database-user' => 'admin', - '--database-pass' => 'admin-password', - '--admin-pass' => 'admin-password', - ], $parameters), $this->command->getDefinition()); - - return self::invokePrivate($this->command, 'validateInput', [$input, new NullOutput(), ['sqlite', 'mysql', 'pgsql', 'oci']]); - } - - public static function encryptionOptions(): array { - return [ - '--database-ssl-mode' => ['--database-ssl-mode', 'verify-full', 'dbsslmode', 'verify-full'], - '--database-ssl-ca' => ['--database-ssl-ca', '/ca.pem', 'dbsslca', '/ca.pem'], - '--database-ssl-cert' => ['--database-ssl-cert', '/client.crt', 'dbsslcert', '/client.crt'], - '--database-ssl-key' => ['--database-ssl-key', '/client.key', 'dbsslkey', '/client.key'], - '--database-ssl-crl' => ['--database-ssl-crl', '/crl.pem', 'dbsslcrl', '/crl.pem'], - '--database-ssl-no-verify' => ['--database-ssl-no-verify', true, 'dbsslnoverify', true], - ]; - } - - /** - * The command only forwards the options, the database setup translates them into the - * system config values and rejects the ones it does not support. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('encryptionOptions')] - public function testForwardsEncryptionOptions(string $parameter, string|bool $value, string $option, string|bool $expected): void { - $options = $this->validateInput([ - '--database' => 'pgsql', - $parameter => $value, - ]); - - $this->assertSame($expected, $options[$option]); - } - - public function testNoEncryptionOptions(): void { - $options = $this->validateInput(['--database' => 'mysql']); - - foreach (['dbsslmode', 'dbsslca', 'dbsslcert', 'dbsslkey', 'dbsslcrl', 'dbsslnoverify'] as $option) { - $this->assertArrayNotHasKey($option, $options); - } - } - - /** - * An option that does not apply to the chosen database is not filtered out here, it - * has to be reported by the database setup instead of being silently dropped. - */ - public function testForwardsEncryptionOptionsRegardlessOfDatabase(): void { - $options = $this->validateInput([ - '--database' => 'sqlite', - '--database-ssl-ca' => '/ca.pem', - ]); - - $this->assertSame('/ca.pem', $options['dbsslca']); - } -} --- a/tests/Core/Command/Maintenance/Mimetype/UpdateDBTest.php +++ /dev/null @@ -1,175 +0,0 @@ -detector = $this->createMock(Detection::class); - $this->loader = $this->createMock(Loader::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new UpdateDB($this->detector, $this->loader); - } - - public function testNoop(): void { - $this->consoleInput->method('getOption') - ->with('repair-filecache') - ->willReturn(false); - - $this->detector->expects($this->once()) - ->method('getAllMappings') - ->willReturn([ - 'ext' => ['testing/existingmimetype'] - ]); - $this->loader->expects($this->once()) - ->method('exists') - ->with('testing/existingmimetype') - ->willReturn(true); - - $this->loader->expects($this->never()) - ->method('updateFilecache'); - - $calls = [ - 'Added 0 new mimetypes', - 'Updated 0 filecache rows', - ]; - $this->consoleOutput->expects($this->exactly(2)) - ->method('writeln') - ->willReturnCallback(function ($message) use (&$calls): void { - $expected = array_shift($calls); - $this->assertStringContainsString($expected, $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testAddMimetype(): void { - $this->consoleInput->method('getOption') - ->with('repair-filecache') - ->willReturn(false); - - $this->detector->expects($this->once()) - ->method('getAllMappings') - ->willReturn([ - 'ext' => ['testing/existingmimetype'], - 'new' => ['testing/newmimetype'] - ]); - $this->loader->expects($this->exactly(2)) - ->method('exists') - ->willReturnMap([ - ['testing/existingmimetype', true], - ['testing/newmimetype', false], - ]); - $this->loader->expects($this->exactly(2)) - ->method('getId') - ->willReturnMap([ - ['testing/existingmimetype', 1], - ['testing/newmimetype', 2], - ]); - - $this->loader->expects($this->once()) - ->method('updateFilecache') - ->with('new', 2) - ->willReturn(3); - - $calls = [ - 'Added mimetype "testing/newmimetype" to database', - 'Updated 3 filecache rows for mimetype "testing/newmimetype"', - 'Added 1 new mimetypes', - 'Updated 3 filecache rows', - ]; - $this->consoleOutput->expects($this->exactly(4)) - ->method('writeln') - ->willReturnCallback(function ($message) use (&$calls): void { - $expected = array_shift($calls); - $this->assertStringContainsString($expected, $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testSkipComments(): void { - $this->detector->expects($this->once()) - ->method('getAllMappings') - ->willReturn([ - '_comment' => 'some comment in the JSON' - ]); - $this->loader->expects($this->never()) - ->method('exists'); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testRepairFilecache(): void { - $this->consoleInput->method('getOption') - ->with('repair-filecache') - ->willReturn(true); - - $this->detector->expects($this->once()) - ->method('getAllMappings') - ->willReturn([ - 'ext' => ['testing/existingmimetype'], - ]); - $this->loader->expects($this->exactly(1)) - ->method('exists') - ->willReturnMap([ - ['testing/existingmimetype', true], - ]); - $this->loader->expects($this->exactly(1)) - ->method('getId') - ->willReturnMap([ - ['testing/existingmimetype', 1], - ]); - - $this->loader->expects($this->once()) - ->method('updateFilecache') - ->with('ext', 1) - ->willReturn(3); - - $calls = [ - 'Updated 3 filecache rows for mimetype "testing/existingmimetype"', - 'Added 0 new mimetypes', - 'Updated 3 filecache rows', - ]; - $this->consoleOutput->expects($this->exactly(3)) - ->method('writeln') - ->willReturnCallback(function ($message) use (&$calls): void { - $expected = array_shift($calls); - $this->assertStringContainsString($expected, $message); - }); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Maintenance/ModeTest.php +++ /dev/null @@ -1,153 +0,0 @@ -config = $this->getMockBuilder(IConfig::class) - ->getMock(); - $this->mode = new Mode($this->config); - $this->input = $this->getMockBuilder(InputInterface::class) - ->getMock(); - $this->output = $this->getMockBuilder(OutputInterface::class) - ->getMock(); - } - - /** - * Provides test data for the execute test. - * - * @return array - */ - public static function getExecuteTestData(): array { - return [ - 'off -> on' => [ - 'on', // command option - false, // current maintenance mode state - true, // expected maintenance mode state, null for no change - 'Maintenance mode enabled', // expected output - ], - 'on -> off' => [ - 'off', - true, - false, - 'Maintenance mode disabled', - ], - 'on -> on' => [ - 'on', - true, - null, - 'Maintenance mode already enabled', - ], - 'off -> off' => [ - 'off', - false, - null, - 'Maintenance mode already disabled', - ], - 'no option, maintenance enabled' => [ - '', - true, - null, - 'Maintenance mode is currently enabled', - ], - 'no option, maintenance disabled' => [ - '', - false, - null, - 'Maintenance mode is currently disabled', - ], - ]; - } - - /** - * Asserts that execute works as expected. - * - * @param string $option The command option. - * @param bool $currentMaintenanceState The current maintenance state. - * @param null|bool $expectedMaintenanceState - * The expected maintenance state. Null for no change. - * @param string $expectedOutput The expected command output. - * @throws \Exception - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getExecuteTestData')] - public function testExecute( - string $option, - bool $currentMaintenanceState, - $expectedMaintenanceState, - string $expectedOutput, - ): void { - $this->config->expects($this->any()) - ->method('getSystemValueBool') - ->willReturn($currentMaintenanceState); - - if ($expectedMaintenanceState !== null) { - $this->config->expects($this->once()) - ->method('setSystemValue') - ->with('maintenance', $expectedMaintenanceState); - } - - $this->input->expects($this->any()) - ->method('getOption') - ->willReturnCallback(function ($callOption) use ($option) { - return $callOption === $option; - }); - - $this->output->expects($this->once()) - ->method('writeln') - ->with($expectedOutput); - - $this->mode->run($this->input, $this->output); - } -} --- a/tests/Core/Command/Maintenance/UpdateTheme.php +++ /dev/null @@ -1,63 +0,0 @@ -detector = $this->createMock(Detection::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - $this->command = new UpdateTheme($this->detector, $this->cacheFactory); - } - - public function testThemeUpdate(): void { - $this->consoleInput->method('getOption') - ->with('maintenance:theme:update') - ->willReturn(true); - $this->detector->expects($this->once()) - ->method('getAllAliases') - ->willReturn([]); - $cache = $this->createMock(ICache::class); - $cache->expects($this->once()) - ->method('clear') - ->with(''); - $this->cacheFactory->expects($this->once()) - ->method('createDistributed') - ->with('imagePath') - ->willReturn($cache); - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/Preview/CleanupTest.php +++ /dev/null @@ -1,156 +0,0 @@ -rootFolder = $this->createMock(IRootFolder::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->previewService = $this->createMock(PreviewService::class); - $this->repair = new Cleanup( - $this->rootFolder, - $this->logger, - $this->previewService, - ); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testCleanup(): void { - $this->previewService->expects($this->once())->method('deleteAll'); - - $previewFolder = $this->createMock(Folder::class); - $previewFolder->expects($this->once()) - ->method('isDeletable') - ->willReturn(true); - - $previewFolder->expects($this->once()) - ->method('delete'); - - $appDataFolder = $this->createMock(Folder::class); - $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder); - - $this->rootFolder->expects($this->once()) - ->method('getAppDataDirectoryName') - ->willReturn('appdata_some_id'); - - $this->rootFolder->expects($this->once()) - ->method('get') - ->with('appdata_some_id') - ->willReturn($appDataFolder); - - $this->output->expects($this->exactly(2))->method('writeln') - ->with(self::callback(function (string $message): bool { - static $i = 0; - return match (++$i) { - 1 => $message === 'Preview folder deleted', - 2 => $message === 'Previews removed' - }; - })); - - $this->assertEquals(0, $this->repair->run($this->input, $this->output)); - } - - public function testCleanupWhenNotDeletable(): void { - $previewFolder = $this->createMock(Folder::class); - $previewFolder->expects($this->once()) - ->method('isDeletable') - ->willReturn(false); - - $previewFolder->expects($this->never()) - ->method('delete'); - - $appDataFolder = $this->createMock(Folder::class); - $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder); - - $this->rootFolder->expects($this->once()) - ->method('getAppDataDirectoryName') - ->willReturn('appdata_some_id'); - - $this->rootFolder->expects($this->once()) - ->method('get') - ->with('appdata_some_id') - ->willReturn($appDataFolder); - - $this->logger->expects($this->once())->method('error')->with("Previews can't be removed: preview folder isn't deletable"); - $this->output->expects($this->once())->method('writeln')->with("Previews can't be removed: preview folder isn't deletable"); - - $this->assertEquals(1, $this->repair->run($this->input, $this->output)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestCleanupWithDeleteException')] - public function testCleanupWithDeleteException(string $exceptionClass, string $errorMessage): void { - $previewFolder = $this->createMock(Folder::class); - $previewFolder->expects($this->once()) - ->method('isDeletable') - ->willReturn(true); - - $previewFolder->expects($this->once()) - ->method('delete') - ->willThrowException(new $exceptionClass()); - - $appDataFolder = $this->createMock(Folder::class); - $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder); - - $this->rootFolder->expects($this->once()) - ->method('getAppDataDirectoryName') - ->willReturn('appdata_some_id'); - - $this->rootFolder->expects($this->once()) - ->method('get') - ->with('appdata_some_id') - ->willReturn($appDataFolder); - - $this->logger->expects($this->once())->method('error')->with($errorMessage); - $this->output->expects($this->once())->method('writeln')->with($errorMessage); - - $this->assertEquals(1, $this->repair->run($this->input, $this->output)); - } - - public static function dataForTestCleanupWithDeleteException(): array { - return [ - [NotFoundException::class, "Previews weren't deleted: preview folder was not found while deleting it"], - [NotPermittedException::class, "Previews weren't deleted: you don't have the permission to delete preview folder"], - ]; - } - - public function testCleanupWithPreviewServiceException(): void { - $this->rootFolder->method('getAppDataDirectoryName') - ->willThrowException(new NotFoundException()); - - $this->previewService->expects($this->once())->method('deleteAll') - ->willThrowException(new NotPermittedException('abc')); - - $this->logger->expects($this->once())->method('info')->with("Legacy previews can't be removed: appdata folder can't be found"); - $this->logger->expects($this->once())->method('error')->with("Previews can't be removed: exception occurred: abc"); - - $this->assertEquals(1, $this->repair->run($this->input, $this->output)); - } -} --- a/tests/Core/Command/SetupChecksTest.php +++ /dev/null @@ -1,146 +0,0 @@ -setupCheckManager = $this->createMock(ISetupCheckManager::class); - $this->richTextFormatter = $this->createMock(IRichTextFormatter::class); - - $this->commandTester = new CommandTester( - new SetupChecks($this->setupCheckManager, $this->richTextFormatter) - ); - } - - /** - * Report one check through the output the command passes to the manager, and return its result. - */ - private function runOneCheck(?IOutput $output): array { - $this->assertInstanceOf(IOutput::class, $output); - $check = new SetupChecksTestCheck(); - $output->debug('Starting check ' . $check->getName() . ' (' . $check::class . ')'); - $output->debug('Check ' . $check->getName() . ' (' . $check::class . ') done in 0.01 seconds, peak memory usage: 8 MB'); - $result = SetupResult::success('Everything is fine'); - $result->setName($check->getName()); - return ['system' => [$check::class => $result]]; - } - - private function expectRunAll(): void { - $this->setupCheckManager->expects($this->once()) - ->method('runAll') - ->willReturnCallback($this->runOneCheck(...)); - } - - public function testProgressIsReportedOnVerboseOutput(): void { - $this->expectRunAll(); - - $this->assertSame(Command::SUCCESS, $this->commandTester->execute([], [ - 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, - 'capture_stderr_separately' => true, - ])); - - $this->assertStringContainsString('Starting check Test check (' . SetupChecksTestCheck::class . ')', $this->commandTester->getErrorOutput()); - $this->assertStringContainsString('done in 0.01 seconds, peak memory usage: 8 MB', $this->commandTester->getErrorOutput()); - $this->assertStringContainsString('Everything is fine', $this->commandTester->getDisplay()); - $this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getDisplay()); - } - - public function testProgressIsNotReportedOnNormalOutput(): void { - $this->expectRunAll(); - - $this->assertSame(Command::SUCCESS, $this->commandTester->execute([], [ - 'capture_stderr_separately' => true, - ])); - - $this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getErrorOutput()); - $this->assertStringNotContainsString('Starting check Test check', $this->commandTester->getDisplay()); - } - - public function testProgressKeepsJsonOutputParsable(): void { - $this->expectRunAll(); - - $this->assertSame(Command::SUCCESS, $this->commandTester->execute(['--output' => 'json'], [ - 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, - 'capture_stderr_separately' => true, - ])); - - $this->assertStringContainsString('Starting check Test check', $this->commandTester->getErrorOutput()); - $this->assertIsArray(json_decode($this->commandTester->getDisplay(), true, flags: JSON_THROW_ON_ERROR)); - } - - public function testFilterByCategory(): void { - $this->setupCheckManager->expects($this->once()) - ->method('runByCategory') - ->willReturnCallback(function (string $category, ?IOutput $output): array { - $this->assertSame('system', $category); - return $this->runOneCheck($output); - }); - - $this->assertSame(Command::SUCCESS, $this->commandTester->execute(['category' => 'system'])); - } - - public function testFilterByClass(): void { - $this->setupCheckManager->expects($this->once()) - ->method('runByClass') - ->willReturnCallback(function (string $class, ?IOutput $output): array { - $this->assertSame(SetupChecksTestCheck::class, $class); - return $this->runOneCheck($output); - }); - - $this->assertSame(Command::SUCCESS, $this->commandTester->execute(['class' => SetupChecksTestCheck::class])); - } - - public function testFilterByCategoryAndClassIsRejected(): void { - $this->setupCheckManager->expects($this->never()) - ->method($this->anything()); - - $this->assertSame(Command::FAILURE, $this->commandTester->execute([ - 'category' => 'system', - 'class' => SetupChecksTestCheck::class, - ])); - $this->assertStringContainsString('Please specify only one of category or class', $this->commandTester->getDisplay()); - } -} --- a/tests/Core/Command/SystemTag/AddTest.php +++ /dev/null @@ -1,122 +0,0 @@ -systemTagManager = $this->createMock(ISystemTagManager::class); - $this->command = $this->getMockBuilder(Add::class) - ->setConstructorArgs([$this->systemTagManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testExecute(): void { - $tagId = '42'; - $tagName = 'wichtig'; - $tagAccess = 'public'; - - $tag = $this->createMock(ISystemTag::class); - $tag->method('getId')->willReturn($tagId); - $tag->method('getName')->willReturn($tagName); - $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC); - - $this->systemTagManager->method('createTag') - ->with( - $tagName, - true, - true - )->willReturn($tag); - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagName, $tagAccess) { - if ($arg === 'name') { - return $tagName; - } elseif ($arg === 'access') { - return $tagAccess; - } - throw new \Exception(); - }); - - $this->command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with( - $this->equalTo($this->input), - $this->equalTo($this->output), - [ - 'id' => $tagId, - 'name' => $tagName, - 'access' => $tagAccess, - ] - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAlreadyExists(): void { - $tagId = '42'; - $tagName = 'wichtig'; - $tagAccess = 'public'; - - $tag = $this->createMock(ISystemTag::class); - $tag->method('getId')->willReturn($tagId); - $tag->method('getName')->willReturn($tagName); - $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC); - - $this->systemTagManager->method('createTag') - ->willReturnCallback(function ($tagName, $userVisible, $userAssignable): void { - throw new TagAlreadyExistsException( - 'Tag ("' . $tagName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists' - ); - }); - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagName, $tagAccess) { - if ($arg === 'name') { - return $tagName; - } elseif ($arg === 'access') { - return $tagAccess; - } - throw new \Exception(); - }); - - $this->output->expects($this->once()) - ->method('writeln') - ->with( - 'Tag ("wichtig", 1, 1) already exists' - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/SystemTag/DeleteTest.php +++ /dev/null @@ -1,84 +0,0 @@ -systemTagManager = $this->createMock(ISystemTagManager::class); - $this->command = $this->getMockBuilder(Delete::class) - ->setConstructorArgs([$this->systemTagManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testExecute(): void { - $tagId = 69; - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagId) { - if ($arg === 'id') { - return $tagId; - } - throw new \Exception(); - }); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('The specified tag was deleted'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testNotFound(): void { - $tagId = 69; - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagId) { - if ($arg === 'id') { - return $tagId; - } - throw new \Exception(); - }); - - $this->systemTagManager->method('deleteTags') - ->willReturnCallback(function ($tagId): void { - throw new TagNotFoundException(); - }); - - $this->output->expects($this->once()) - ->method('writeln') - ->with('Tag not found'); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/SystemTag/EditTest.php +++ /dev/null @@ -1,187 +0,0 @@ -systemTagManager = $this->createMock(ISystemTagManager::class); - $this->command = $this->getMockBuilder(Edit::class) - ->setConstructorArgs([$this->systemTagManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testExecute(): void { - $tagId = '5'; - $tagName = 'unwichtige Dateien'; - $newTagName = 'moderat wichtige Dateien'; - $newTagAccess = 'restricted'; - $newTagUserVisible = true; - $newTagUserAssignable = false; - - $tag = $this->createMock(ISystemTag::class); - $tag->method('getId')->willReturn($tagId); - $tag->method('getName')->willReturn($tagName); - $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE); - - $this->systemTagManager->method('getTagsByIds') - ->with($tagId) - ->willReturn([$tag]); - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagId) { - if ($arg === 'id') { - return $tagId; - } - throw new \Exception(); - }); - - $this->input->method('getOption') - ->willReturnCallback(function ($arg) use ($newTagName, $newTagAccess) { - if ($arg === 'name') { - return $newTagName; - } elseif ($arg === 'access') { - return $newTagAccess; - } - throw new \Exception(); - }); - - $this->systemTagManager->expects($this->once()) - ->method('updateTag') - ->with( - $tagId, - $newTagName, - $newTagUserVisible, - $newTagUserAssignable, - '' - ); - - $this->output->expects($this->once()) - ->method('writeln') - ->with( - 'Tag updated ("' . $newTagName . '", ' . json_encode($newTagUserVisible) . ', ' . json_encode($newTagUserAssignable) . ', "")' - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testAlreadyExists(): void { - $tagId = '5'; - $tagName = 'unwichtige Dateien'; - $tagUserVisible = false; - $tagUserAssignable = false; - $newTagName = 'moderat wichtige Dateien'; - $newTagAccess = 'restricted'; - $newTagUserVisible = true; - $newTagUserAssignable = false; - - $tag = $this->createMock(ISystemTag::class); - $tag->method('getId')->willReturn($tagId); - $tag->method('getName')->willReturn($tagName); - $tag->method('isUserVisible')->willReturn($tagUserVisible); - $tag->method('isUserAssignable')->willReturn($tagUserAssignable); - $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE); - - $this->systemTagManager->method('getTagsByIds') - ->with($tagId) - ->willReturn([$tag]); - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagId) { - if ($arg === 'id') { - return $tagId; - } - throw new \Exception(); - }); - - $this->input->method('getOption') - ->willReturnCallback(function ($arg) use ($newTagName, $newTagAccess) { - if ($arg === 'name') { - return $newTagName; - } elseif ($arg === 'access') { - return $newTagAccess; - } - throw new \Exception(); - }); - - $this->systemTagManager->method('updateTag') - ->willReturnCallback(function ($tagId, $tagName, $userVisible, $userAssignable): void { - throw new TagAlreadyExistsException( - 'Tag ("' . $tagName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists' - ); - }); - - $this->systemTagManager->expects($this->once()) - ->method('updateTag') - ->with( - $tagId, - $newTagName, - $newTagUserVisible, - $newTagUserAssignable, - '' - ); - - $this->output->expects($this->once()) - ->method('writeln') - ->with( - 'Tag ("' . $newTagName . '", ' . $newTagUserVisible . ', ' . $newTagUserAssignable . ') already exists' - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } - - public function testNotFound(): void { - $tagId = '404'; - - $this->input->method('getArgument') - ->willReturnCallback(function ($arg) use ($tagId) { - if ($arg === 'id') { - return $tagId; - } - throw new \Exception(); - }); - - $this->systemTagManager->method('getTagsByIds') - ->with($tagId) - ->willReturn([]); - - $this->output->expects($this->once()) - ->method('writeln') - ->with( - 'Tag not found' - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/SystemTag/ListCommandTest.php +++ /dev/null @@ -1,97 +0,0 @@ -systemTagManager = $this->createMock(ISystemTagManager::class); - $this->command = $this->getMockBuilder(ListCommand::class) - ->setConstructorArgs([$this->systemTagManager]) - ->onlyMethods(['writeArrayInOutputFormat']) - ->getMock(); - - $this->input = $this->createMock(InputInterface::class); - $this->output = $this->createMock(OutputInterface::class); - } - - public function testExecute(): void { - $tag1 = $this->createMock(ISystemTag::class); - $tag1->method('getId')->willReturn('1'); - $tag1->method('getName')->willReturn('public_tag'); - $tag1->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC); - $tag2 = $this->createMock(ISystemTag::class); - $tag2->method('getId')->willReturn('2'); - $tag2->method('getName')->willReturn('restricted_tag'); - $tag2->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_RESTRICTED); - $tag3 = $this->createMock(ISystemTag::class); - $tag3->method('getId')->willReturn('3'); - $tag3->method('getName')->willReturn('invisible_tag'); - $tag3->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE); - - $this->systemTagManager->method('getAllTags') - ->with( - null, - null - )->willReturn([$tag1, $tag2, $tag3]); - - $this->input->method('getOption') - ->willReturnCallback(function ($arg) { - if ($arg === 'visibilityFilter') { - return null; - } elseif ($arg === 'nameSearchPattern') { - return null; - } - throw new \Exception(); - }); - - $this->command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with( - $this->equalTo($this->input), - $this->equalTo($this->output), - [ - '1' => [ - 'name' => 'public_tag', - 'access' => 'public', - ], - '2' => [ - 'name' => 'restricted_tag', - 'access' => 'restricted', - ], - '3' => [ - 'name' => 'invisible_tag', - 'access' => 'invisible', - ] - ] - ); - - $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]); - } -} --- a/tests/Core/Command/TaskProcessing/WorkerCommandTest.php +++ /dev/null @@ -1,352 +0,0 @@ -manager = $this->createMock(IManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->timeFactory->method('now')->willReturnCallback(fn () => new \DateTimeImmutable()); - $this->command = new WorkerCommand($this->manager, $this->logger, $this->appConfig, $this->timeFactory); - } - - /** - * Helper to create a minimal ISynchronousProvider mock. - */ - private function createProvider(string $id, string $taskTypeId): ISynchronousProvider&MockObject { - $provider = $this->createMock(ISynchronousProvider::class); - $provider->method('getId')->willReturn($id); - $provider->method('getTaskTypeId')->willReturn($taskTypeId); - return $provider; - } - - /** - * Helper to create a real Task with a given id and optional task type. - */ - private function createTask(int $id, string $type = 'test_task_type'): Task { - $task = new Task($type, [], 'testapp', null); - $task->setId($id); - return $task; - } - - public function testOnceExitsAfterNoTask(): void { - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([]); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testOnceProcessesOneTask(): void { - $taskTypeId = 'test_task_type'; - $provider = $this->createProvider('test_provider', $taskTypeId); - $task = $this->createTask(42); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId) - ->willReturn($provider); - - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with([$taskTypeId]) - ->willReturn($task); - - $this->manager->expects($this->once()) - ->method('processTask') - ->with($task, $provider) - ->willReturn(true); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testSkipsNonSynchronousProviders(): void { - // A provider that is NOT an ISynchronousProvider - $nonSyncProvider = $this->createMock(\OCP\TaskProcessing\IProvider::class); - $nonSyncProvider->method('getId')->willReturn('non_sync_provider'); - $nonSyncProvider->method('getTaskTypeId')->willReturn('some_type'); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$nonSyncProvider]); - - $this->manager->expects($this->never()) - ->method('getPreferredProvider'); - - $this->manager->expects($this->never()) - ->method('claimNextScheduledTask'); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testSkipsNonPreferredProviders(): void { - $taskTypeId = 'test_task_type'; - $provider = $this->createProvider('provider_a', $taskTypeId); - $preferredProvider = $this->createProvider('provider_b', $taskTypeId); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId) - ->willReturn($preferredProvider); - - // provider_a is not preferred (provider_b is), so claimNextScheduledTask is never called - $this->manager->expects($this->never()) - ->method('claimNextScheduledTask'); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testContinuesWhenNoTaskFound(): void { - $taskTypeId = 'test_task_type'; - $provider = $this->createProvider('test_provider', $taskTypeId); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId) - ->willReturn($provider); - - // The no-task path is now claimNextScheduledTask returning null (not an exception). - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with([$taskTypeId]) - ->willReturn(null); - - $this->manager->expects($this->never()) - ->method('processTask'); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testLogsErrorAndContinuesOnException(): void { - $taskTypeId = 'test_task_type'; - $provider = $this->createProvider('test_provider', $taskTypeId); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId) - ->willReturn($provider); - - $exception = new Exception('DB error'); - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with([$taskTypeId]) - ->willThrowException($exception); - - $this->logger->expects($this->once()) - ->method('error') - ->with('Unknown error while claiming scheduled TaskProcessing tasks', ['exception' => $exception]); - - $this->manager->expects($this->never()) - ->method('processTask'); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testProcessesCorrectProviderForReturnedTaskType(): void { - $taskTypeId1 = 'type_a'; - $taskTypeId2 = 'type_b'; - - $provider1 = $this->createProvider('provider_a', $taskTypeId1); - $provider2 = $this->createProvider('provider_b', $taskTypeId2); - // Task has type_a, so provider1 must be chosen to process it - $task = $this->createTask(7, $taskTypeId1); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider1, $provider2]); - - // Both providers are eligible, so getPreferredProvider is called for each - $this->manager->expects($this->exactly(2)) - ->method('getPreferredProvider') - ->willReturnMap([ - [$taskTypeId1, $provider1], - [$taskTypeId2, $provider2], - ]); - - // All eligible task types are passed in a single atomic claim - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with($this->equalTo([$taskTypeId1, $taskTypeId2])) - ->willReturn($task); - - $this->manager->expects($this->once()) - ->method('processTask') - ->with($task, $provider1) - ->willReturn(true); - - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testTaskTypesWhitelistFiltersProviders(): void { - $taskTypeId1 = 'type_a'; - $taskTypeId2 = 'type_b'; - - $provider1 = $this->createProvider('provider_a', $taskTypeId1); - $provider2 = $this->createProvider('provider_b', $taskTypeId2); - $task = $this->createTask(99, $taskTypeId2); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider1, $provider2]); - - // Only type_b is whitelisted, so provider_a (type_a) must be skipped entirely - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId2) - ->willReturn($provider2); - - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with([$taskTypeId2]) - ->willReturn($task); - - $this->manager->expects($this->once()) - ->method('processTask') - ->with($task, $provider2) - ->willReturn(true); - - $input = new ArrayInput(['--once' => true, '--taskTypes' => [$taskTypeId2]], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testTaskTypesWhitelistWithNoMatchingProviders(): void { - $provider = $this->createProvider('provider_a', 'type_a'); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - // Whitelist does not include type_a so nothing should be processed - $this->manager->expects($this->never()) - ->method('getPreferredProvider'); - - $this->manager->expects($this->never()) - ->method('claimNextScheduledTask'); - - $input = new ArrayInput(['--once' => true, '--taskTypes' => ['type_b']], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } - - public function testEmptyTaskTypesAllowsAllProviders(): void { - $taskTypeId = 'type_a'; - $provider = $this->createProvider('provider_a', $taskTypeId); - $task = $this->createTask(5, $taskTypeId); - - $this->manager->expects($this->once()) - ->method('getProviders') - ->willReturn([$provider]); - - $this->manager->expects($this->once()) - ->method('getPreferredProvider') - ->with($taskTypeId) - ->willReturn($provider); - - $this->manager->expects($this->once()) - ->method('claimNextScheduledTask') - ->with([$taskTypeId]) - ->willReturn($task); - - $this->manager->expects($this->once()) - ->method('processTask') - ->with($task, $provider) - ->willReturn(true); - - // No --taskTypes option provided - $input = new ArrayInput(['--once' => true], $this->command->getDefinition()); - $output = new NullOutput(); - - $result = $this->command->run($input, $output); - - $this->assertSame(0, $result); - } -} --- a/tests/Core/Command/TwoFactorAuth/CleanupTest.php +++ /dev/null @@ -1,53 +0,0 @@ -registry = $this->createMock(IRegistry::class); - $this->userManager = $this->createMock(IUserManager::class); - - $cmd = new Cleanup($this->registry, $this->userManager); - $this->cmd = new CommandTester($cmd); - } - - public function testCleanup(): void { - $this->registry->expects($this->once()) - ->method('cleanUp') - ->with('u2f'); - - $rc = $this->cmd->execute([ - 'provider-id' => 'u2f', - ]); - - $this->assertEquals(0, $rc); - $output = $this->cmd->getDisplay(); - $this->assertStringContainsString('All user-provider associations for provider u2f have been removed', $output); - } -} --- a/tests/Core/Command/TwoFactorAuth/DisableTest.php +++ /dev/null @@ -1,95 +0,0 @@ -providerManager = $this->createMock(ProviderManager::class); - $this->userManager = $this->createMock(IUserManager::class); - - $cmd = new Disable($this->providerManager, $this->userManager); - $this->command = new CommandTester($cmd); - } - - public function testInvalidUID(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn(null); - - $rc = $this->command->execute([ - 'uid' => 'nope', - 'provider_id' => 'nope', - ]); - - $this->assertEquals(1, $rc); - $this->assertStringContainsString('Invalid UID', $this->command->getDisplay()); - } - - public function testEnableNotSupported(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('ricky') - ->willReturn($user); - $this->providerManager->expects($this->once()) - ->method('tryDisableProviderFor') - ->with('totp', $user) - ->willReturn(false); - - $rc = $this->command->execute([ - 'uid' => 'ricky', - 'provider_id' => 'totp', - ]); - - $this->assertEquals(2, $rc); - $this->assertStringContainsString('The provider does not support this operation', $this->command->getDisplay()); - } - - public function testEnabled(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('ricky') - ->willReturn($user); - $this->providerManager->expects($this->once()) - ->method('tryDisableProviderFor') - ->with('totp', $user) - ->willReturn(true); - - $rc = $this->command->execute([ - 'uid' => 'ricky', - 'provider_id' => 'totp', - ]); - - $this->assertEquals(0, $rc); - $this->assertStringContainsString('Two-factor provider totp disabled for user ricky', $this->command->getDisplay()); - } -} --- a/tests/Core/Command/TwoFactorAuth/EnableTest.php +++ /dev/null @@ -1,95 +0,0 @@ -providerManager = $this->createMock(ProviderManager::class); - $this->userManager = $this->createMock(IUserManager::class); - - $cmd = new Enable($this->providerManager, $this->userManager); - $this->command = new CommandTester($cmd); - } - - public function testInvalidUID(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn(null); - - $rc = $this->command->execute([ - 'uid' => 'nope', - 'provider_id' => 'nope', - ]); - - $this->assertEquals(1, $rc); - $this->assertStringContainsString('Invalid UID', $this->command->getDisplay()); - } - - public function testEnableNotSupported(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('belle') - ->willReturn($user); - $this->providerManager->expects($this->once()) - ->method('tryEnableProviderFor') - ->with('totp', $user) - ->willReturn(false); - - $rc = $this->command->execute([ - 'uid' => 'belle', - 'provider_id' => 'totp', - ]); - - $this->assertEquals(2, $rc); - $this->assertStringContainsString('The provider does not support this operation', $this->command->getDisplay()); - } - - public function testEnabled(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('belle') - ->willReturn($user); - $this->providerManager->expects($this->once()) - ->method('tryEnableProviderFor') - ->with('totp', $user) - ->willReturn(true); - - $rc = $this->command->execute([ - 'uid' => 'belle', - 'provider_id' => 'totp', - ]); - - $this->assertEquals(0, $rc); - $this->assertStringContainsString('Two-factor provider totp enabled for user belle', $this->command->getDisplay()); - } -} --- a/tests/Core/Command/TwoFactorAuth/EnforceTest.php +++ /dev/null @@ -1,129 +0,0 @@ -mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); - $command = new Enforce($this->mandatoryTwoFactor); - - $this->command = new CommandTester($command); - } - - public function testEnforce(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('setState') - ->with($this->equalTo(new EnforcementState(true))); - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(true)); - - $rc = $this->command->execute([ - '--on' => true, - ]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is enforced for all users', $display); - } - - public function testEnforceForOneGroup(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('setState') - ->with($this->equalTo(new EnforcementState(true, ['twofactorers']))); - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(true, ['twofactorers'])); - - $rc = $this->command->execute([ - '--on' => true, - '--group' => ['twofactorers'], - ]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is enforced for members of the group(s) twofactorers', $display); - } - - public function testEnforceForAllExceptOneGroup(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('setState') - ->with($this->equalTo(new EnforcementState(true, [], ['yoloers']))); - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(true, [], ['yoloers'])); - - $rc = $this->command->execute([ - '--on' => true, - '--exclude' => ['yoloers'], - ]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is enforced for all users, except members of yoloers', $display); - } - - public function testDisableEnforced(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('setState') - ->with(new EnforcementState(false)); - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(false)); - - $rc = $this->command->execute([ - '--off' => true, - ]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is not enforced', $display); - } - - public function testCurrentStateEnabled(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(true)); - - $rc = $this->command->execute([]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is enforced for all users', $display); - } - - public function testCurrentStateDisabled(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('getState') - ->willReturn(new EnforcementState(false)); - - $rc = $this->command->execute([]); - - $this->assertEquals(0, $rc); - $display = $this->command->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is not enforced', $display); - } -} --- a/tests/Core/Command/TwoFactorAuth/StateTest.php +++ /dev/null @@ -1,95 +0,0 @@ -registry = $this->createMock(IRegistry::class); - $this->userManager = $this->createMock(IUserManager::class); - - $cmd = new State($this->registry, $this->userManager); - $this->cmd = new CommandTester($cmd); - } - - public function testWrongUID(): void { - $this->cmd->execute([ - 'uid' => 'nope', - ]); - - $output = $this->cmd->getDisplay(); - $this->assertStringContainsString('Invalid UID', $output); - } - - public function testStateNoProvidersActive(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('eldora') - ->willReturn($user); - $states = [ - 'u2f' => false, - 'totp' => false, - ]; - $this->registry->expects($this->once()) - ->method('getProviderStates') - ->with($user) - ->willReturn($states); - - $this->cmd->execute([ - 'uid' => 'eldora', - ]); - - $output = $this->cmd->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is not enabled for user eldora', $output); - } - - public function testStateOneProviderActive(): void { - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('mohamed') - ->willReturn($user); - $states = [ - 'u2f' => true, - 'totp' => false, - ]; - $this->registry->expects($this->once()) - ->method('getProviderStates') - ->with($user) - ->willReturn($states); - - $this->cmd->execute([ - 'uid' => 'mohamed', - ]); - - $output = $this->cmd->getDisplay(); - $this->assertStringContainsString('Two-factor authentication is enabled for user mohamed', $output); - } -} --- a/tests/Core/Command/User/AddTest.php +++ /dev/null @@ -1,157 +0,0 @@ -userManager = static::createMock(IUserManager::class); - $this->groupManager = static::createStub(IGroupManager::class); - $this->appConfig = static::createMock(IAppConfig::class); - $this->mailHelper = static::createMock(NewUserMailHelper::class); - $this->eventDispatcher = static::createStub(IEventDispatcher::class); - $this->secureRandom = static::createStub(ISecureRandom::class); - - $this->user = static::createMock(IUser::class); - - $this->consoleInput = static::createMock(InputInterface::class); - $this->consoleOutput = static::createMock(OutputInterface::class); - - $this->addCommand = new Add( - $this->userManager, - $this->groupManager, - $this->getEmailValidatorWithStrictEmailCheck(), - $this->appConfig, - $this->mailHelper, - $this->eventDispatcher, - $this->secureRandom - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('addEmailDataProvider')] - public function testAddEmail( - ?string $email, - bool $isEmailValid, - bool $shouldSendEmail, - ): void { - $this->user->expects($isEmailValid ? static::once() : static::never()) - ->method('setSystemEMailAddress') - ->with(static::equalTo($email)); - - $this->userManager->method('createUser') - ->willReturn($this->user); - - $this->appConfig->method('getValueBool') - ->willReturn($shouldSendEmail); - - $this->mailHelper->method('generateTemplate') - ->willReturn(static::createMock(IEMailTemplate::class)); - - $this->mailHelper->expects($isEmailValid && $shouldSendEmail ? static::once() : static::never()) - ->method('sendMail'); - - $this->consoleInput->method('getArgument') - ->willReturnMap([ - ['uid', 'JohnDoe'], - ]); - - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['generate-password', 'true'], - ['email', $email], - ['group', []], - ]); - - $this->invokePrivate($this->addCommand, 'execute', [ - $this->consoleInput, - $this->consoleOutput - ]); - } - - /** - * @return array - */ - public static function addEmailDataProvider(): array { - return [ - 'Valid E-Mail' => [ - 'info@example.com', - true, - true, - ], - 'Invalid E-Mail' => [ - 'info@@example.com', - false, - false, - ], - 'No E-Mail' => [ - '', - false, - false, - ], - 'Valid E-Mail, but no mail should be sent' => [ - 'info@example.com', - true, - false, - ], - ]; - } -} --- a/tests/Core/Command/User/AuthTokens/DeleteTest.php +++ /dev/null @@ -1,214 +0,0 @@ -tokenProvider = $this->getMockBuilder(IProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var \OC\Authentication\Token\IProvider $tokenProvider */ - $this->command = new Delete($tokenProvider); - } - - /** - * Default option mapping: --last-used-before unset, --cancel-wipe unset. - * - * @param string|null $lastUsedBefore - * @param bool $cancelWipe - */ - private function mockOptions(?string $lastUsedBefore = null, bool $cancelWipe = false): void { - $this->consoleInput->method('getOption') - ->willReturnMap([ - ['last-used-before', $lastUsedBefore], - ['cancel-wipe', $cancelWipe], - ]); - } - - public function testDeleteTokenById(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', '42'] - ]); - - $this->mockOptions(); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateTokenById') - ->with('user', 42); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } - - public function testDeleteTokenByIdRequiresTokenId(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', null] - ]); - - $this->mockOptions(); - - $this->expectException(RuntimeException::class); - - $this->tokenProvider->expects($this->never())->method('invalidateTokenById'); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::FAILURE, $result); - } - - public function testDeleteTokensLastUsedBefore(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', null] - ]); - - $this->mockOptions('946684800'); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateLastUsedBefore') - ->with('user', 946684800); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } - - public function testLastUsedBeforeAcceptsIso8601Expanded(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', null] - ]); - - $this->mockOptions('2000-01-01T00:00:00Z'); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateLastUsedBefore') - ->with('user', 946684800); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } - - public function testLastUsedBeforeAcceptsYmd(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', null] - ]); - - $this->mockOptions('2000-01-01'); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateLastUsedBefore') - ->with('user', 946684800); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } - - public function testIdAndLastUsedBeforeAreMutuallyExclusive(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', '42'] - ]); - - $this->mockOptions('946684800'); - - $this->expectException(RuntimeException::class); - - $this->tokenProvider->expects($this->never())->method('invalidateLastUsedBefore'); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } - - public function testDeleteByIdRefusesWipePendingWithoutFlag(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', '42'] - ]); - - $this->mockOptions(); - - $wipeToken = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getTokenById') - ->with(42) - ->willThrowException(new WipeTokenException($wipeToken)); - - $this->tokenProvider->expects($this->never())->method('invalidateTokenById'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('marked for remote wipe')); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::FAILURE, $result); - } - - public function testDeleteByIdAllowsWipePendingWithFlag(): void { - $this->consoleInput->expects($this->exactly(2)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'user'], - ['id', '42'] - ]); - - $this->mockOptions(null, true); - - // With --cancel-wipe, the wipe-state pre-check is skipped entirely - // (the operator has already opted in), so getTokenById should not run. - $this->tokenProvider->expects($this->never())->method('getTokenById'); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateTokenById') - ->with('user', 42); - - $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - $this->assertSame(Command::SUCCESS, $result); - } -} --- a/tests/Core/Command/User/DeleteTest.php +++ /dev/null @@ -1,96 +0,0 @@ -userManager = $this->getMockBuilder(IUserManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var IUserManager $userManager */ - $this->command = new Delete($userManager); - } - - public static function validUserLastSeen(): array { - return [ - [true, 'The specified user was deleted'], - [false, 'The specified user could not be deleted'], - ]; - } - - /** - * - * @param bool $deleteSuccess - * @param string $expectedString - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validUserLastSeen')] - public function testValidUser($deleteSuccess, $expectedString): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->once()) - ->method('delete') - ->willReturn($deleteSuccess); - - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn($user); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains($expectedString)); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testInvalidUser(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn(null); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('User does not exist')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/User/DisableTest.php +++ /dev/null @@ -1,79 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new Disable($this->userManager); - } - - public function testValidUser(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('setEnabled') - ->with(false); - - $this->userManager - ->method('get') - ->with('user') - ->willReturn($user); - - $this->consoleInput - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('The specified user is disabled')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testInvalidUser(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn(null); - - $this->consoleInput - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('User does not exist')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/User/EnableTest.php +++ /dev/null @@ -1,79 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - - $this->command = new Enable($this->userManager); - } - - public function testValidUser(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('setEnabled') - ->with(true); - - $this->userManager - ->method('get') - ->with('user') - ->willReturn($user); - - $this->consoleInput - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('The specified user is enabled')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testInvalidUser(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn(null); - - $this->consoleInput - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('User does not exist')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/User/LastSeenTest.php +++ /dev/null @@ -1,190 +0,0 @@ -userManager = $this->getMockBuilder(IUserManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); - $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); - - /** @var IUserManager $userManager */ - $this->command = new LastSeen($userManager); - } - - public static function validUserLastSeen(): array { - return [ - [0, 'never logged in'], - [time(), 'last login'], - ]; - } - - /** - * - * @param int $lastSeen - * @param string $expectedString - */ - #[\PHPUnit\Framework\Attributes\DataProvider('validUserLastSeen')] - public function testValidUser($lastSeen, $expectedString): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->once()) - ->method('getLastLogin') - ->willReturn($lastSeen); - - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn($user); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains($expectedString)); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testInvalidUser(): void { - $this->userManager->expects($this->once()) - ->method('get') - ->with('user') - ->willReturn(null); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn('user'); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains('User does not exist')); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testAllUsersWithoutExcludeDisabled(): void { - $enabledUser = $this->getMockBuilder(IUser::class)->getMock(); - $enabledUser->expects($this->once()) - ->method('getLastLogin') - ->willReturn(time()); - $enabledUser->expects($this->once()) - ->method('getUID') - ->willReturn('enabled_user'); - $enabledUser->expects($this->never()) - ->method('isEnabled'); - - $disabledUser = $this->getMockBuilder(IUser::class)->getMock(); - $disabledUser->expects($this->once()) - ->method('getLastLogin') - ->willReturn(time()); - $disabledUser->expects($this->once()) - ->method('getUID') - ->willReturn('disabled_user'); - $disabledUser->expects($this->never()) - ->method('isEnabled'); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn(null); - - $this->consoleInput->expects($this->exactly(2)) - ->method('getOption') - ->willReturnMap([ - ['all', true], - ['exclude-disabled', false], - ]); - - $this->userManager->expects($this->once()) - ->method('callForAllUsers') - ->willReturnCallback(function ($callback) use ($enabledUser, $disabledUser): void { - $callback($enabledUser); - $callback($disabledUser); - }); - - $this->consoleOutput->expects($this->exactly(2)) - ->method('writeln') - ->with($this->stringContains("'s last login:")); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } - - public function testAllUsersWithExcludeDisabled(): void { - $enabledUser = $this->getMockBuilder(IUser::class)->getMock(); - $enabledUser->expects($this->once()) - ->method('getLastLogin') - ->willReturn(time()); - $enabledUser->expects($this->once()) - ->method('getUID') - ->willReturn('enabled_user'); - $enabledUser->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - - $disabledUser = $this->getMockBuilder(IUser::class)->getMock(); - $disabledUser->expects($this->never()) - ->method('getLastLogin'); - $disabledUser->expects($this->never()) - ->method('getUID'); - $disabledUser->expects($this->once()) - ->method('isEnabled') - ->willReturn(false); - - $this->consoleInput->expects($this->once()) - ->method('getArgument') - ->with('uid') - ->willReturn(null); - - $this->consoleInput->expects($this->exactly(2)) - ->method('getOption') - ->willReturnMap([ - ['all', true], - ['exclude-disabled', true], - ]); - - $this->userManager->expects($this->once()) - ->method('callForAllUsers') - ->willReturnCallback(function ($callback) use ($enabledUser, $disabledUser): void { - $callback($enabledUser); - $callback($disabledUser); - }); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($this->stringContains("enabled_user's last login:")); - - self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]); - } -} --- a/tests/Core/Command/User/ProfileTest.php +++ /dev/null @@ -1,466 +0,0 @@ -accountManager = $this->createMock(IAccountManager::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->connection = $this->createMock(IDBConnection::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - } - - public function getCommand(array $methods = []): Profile|MockObject { - if (empty($methods)) { - return new Profile($this->userManager, $this->accountManager); - } else { - return $this->getMockBuilder(Profile::class) - ->setConstructorArgs([ - $this->userManager, - $this->accountManager, - ]) - ->onlyMethods($methods) - ->getMock(); - } - } - - public static function dataCheckInput(): array { - return [ - 'Call with existing user should pass check' => [ - [['uid', 'username']], - [], - [], - true, - null, - ], - 'Call with non-existing user should fail check' => [ - [['uid', 'username']], - [], - [], - false, - 'The user "username" does not exist.', - ], - - 'Call with uid, key and --default value should pass check' => [ - [['uid', 'username'], ['key', 'configkey']], - [], - [['--default-value', false, true]], - true, - null, - ], - 'Call with uid and empty key with default-value option should fail check' => [ - [['uid', 'username'], ['key', '']], - [], - [['--default-value', false, true]], - true, - 'The "default-value" option can only be used when specifying a key.', - ], - - 'Call with uid, key, value should pass check' => [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [], - [], - true, - null, - ], - 'Call with uid, empty key and empty value should fail check' => [ - [['uid', 'username'], ['key', ''], ['value', '']], - [], - [], - true, - 'The value argument can only be used when specifying a key.', - ], - 'Call with uid, key, empty value and default-value option should fail check' => [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [], - [['--default-value', false, true]], - true, - 'The value argument can not be used together with "default-value".', - ], - 'Call with uid, key, empty value and update-only option should pass check' => [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['update-only', true]], - [], - true, - null, - ], - 'Call with uid, key, null value and update-only option should fail check' => [ - [['uid', 'username'], ['key', 'configkey'], ['value', null]], - [['update-only', true]], - [], - true, - 'The "update-only" option can only be used together with "value".', - ], - - 'Call with uid, key and delete option should pass check' => [ - [['uid', 'username'], ['key', 'configkey']], - [['delete', true]], - [], - true, - null, - ], - 'Call with uid, empty key and delete option should fail check' => [ - [['uid', 'username'], ['key', '']], - [['delete', true]], - [], - true, - 'The "delete" option can only be used when specifying a key.', - ], - 'Call with uid, key, delete option and default-value should fail check' => [ - [['uid', 'username'], ['key', 'configkey']], - [['delete', true]], - [['--default-value', false, true]], - true, - 'The "delete" option can not be used together with "default-value".', - ], - 'Call with uid, key, empty value and delete option should fail check' => [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['delete', true]], - [], - true, - 'The "delete" option can not be used together with "value".', - ], - 'Call with uid, key, delete and error-if-not-exists should pass check' => [ - [['uid', 'username'], ['key', 'configkey']], - [['delete', true], ['error-if-not-exists', true]], - [], - true, - null, - ], - 'Call with uid, key and error-if-not-exists should fail check' => [ - [['uid', 'username'], ['key', 'configkey']], - [['delete', false], ['error-if-not-exists', true]], - [], - true, - 'The "error-if-not-exists" option can only be used together with "delete".', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckInput')] - public function testCheckInput(array $arguments, array $options, array $parameterOptions, bool $existingUser, ?string $expectedException): void { - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap($arguments); - $this->consoleInput->expects($this->any()) - ->method('getOption') - ->willReturnMap($options); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->willReturnCallback(function (string|array $values, bool $onlyParams = false) use ($parameterOptions): bool { - $arguments = func_get_args(); - foreach ($parameterOptions as $parameterOption) { - // check the arguments of the function, if they are the same, return the mocked value - if (array_diff($arguments, $parameterOption) === []) { - return end($parameterOption); - } - } - - return false; - }); - - $returnedUser = null; - if ($existingUser) { - $mockUser = $this->createMock(IUser::class); - $mockUser->expects($this->once())->method('getUID')->willReturn('user'); - $returnedUser = $mockUser; - } - $this->userManager->expects($this->once()) - ->method('get') - ->willReturn($returnedUser); - - $command = $this->getCommand(); - try { - $this->invokePrivate($command, 'checkInput', [$this->consoleInput]); - $this->assertNull($expectedException); - } catch (\InvalidArgumentException $e) { - $this->assertEquals($expectedException, $e->getMessage()); - } - } - - public function testCheckInputExceptionCatch(): void { - $command = $this->getCommand(['checkInput']); - $command->expects($this->once()) - ->method('checkInput') - ->willThrowException(new \InvalidArgumentException('test')); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with('test'); - - $this->assertEquals(1, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function dataExecuteDeleteProfileProperty(): array { - return [ - 'Deleting existing property should succeed' => ['address', 'Berlin', false, null, Command::SUCCESS], - 'Deleting existing property with error-if-not-exists should succeed' => ['address', 'Berlin', true, null, Command::SUCCESS], - 'Deleting non-existing property should succeed' => ['address', '', false, null, Command::SUCCESS], - 'Deleting non-existing property with error-if-not-exists should fail' => ['address', '', true, 'The property does not exist for user "username".', Command::FAILURE], - ]; - } - - /** - * Tests the deletion mechanism on profile settings. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteDeleteProfileProperty')] - public function testExecuteDeleteProfileProperty(string $configKey, string $value, bool $errorIfNotExists, ?string $expectedLine, int $expectedReturn): void { - $uid = 'username'; - $appName = 'profile'; - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - ]); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', $uid], - ['app', $appName], - ['key', $configKey], - ]); - - $mocks = $this->setupProfilePropertiesMock([$configKey => $value]); - - $command->expects($this->once()) - ->method('checkInput') - ->willReturn($mocks['userMock']); - - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturnMap([ - ['--delete', false, true], - ['--error-if-not-exists', false, $errorIfNotExists], - ]); - - if ($expectedLine === null) { - $this->consoleOutput->expects($this->never()) - ->method('writeln'); - $mocks['profilePropertiesMocks'][0]->expects($this->once()) - ->method('setValue') - ->with(''); - $this->accountManager->expects($this->once()) - ->method('updateAccount') - ->with($mocks['accountMock']); - } else { - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($expectedLine); - $this->accountManager->expects($this->never()) - ->method('updateAccount'); - } - - $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public function testExecuteSetProfileProperty(): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - ]); - - $uid = 'username'; - $propertyKey = 'address'; - $propertyValue = 'Barcelona'; - - $this->consoleInput->expects($this->atLeast(3)) - ->method('getArgument') - ->willReturnMap([ - ['uid', $uid], - ['key', $propertyKey], - ['value', $propertyValue], - ]); - - $mocks = $this->setupProfilePropertiesMock([$propertyKey => $propertyValue]); - - $command->expects($this->once()) - ->method('checkInput') - ->willReturn($mocks['userMock']); - - $mocks['profilePropertiesMocks'][0]->expects($this->once()) - ->method('setValue') - ->with($propertyValue); - $this->accountManager->expects($this->once()) - ->method('updateAccount') - ->with($mocks['accountMock']); - - $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function dataExecuteGet(): array { - return [ - 'Get property with set value should pass' => ['configkey', 'value', null, 'value', Command::SUCCESS], - 'Get property with empty value and default-value option should pass' => ['configkey', '', 'default-value', 'default-value', Command::SUCCESS], - 'Get property with empty value should fail' => ['configkey', '', null, 'The property does not exist for user "username".', Command::FAILURE], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteGet')] - public function testExecuteGet(string $key, string $value, ?string $defaultValue, string $expectedLine, int $expectedReturn): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - ]); - - $uid = 'username'; - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', $uid], - ['key', $key], - ]); - - $mocks = $this->setupProfilePropertiesMock([$key => $value]); - - $command->expects($this->once()) - ->method('checkInput') - ->willReturn($mocks['userMock']); - - if ($value === '') { - if ($defaultValue === null) { - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturn(false); - } else { - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturnCallback(fn (string|array $values): bool => $values === '--default-value'); - $this->consoleInput->expects($this->once()) - ->method('getOption') - ->with('default-value') - ->willReturn($defaultValue); - } - } - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($expectedLine); - - $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public function testExecuteList(): void { - $uid = 'username'; - $profileData = [ - 'pronouns' => 'they/them', - 'address' => 'Berlin', - ]; - - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - ]); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', $uid], - ['key', ''], - ]); - - $mocks = $this->setupProfilePropertiesMock(['address' => $profileData['address'], 'pronouns' => $profileData['pronouns']]); - - $command->expects($this->once()) - ->method('checkInput') - ->willReturn($mocks['userMock']); - - $command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with($this->consoleInput, $this->consoleOutput, $profileData); - - $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - /** - * Helper to avoid boilerplate in tests in this file when mocking objects - * of IAccountProperty type. - * - * @param array $properties the properties to be set up as key => value - * @return array{ - * userMock: IUser&MockObject, - * accountMock: IAccount&MockObject, - * profilePropertiesMocks: IAccountProperty&MockObject[] - * } - */ - private function setupProfilePropertiesMock(array $properties): array { - $userMock = $this->createMock(IUser::class); - $accountMock = $this->createMock(IAccount::class); - $this->accountManager->expects($this->atLeastOnce()) - ->method('getAccount') - ->with($userMock) - ->willReturn($accountMock); - - /** @var IAccountProperty&MockObject[] $propertiesMocks */ - $propertiesMocks = []; - foreach ($properties as $key => $value) { - $propertiesMocks[] = $this->getAccountPropertyMock($key, $value); - } - - if (count($properties) === 1) { - $accountMock->expects($this->atLeastOnce()) - ->method('getProperty') - ->with(array_keys($properties)[0]) - ->willReturn($propertiesMocks[array_key_first($propertiesMocks)]); - } else { - $accountMock->expects($this->atLeastOnce()) - ->method('getAllProperties') - ->willReturnCallback(function () use ($propertiesMocks) { - foreach ($propertiesMocks as $property) { - yield $property; - } - }); - } - - return [ - 'userMock' => $userMock, - 'accountMock' => $accountMock, - 'profilePropertiesMocks' => $propertiesMocks, - ]; - } - - private function getAccountPropertyMock(string $name, string $value): IAccountProperty&MockObject { - $propertyMock = $this->getMockBuilder(IAccountProperty::class) - ->disableOriginalConstructor() - ->getMock(); - $propertyMock->expects($this->any()) - ->method('getName') - ->willReturn($name); - $propertyMock->expects($this->any()) - ->method('getValue') - ->willReturn($value); - - return $propertyMock; - } -} --- a/tests/Core/Command/User/ResetPasswordTest.php +++ /dev/null @@ -1,117 +0,0 @@ -createMock(IUser::class); - $user->expects(self::once()) - ->method('setPassword') - ->with('NewPassword') - ->willReturnCallback(static function () use (&$sequence, $passwordChangeSucceeds): bool { - $sequence[] = 'setPassword'; - return $passwordChangeSucceeds; - }); - - $userManager = $this->createMock(IUserManager::class); - $userManager->expects(self::once()) - ->method('get') - ->with('alice') - ->willReturn($user); - - $eventDispatcher = $this->createMock(IEventDispatcher::class); - $eventDispatcher->expects(self::exactly(count($expectedSequence) - 1)) - ->method('dispatchTyped') - ->willReturnCallback(static function (object $event) use (&$sequence, $user): void { - if ($event instanceof BeforePasswordResetEvent) { - self::assertSame($user, $event->getUser()); - self::assertSame('NewPassword', $event->getPassword()); - $sequence[] = 'beforeReset'; - return; - } - - self::assertInstanceOf(PasswordResetEvent::class, $event); - self::assertSame($user, $event->getUser()); - self::assertSame('NewPassword', $event->getPassword()); - $sequence[] = 'afterReset'; - }); - - $input = $this->createMock(InputInterface::class); - $input->method('getArgument') - ->with('user') - ->willReturn('alice'); - $input->method('getOption') - ->with('password-from-env') - ->willReturn(true); - - $output = $this->createMock(OutputInterface::class); - $output->expects(self::once()) - ->method('writeln') - ->with($expectedOutput); - - $command = new ResetPassword( - $userManager, - $this->createStub(IAppManager::class), - $eventDispatcher, - ); - - $previousPassword = getenv('NC_PASS'); - putenv('NC_PASS=NewPassword'); - - try { - $exitCode = self::invokePrivate($command, 'execute', [$input, $output]); - } finally { - if ($previousPassword === false) { - putenv('NC_PASS'); - } else { - putenv('NC_PASS=' . $previousPassword); - } - } - - self::assertSame($expectedExitCode, $exitCode); - self::assertSame($expectedSequence, $sequence); - } - - public static function passwordResetResults(): array { - return [ - 'success dispatches both reset events around the password change' => [ - true, - 0, - ['beforeReset', 'setPassword', 'afterReset'], - 'Successfully reset password for alice', - ], - 'failure does not dispatch the after-reset event' => [ - false, - 1, - ['beforeReset', 'setPassword'], - 'Error while resetting password!', - ], - ]; - } -} --- a/tests/Core/Command/User/SettingTest.php +++ /dev/null @@ -1,452 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->config = $this->createMock(IConfig::class); - $this->connection = $this->createMock(IDBConnection::class); - $this->consoleInput = $this->createMock(InputInterface::class); - $this->consoleOutput = $this->createMock(OutputInterface::class); - } - - public function getCommand(array $methods = []) { - if (empty($methods)) { - return new Setting($this->userManager, $this->config); - } else { - $mock = $this->getMockBuilder(Setting::class) - ->setConstructorArgs([ - $this->userManager, - $this->config, - ]) - ->onlyMethods($methods) - ->getMock(); - return $mock; - } - } - - public static function dataCheckInput(): array { - return [ - [ - [['uid', 'username']], - [['ignore-missing-user', true]], - [], - false, - false, - ], - [ - [['uid', 'username']], - [['ignore-missing-user', false]], - [], - null, - 'The user "username" does not exist.', - ], - - [ - [['uid', 'username'], ['key', 'configkey']], - [['ignore-missing-user', true]], - [['--default-value', false, true]], - false, - false, - ], - [ - [['uid', 'username'], ['key', '']], - [['ignore-missing-user', true]], - [['--default-value', false, true]], - false, - 'The "default-value" option can only be used when specifying a key.', - ], - - [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['ignore-missing-user', true]], - [], - false, - false, - ], - [ - [['uid', 'username'], ['key', ''], ['value', '']], - [['ignore-missing-user', true]], - [], - false, - 'The value argument can only be used when specifying a key.', - ], - [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['ignore-missing-user', true]], - [['--default-value', false, true]], - false, - 'The value argument can not be used together with "default-value".', - ], - [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['ignore-missing-user', true], ['update-only', true]], - [], - false, - false, - ], - [ - [['uid', 'username'], ['key', 'configkey'], ['value', null]], - [['ignore-missing-user', true], ['update-only', true]], - [], - false, - 'The "update-only" option can only be used together with "value".', - ], - - [ - [['uid', 'username'], ['key', 'configkey']], - [['ignore-missing-user', true], ['delete', true]], - [], - false, - false, - ], - [ - [['uid', 'username'], ['key', '']], - [['ignore-missing-user', true], ['delete', true]], - [], - false, - 'The "delete" option can only be used when specifying a key.', - ], - [ - [['uid', 'username'], ['key', 'configkey']], - [['ignore-missing-user', true], ['delete', true]], - [['--default-value', false, true]], - false, - 'The "delete" option can not be used together with "default-value".', - ], - [ - [['uid', 'username'], ['key', 'configkey'], ['value', '']], - [['ignore-missing-user', true], ['delete', true]], - [], - false, - 'The "delete" option can not be used together with "value".', - ], - [ - [['uid', 'username'], ['key', 'configkey']], - [['ignore-missing-user', true], ['delete', true], ['error-if-not-exists', true]], - [], - false, - false, - ], - [ - [['uid', 'username'], ['key', 'configkey']], - [['ignore-missing-user', true], ['delete', false], ['error-if-not-exists', true]], - [], - false, - 'The "error-if-not-exists" option can only be used together with "delete".', - ], - ]; - } - - /** - * - * @param array $arguments - * @param array $options - * @param array $parameterOptions - * @param mixed $user - * @param string $expectedException - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckInput')] - public function testCheckInput($arguments, $options, $parameterOptions, $user, $expectedException): void { - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap($arguments); - $this->consoleInput->expects($this->any()) - ->method('getOption') - ->willReturnMap($options); - $this->consoleInput->expects($this->any()) - ->method('hasParameterOption') - ->willReturnCallback(function (string|array $config, bool $default = false) use ($parameterOptions): bool { - foreach ($parameterOptions as $parameterOption) { - if ($config === $parameterOption[0] - // Check the default value if the maps has 3 entries - && (!isset($parameterOption[2]) || $default === $parameterOption[1])) { - return end($parameterOption); - } - } - return false; - }); - - if ($user !== false) { - $this->userManager->expects($this->once()) - ->method('get') - ->willReturn($user); - } else { - $this->userManager->expects($this->never()) - ->method('get'); - } - - $command = $this->getCommand(); - try { - $this->invokePrivate($command, 'checkInput', [$this->consoleInput]); - $this->assertFalse($expectedException); - } catch (InvalidArgumentException $e) { - $this->assertEquals($expectedException, $e->getMessage()); - } - } - - public function testCheckInputExceptionCatch(): void { - $command = $this->getCommand(['checkInput']); - $command->expects($this->once()) - ->method('checkInput') - ->willThrowException(new InvalidArgumentException('test')); - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with('test'); - - $this->assertEquals(1, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function dataExecuteDelete(): array { - return [ - ['config', false, null, 0], - ['config', true, null, 0], - [null, false, null, 0], - [null, true, 'The setting does not exist for user "username".', 1], - ]; - } - - /** - * - * @param string|null $value - * @param bool $errorIfNotExists - * @param string $expectedLine - * @param int $expectedReturn - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteDelete')] - public function testExecuteDelete($value, $errorIfNotExists, $expectedLine, $expectedReturn): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - 'getUserSettings', - ]); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'username'], - ['app', 'appname'], - ['key', 'configkey'], - ]); - - $command->expects($this->once()) - ->method('checkInput'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with('username', 'appname', 'configkey', null) - ->willReturn($value); - - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturnMap([ - ['--delete', false, true], - ['--error-if-not-exists', false, $errorIfNotExists], - ]); - - if ($expectedLine === null) { - $this->consoleOutput->expects($this->never()) - ->method('writeln'); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('username', 'appname', 'configkey'); - } else { - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($expectedLine); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - } - - $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function dataExecuteSet(): array { - return [ - ['config', false, null, 0], - ['config', true, null, 0], - [null, false, null, 0], - [null, true, 'The setting does not exist for user "username".', 1], - ]; - } - - /** - * - * @param string|null $value - * @param bool $updateOnly - * @param string $expectedLine - * @param int $expectedReturn - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteSet')] - public function testExecuteSet($value, $updateOnly, $expectedLine, $expectedReturn): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - 'getUserSettings', - ]); - - $this->consoleInput->expects($this->atLeast(4)) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'username'], - ['app', 'appname'], - ['key', 'configkey'], - ['value', 'setValue'], - ]); - - $command->expects($this->once()) - ->method('checkInput'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with('username', 'appname', 'configkey', null) - ->willReturn($value); - - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturnMap([ - ['--update-only', false, $updateOnly], - ]); - - if ($expectedLine === null) { - $this->consoleOutput->expects($this->never()) - ->method('writeln'); - - $this->consoleInput->expects($this->never()) - ->method('getOption'); - - $this->config->expects($this->once()) - ->method('setUserValue') - ->with('username', 'appname', 'configkey', 'setValue'); - } else { - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($expectedLine); - $this->config->expects($this->never()) - ->method('setUserValue'); - } - - $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public static function dataExecuteGet(): array { - return [ - ['config', null, 'config', 0], - [null, 'config', 'config', 0], - [null, null, 'The setting does not exist for user "username".', 1], - ]; - } - - /** - * - * @param string|null $value - * @param string|null $defaultValue - * @param string $expectedLine - * @param int $expectedReturn - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteGet')] - public function testExecuteGet($value, $defaultValue, $expectedLine, $expectedReturn): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - 'getUserSettings', - ]); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'username'], - ['app', 'appname'], - ['key', 'configkey'], - ]); - - $command->expects($this->once()) - ->method('checkInput'); - - $this->config->expects($this->once()) - ->method('getUserValue') - ->with('username', 'appname', 'configkey', null) - ->willReturn($value); - - if ($value === null) { - if ($defaultValue === null) { - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturn(false); - } else { - $this->consoleInput->expects($this->atLeastOnce()) - ->method('hasParameterOption') - ->willReturnCallback(function (string|array $config, bool $default = false): bool { - if ($config === '--default-value' && $default === false) { - return true; - } - return false; - }); - $this->consoleInput->expects($this->once()) - ->method('getOption') - ->with('default-value') - ->willReturn($defaultValue); - } - } - - $this->consoleOutput->expects($this->once()) - ->method('writeln') - ->with($expectedLine); - - $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } - - public function testExecuteList(): void { - $command = $this->getCommand([ - 'writeArrayInOutputFormat', - 'checkInput', - 'getUserSettings', - ]); - - $this->consoleInput->expects($this->any()) - ->method('getArgument') - ->willReturnMap([ - ['uid', 'username'], - ['app', 'appname'], - ['key', ''], - ]); - - $command->expects($this->once()) - ->method('checkInput'); - $command->expects($this->once()) - ->method('getUserSettings') - ->willReturn(['settings']); - $command->expects($this->once()) - ->method('writeArrayInOutputFormat') - ->with($this->consoleInput, $this->consoleOutput, ['settings']); - - $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput])); - } -} --- a/tests/Core/Controller/AppPasswordControllerTest.php +++ /dev/null @@ -1,296 +0,0 @@ -session = $this->createMock(ISession::class); - $this->random = $this->createMock(ISecureRandom::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->credentialStore = $this->createMock(IStore::class); - $this->request = $this->createMock(IRequest::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->userSession = $this->createMock(Session::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->throttler = $this->createMock(IThrottler::class); - $this->serverConfig = $this->createMock(IConfig::class); - - $this->controller = new AppPasswordController( - 'core', - $this->request, - $this->session, - $this->random, - $this->tokenProvider, - $this->credentialStore, - $this->eventDispatcher, - $this->userSession, - $this->userManager, - $this->throttler, - $this->serverConfig, - ); - } - - public function testGetAppPasswordWithAppPassword(): void { - $this->serverConfig->method('getSystemValueBool') - ->with('auth_can_create_app_token', true) - ->willReturn(true); - $this->session->method('exists') - ->with('app_password') - ->willReturn(true); - - $this->tokenProvider->expects($this->never()) - ->method('generateToken'); - - $this->eventDispatcher->expects($this->never()) - ->method('dispatchTyped'); - - $this->expectException(OCSForbiddenException::class); - - $this->controller->getAppPassword(); - } - - public function testGetAppPasswordNoLoginCreds(): void { - $this->serverConfig->method('getSystemValueBool') - ->with('auth_can_create_app_token', true) - ->willReturn(true); - $this->session->method('exists') - ->with('app_password') - ->willReturn(false); - $this->credentialStore->method('getLoginCredentials') - ->willThrowException(new CredentialsUnavailableException()); - - $this->tokenProvider->expects($this->never()) - ->method('generateToken'); - - $this->eventDispatcher->expects($this->never()) - ->method('dispatchTyped'); - - $this->expectException(OCSForbiddenException::class); - - $this->controller->getAppPassword(); - } - - public function testGetAppPassword(): void { - $this->serverConfig->method('getSystemValueBool') - ->with('auth_can_create_app_token', true) - ->willReturn(true); - - $credentials = $this->createMock(ICredentials::class); - - $this->session->method('exists') - ->with('app_password') - ->willReturn(false); - $this->credentialStore->method('getLoginCredentials') - ->willReturn($credentials); - $credentials->method('getUid') - ->willReturn('myUID'); - $credentials->method('getPassword') - ->willReturn('myPassword'); - $credentials->method('getLoginName') - ->willReturn('myLoginName'); - $this->request->method('getHeader') - ->with('user-agent') - ->willReturn('myUA'); - $this->random->method('generate') - ->with( - 72, - ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS - )->willReturn('myToken'); - - $this->tokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'myToken', - 'myUID', - 'myLoginName', - 'myPassword', - 'myUA', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $this->controller->getAppPassword(); - } - - public function testGetAppPasswordNoPassword(): void { - $this->serverConfig->method('getSystemValueBool') - ->with('auth_can_create_app_token', true) - ->willReturn(true); - - $credentials = $this->createMock(ICredentials::class); - - $this->session->method('exists') - ->with('app_password') - ->willReturn(false); - $this->credentialStore->method('getLoginCredentials') - ->willReturn($credentials); - $credentials->method('getUid') - ->willReturn('myUID'); - $credentials->method('getPassword') - ->willThrowException(new PasswordUnavailableException()); - $credentials->method('getLoginName') - ->willReturn('myLoginName'); - $this->request->method('getHeader') - ->with('user-agent') - ->willReturn('myUA'); - $this->random->method('generate') - ->with( - 72, - ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS - )->willReturn('myToken'); - - $this->tokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'myToken', - 'myUID', - 'myLoginName', - null, - 'myUA', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $this->controller->getAppPassword(); - } - - public function testGetAppPasswordDisabledBySystemConfig(): void { - $this->serverConfig->method('getSystemValueBool') - ->with('auth_can_create_app_token', true) - ->willReturn(false); - - $this->tokenProvider->expects($this->never()) - ->method('generateToken'); - - $this->eventDispatcher->expects($this->never()) - ->method('dispatchTyped'); - - $this->expectException(OCSForbiddenException::class); - - $this->controller->getAppPassword(); - } - - public function testDeleteAppPasswordNoAppPassword(): void { - $this->session->method('exists') - ->with('app_password') - ->willReturn(false); - - $this->expectException(OCSForbiddenException::class); - - $this->controller->deleteAppPassword(); - } - - public function testDeleteAppPasswordFails(): void { - $this->session->method('exists') - ->with('app_password') - ->willReturn(true); - $this->session->method('get') - ->with('app_password') - ->willReturn('myAppPassword'); - - $this->tokenProvider->method('getToken') - ->with('myAppPassword') - ->willThrowException(new InvalidTokenException()); - - $this->expectException(OCSForbiddenException::class); - - $this->controller->deleteAppPassword(); - } - - public function testDeleteAppPasswordSuccess(): void { - $this->session->method('exists') - ->with('app_password') - ->willReturn(true); - $this->session->method('get') - ->with('app_password') - ->willReturn('myAppPassword'); - - $token = $this->createMock(IToken::class); - $this->tokenProvider->method('getToken') - ->with('myAppPassword') - ->willReturn($token); - - $token->method('getUID') - ->willReturn('myUID'); - $token->method('getId') - ->willReturn(42); - - $this->tokenProvider->expects($this->once()) - ->method('invalidateTokenById') - ->with( - 'myUID', - 42 - ); - - $result = $this->controller->deleteAppPassword(); - - $this->assertEquals(new DataResponse(), $result); - } -} --- a/tests/Core/Controller/AutoCompleteControllerTest.php +++ /dev/null @@ -1,169 +0,0 @@ -createMock(IRequest::class); - $this->collaboratorSearch = $this->createMock(ISearch::class); - $this->autoCompleteManager = $this->createMock(IManager::class); - - $this->controller = new AutoCompleteController( - 'core', - $request, - $this->collaboratorSearch, - $this->autoCompleteManager, - ); - } - - public static function searchDataProvider(): array { - return [ - [ #0 – regular search - // searchResults - [ - 'exact' => [ - 'users' => [], - 'robots' => [], - ], - 'users' => [ - ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']], - ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']], - ], - ], - // expected - [ - [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - ], - '', - 'files', - '42', - null - ], - [ #1 – missing itemtype and id - [ - 'exact' => [ - 'users' => [], - 'robots' => [], - ], - 'users' => [ - ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']], - ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']], - ], - ], - // expected - [ - [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - ], - '', - null, - null, - null - ], - [ #2 – with sorter - [ - 'exact' => [ - 'users' => [], - 'robots' => [], - ], - 'users' => [ - ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']], - ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']], - ], - ], - // expected - [ - [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - ], - '', - 'files', - '42', - 'karma|bus-factor' - ], - [ #3 – exact Match - [ - 'exact' => [ - 'users' => [ - ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']], - ], - 'robots' => [], - ], - 'users' => [ - ['label' => 'Robert R.', 'value' => ['shareWith' => 'bobby']], - ], - ], - [ - [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - [ 'id' => 'bobby', 'label' => 'Robert R.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''], - ], - 'bob', - 'files', - '42', - null - ], - [ #4 – with unique name - [ - 'exact' => [ - 'users' => [], - 'robots' => [], - ], - 'users' => [ - ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice'], 'shareWithDisplayNameUnique' => 'alica@nextcloud.com'], - ['label' => 'Alice A.', 'value' => ['shareWith' => 'alicea'], 'shareWithDisplayNameUnique' => 'alicaa@nextcloud.com'], - ], - ], - // expected - [ - [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => 'alica@nextcloud.com'], - [ 'id' => 'alicea', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => 'alicaa@nextcloud.com'], - ], - '', - 'files', - '42', - 'karma|bus-factor' - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')] - public function testGet(array $searchResults, array $expected, string $searchTerm, ?string $itemType, ?string $itemId, ?string $sorter): void { - $this->collaboratorSearch->expects($this->once()) - ->method('filteredSearch') - ->willReturn([$searchResults, false]); - - $runSorterFrequency = $sorter === null ? $this->never() : $this->once(); - $this->autoCompleteManager->expects($runSorterFrequency) - ->method('runSorters'); - - $response = $this->controller->get($searchTerm, $itemType, $itemId, $sorter); - - $list = $response->getData(); - $this->assertEquals($expected, $list); // has better error output… - $this->assertSame($expected, $list); - } -} --- a/tests/Core/Controller/AvatarControllerTest.php +++ /dev/null @@ -1,450 +0,0 @@ -avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock(); - $this->l = $this->getMockBuilder(IL10N::class)->getMock(); - $this->l->method('t')->willReturnArgument(0); - $this->userManager = $this->getMockBuilder(IUserManager::class)->getMock(); - $this->request = $this->getMockBuilder(IRequest::class)->getMock(); - $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); - $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $this->timeFactory = $this->getMockBuilder('OC\AppFramework\Utility\TimeFactory')->getMock(); - - $this->avatarMock = $this->getMockBuilder('OCP\IAvatar')->getMock(); - $this->userMock = $this->getMockBuilder(IUser::class)->getMock(); - - $this->guestAvatarController = new GuestAvatarController( - 'core', - $this->request, - $this->avatarManager, - $this->logger - ); - - $this->avatarController = new AvatarController( - 'core', - $this->request, - $this->avatarManager, - $this->l, - $this->userManager, - $this->rootFolder, - $this->logger, - 'userid', - $this->timeFactory, - $this->guestAvatarController, - ); - - // Configure userMock - $this->userMock->method('getDisplayName')->willReturn('displayName'); - $this->userMock->method('getUID')->willReturn('userId'); - $this->userManager->method('get') - ->willReturnMap([['userId', $this->userMock]]); - - $this->avatarFile = $this->getMockBuilder(ISimpleFile::class)->getMock(); - $this->avatarFile->method('getContent')->willReturn('image data'); - $this->avatarFile->method('getMimeType')->willReturn('image type'); - $this->avatarFile->method('getEtag')->willReturn('my etag'); - $this->avatarFile->method('getName')->willReturn('my name'); - $this->avatarFile->method('getMTime')->willReturn(42); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - } - - /** - * Fetch an avatar if a user has no avatar - */ - public function testGetAvatarNoAvatar(): void { - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - $this->avatarMock->method('getFile')->willThrowException(new NotFoundException()); - $response = $this->avatarController->getAvatar('userId', 32); - - //Comment out until JS is fixed - $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); - } - - /** - * Fetch the user's avatar - */ - public function testGetAvatar(): void { - $this->avatarMock->method('getFile')->willReturn($this->avatarFile); - $this->avatarManager->method('getAvatar')->with('userId')->willReturn($this->avatarMock); - $this->avatarMock->expects($this->once()) - ->method('isCustomAvatar') - ->willReturn(true); - - $response = $this->avatarController->getAvatar('userId', 32); - - $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - $this->assertArrayHasKey('Content-Type', $response->getHeaders()); - $this->assertEquals('image type', $response->getHeaders()['Content-Type']); - $this->assertArrayHasKey('X-NC-IsCustomAvatar', $response->getHeaders()); - $this->assertEquals('1', $response->getHeaders()['X-NC-IsCustomAvatar']); - - $this->assertEquals('my etag', $response->getETag()); - } - - /** - * Fetch the user's avatar - */ - public function testGetGeneratedAvatar(): void { - $this->avatarMock->method('getFile')->willReturn($this->avatarFile); - $this->avatarManager->method('getAvatar')->with('userId')->willReturn($this->avatarMock); - - $response = $this->avatarController->getAvatar('userId', 32); - - $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - $this->assertArrayHasKey('Content-Type', $response->getHeaders()); - $this->assertEquals('image type', $response->getHeaders()['Content-Type']); - $this->assertArrayHasKey('X-NC-IsCustomAvatar', $response->getHeaders()); - $this->assertEquals('0', $response->getHeaders()['X-NC-IsCustomAvatar']); - - $this->assertEquals('my etag', $response->getETag()); - } - - /** - * Fetch the avatar of a non-existing user - */ - public function testGetAvatarNoUser(): void { - $this->avatarManager - ->method('getAvatar') - ->with('userDoesNotExist') - ->willThrowException(new \Exception('user does not exist')); - - $response = $this->avatarController->getAvatar('userDoesNotExist', 32); - - //Comment out until JS is fixed - $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); - } - - public function testGetAvatarSize64(): void { - $this->avatarMock->expects($this->once()) - ->method('getFile') - ->with($this->equalTo(64)) - ->willReturn($this->avatarFile); - - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->never()) - ->method('debug'); - - $this->avatarController->getAvatar('userId', 64); - } - - public function testGetAvatarSize512(): void { - $this->avatarMock->expects($this->once()) - ->method('getFile') - ->with($this->equalTo(512)) - ->willReturn($this->avatarFile); - - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->never()) - ->method('debug'); - - $this->avatarController->getAvatar('userId', 512); - } - - /** - * Small sizes return 64 and generate a log - */ - public function testGetAvatarSizeTooSmall(): void { - $this->avatarMock->expects($this->once()) - ->method('getFile') - ->with($this->equalTo(64)) - ->willReturn($this->avatarFile); - - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->once()) - ->method('debug') - ->with('Avatar requested in deprecated size 32'); - - $this->avatarController->getAvatar('userId', 32); - } - - /** - * Avatars between 64 and 512 are upgraded to 512 - */ - public function testGetAvatarSizeBetween(): void { - $this->avatarMock->expects($this->once()) - ->method('getFile') - ->with($this->equalTo(512)) - ->willReturn($this->avatarFile); - - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->once()) - ->method('debug') - ->with('Avatar requested in deprecated size 65'); - - $this->avatarController->getAvatar('userId', 65); - } - - /** - * We do not support avatars larger than 512 - */ - public function testGetAvatarSizeTooBig(): void { - $this->avatarMock->expects($this->once()) - ->method('getFile') - ->with($this->equalTo(512)) - ->willReturn($this->avatarFile); - - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->once()) - ->method('debug') - ->with('Avatar requested in deprecated size 513'); - - $this->avatarController->getAvatar('userId', 513); - } - - /** - * Remove an avatar - */ - public function testDeleteAvatar(): void { - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $response = $this->avatarController->deleteAvatar(); - $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - } - - /** - * Test what happens if the removing of the avatar fails - */ - public function testDeleteAvatarException(): void { - $this->avatarMock->method('remove')->willThrowException(new \Exception('foo')); - $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock); - - $this->logger->expects($this->once()) - ->method('error') - ->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']); - $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST); - $this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar()); - } - - /** - * When trying to post a new avatar a path or image should be posted. - */ - public function testPostAvatarNoPathOrImage(): void { - $response = $this->avatarController->postAvatar(null); - - $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus()); - } - - /** - * Test a correct post of an avatar using POST - */ - public function testPostAvatarFile(): void { - //Create temp file - $fileName = tempnam('', 'avatarTest'); - $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName); - $this->assertTrue($copyRes); - - //Create request return - $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]]; - $this->request->method('getUploadedFile')->willReturn($reqRet); - - $response = $this->avatarController->postAvatar(null); - - //On correct upload always respond with the notsquare message - $this->assertEquals('notsquare', $response->getData()['data']); - - //File should be deleted - $this->assertFalse(file_exists($fileName)); - } - - /** - * Test invalid post os an avatar using POST - */ - public function testPostAvatarInvalidFile(): void { - //Create request return - $reqRet = ['error' => [1], 'tmp_name' => ['foo']]; - $this->request->method('getUploadedFile')->willReturn($reqRet); - - $response = $this->avatarController->postAvatar(null); - - $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus()); - } - - /** - * Check what happens when we upload a GIF - */ - public function testPostAvatarFileGif(): void { - //Create temp file - $fileName = tempnam('', 'avatarTest'); - $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName); - $this->assertTrue($copyRes); - - //Create request return - $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]]; - $this->request->method('getUploadedFile')->willReturn($reqRet); - - $response = $this->avatarController->postAvatar(null); - - $this->assertEquals('Unknown filetype', $response->getData()['data']['message']); - - //File should be deleted - $this->assertFalse(file_exists($fileName)); - } - - /** - * Test posting avatar from existing file - */ - public function testPostAvatarFromFile(): void { - //Mock node API call - $file = $this->getMockBuilder('OCP\Files\File') - ->disableOriginalConstructor()->getMock(); - $file->expects($this->once()) - ->method('getContent') - ->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $file->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/jpeg'); - $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); - $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder); - $userFolder->method('get')->willReturn($file); - - //Create request return - $response = $this->avatarController->postAvatar('avatar.jpg'); - - //On correct upload always respond with the notsquare message - $this->assertEquals('notsquare', $response->getData()['data']); - } - - /** - * Test posting avatar from existing folder - */ - public function testPostAvatarFromNoFile(): void { - $file = $this->getMockBuilder('OCP\Files\Node')->getMock(); - $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); - $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder); - $userFolder - ->method('get') - ->with('folder') - ->willReturn($file); - - //Create request return - $response = $this->avatarController->postAvatar('folder'); - - //On correct upload always respond with the notsquare message - $this->assertEquals(['data' => ['message' => 'Please select a file.']], $response->getData()); - } - - public function testPostAvatarInvalidType(): void { - $file = $this->getMockBuilder('OCP\Files\File') - ->disableOriginalConstructor()->getMock(); - $file->expects($this->never()) - ->method('getContent'); - $file->expects($this->exactly(2)) - ->method('getMimeType') - ->willReturn('text/plain'); - $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); - $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder); - $userFolder->method('get')->willReturn($file); - - $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'The selected file is not an image.']], Http::STATUS_BAD_REQUEST); - $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg')); - } - - public function testPostAvatarNotPermittedException(): void { - $file = $this->getMockBuilder('OCP\Files\File') - ->disableOriginalConstructor()->getMock(); - $file->expects($this->once()) - ->method('getContent') - ->willThrowException(new NotPermittedException()); - $file->expects($this->once()) - ->method('getMimeType') - ->willReturn('image/jpeg'); - $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); - $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder); - $userFolder->method('get')->willReturn($file); - - $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'The selected file cannot be read.']], Http::STATUS_BAD_REQUEST); - $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg')); - } - - /** - * Check for proper reply on proper crop argument - */ - public function testFileTooBig(): void { - $fileName = \OC::$SERVERROOT . '/tests/data/testimage.jpg'; - //Create request return - $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [21 * 1024 * 1024]]; - $this->request->method('getUploadedFile')->willReturn($reqRet); - - $response = $this->avatarController->postAvatar(null); - - $this->assertEquals('File is too big', $response->getData()['data']['message']); - } -} --- a/tests/Core/Controller/CSRFTokenControllerTest.php +++ /dev/null @@ -1,63 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->tokenManager = $this->createMock(CsrfTokenManager::class); - - $this->controller = new CSRFTokenController('core', $this->request, - $this->tokenManager); - } - - public function testGetToken(): void { - $this->request->method('passesStrictCookieCheck')->willReturn(true); - - $token = $this->createMock(CsrfToken::class); - $this->tokenManager->method('getToken')->willReturn($token); - $token->method('getEncryptedValue')->willReturn('toktok123'); - - $response = $this->controller->index(); - - $this->assertInstanceOf(JSONResponse::class, $response); - $this->assertSame(Http::STATUS_OK, $response->getStatus()); - $this->assertEquals([ - 'token' => 'toktok123' - ], $response->getData()); - } - - public function testGetTokenNoStrictSameSiteCookie(): void { - $this->request->method('passesStrictCookieCheck')->willReturn(false); - - $response = $this->controller->index(); - - $this->assertInstanceOf(JSONResponse::class, $response); - $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus()); - } -} --- a/tests/Core/Controller/ChangePasswordControllerTest.php +++ /dev/null @@ -1,194 +0,0 @@ -userManager = $this->createMock(\OC\User\Manager::class); - $this->userSession = $this->createMock(Session::class); - $this->groupManager = $this->createMock(\OC\Group\Manager::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->l = $this->createMock(IL10N::class); - $this->l->method('t')->willReturnArgument(0); - - /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject $request */ - $request = $this->createMock(IRequest::class); - - $this->controller = new ChangePasswordController( - 'core', - $request, - $this->userId, - $this->userManager, - $this->userSession, - $this->groupManager, - $this->appManager, - $this->l - ); - } - - public function testChangePersonalPasswordWrongPassword(): void { - $this->userSession->expects($this->once()) - ->method('getLoginName') - ->willReturn($this->loginName); - - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with($this->loginName, 'old') - ->willReturn(false); - - $expects = new JSONResponse([ - 'status' => 'error', - 'data' => [ - 'message' => 'Wrong password', - ], - ]); - $expects->throttle(); - - $actual = $this->controller->changePersonalPassword('old', 'new'); - $this->assertEquals($expects, $actual); - } - - public function testChangePersonalPasswordCommonPassword(): void { - $this->userSession->expects($this->once()) - ->method('getLoginName') - ->willReturn($this->loginName); - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with($this->loginName, 'old') - ->willReturn($user); - - $user->expects($this->once()) - ->method('setPassword') - ->with('new') - ->willThrowException(new HintException('Common password')); - - $expects = new JSONResponse([ - 'status' => 'error', - 'data' => [ - 'message' => 'Common password', - ], - ]); - - $actual = $this->controller->changePersonalPassword('old', 'new'); - $this->assertEquals($expects, $actual); - } - - public function testChangePersonalPasswordNoNewPassword(): void { - $this->userSession->expects($this->once()) - ->method('getLoginName') - ->willReturn($this->loginName); - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with($this->loginName, 'old') - ->willReturn($user); - - $expects = [ - 'status' => 'error', - 'data' => [ - 'message' => 'Unable to change personal password', - ], - ]; - - $res = $this->controller->changePersonalPassword('old'); - - $this->assertEquals($expects, $res->getData()); - } - - public function testChangePersonalPasswordCantSetPassword(): void { - $this->userSession->expects($this->once()) - ->method('getLoginName') - ->willReturn($this->loginName); - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with($this->loginName, 'old') - ->willReturn($user); - - $user->expects($this->once()) - ->method('setPassword') - ->with('new') - ->willReturn(false); - - $expects = new JSONResponse([ - 'status' => 'error', - 'data' => [ - 'message' => 'Unable to change personal password', - ], - ]); - - $actual = $this->controller->changePersonalPassword('old', 'new'); - $this->assertEquals($expects, $actual); - } - - public function testChangePersonalPassword(): void { - $this->userSession->expects($this->once()) - ->method('getLoginName') - ->willReturn($this->loginName); - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with($this->loginName, 'old') - ->willReturn($user); - - $user->expects($this->once()) - ->method('setPassword') - ->with('new') - ->willReturn(true); - - $this->userSession->expects($this->once()) - ->method('updateSessionTokenPassword') - ->with('new'); - - $expects = new JSONResponse([ - 'status' => 'success', - 'data' => [ - 'message' => 'Saved', - ], - ]); - - $actual = $this->controller->changePersonalPassword('old', 'new'); - $this->assertEquals($expects, $actual); - } -} --- a/tests/Core/Controller/ClientFlowLoginControllerTest.php +++ /dev/null @@ -1,715 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->l10n = $this->createMock(IL10N::class); - $this->l10n - ->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - $this->defaults = $this->createMock(Defaults::class); - $this->session = $this->createMock(ISession::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->random = $this->createMock(ISecureRandom::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->clientMapper = $this->createMock(ClientMapper::class); - $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->config = $this->createMock(IConfig::class); - $this->initialState = $this->createMock(IInitialState::class); - - $this->clientFlowLoginController = new ClientFlowLoginController( - 'core', - $this->request, - $this->userSession, - $this->l10n, - $this->defaults, - $this->session, - $this->tokenProvider, - $this->random, - $this->urlGenerator, - $this->clientMapper, - $this->accessTokenMapper, - $this->crypto, - $this->eventDispatcher, - $this->timeFactory, - $this->config, - $this->initialState, - ); - } - - public function testShowAuthPickerPageNoClientOrOauthRequest(): void { - $expected = new StandaloneTemplateResponse( - 'core', - 'error', - [ - 'errors' - => [ - [ - 'error' => 'Access Forbidden', - 'hint' => 'Invalid request', - ], - ], - ], - 'guest' - ); - - $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage()); - } - - public function testShowAuthPickerPageWithOcsHeader(): void { - $this->request - ->method('getHeader') - ->willReturnMap([ - ['user-agent', 'Mac OS X Sync Client'], - ['OCS-APIREQUEST', 'true'], - ]); - $this->random - ->expects($this->once()) - ->method('generate') - ->with( - 64, - ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS - ) - ->willReturn('StateToken'); - $this->session - ->expects($this->once()) - ->method('set') - ->with('client.flow.state.token', 'StateToken'); - $this->session - ->expects($this->atLeastOnce()) - ->method('get') - ->with('oauth.state') - ->willReturn('OauthStateToken'); - $this->defaults - ->expects($this->once()) - ->method('getName') - ->willReturn('ExampleCloud'); - $this->request - ->expects($this->once()) - ->method('getServerHost') - ->willReturn('example.com'); - $this->request - ->method('getServerProtocol') - ->willReturn('https'); - - $initialState = []; - $this->initialState->expects($this->exactly(2)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$initialState): void { - $initialState[] = func_get_args(); - }); - - $expected = new StandaloneTemplateResponse( - 'core', - 'loginflow', - renderAs: 'guest' - ); - $csp = new ContentSecurityPolicy(); - $csp->addAllowedFormActionDomain('nc://*'); - $expected->setContentSecurityPolicy($csp); - $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage()); - self::assertEquals([ - ['loginFlowState', 'auth'], - [ - 'loginFlowAuth', [ - 'client' => 'Mac OS X Sync Client', - 'clientIdentifier' => '', - 'instanceName' => 'ExampleCloud', - 'stateToken' => 'StateToken', - 'serverHost' => 'https://example.com', - 'oauthState' => 'OauthStateToken', - 'direct' => false, - 'providedRedirectUri' => '', - 'appTokenUrl' => '', - 'loginRedirectUrl' => '', - ], - ], - ], $initialState); - } - - public function testShowAuthPickerPageWithOauth(): void { - $this->request - ->method('getHeader') - ->willReturnMap([ - ['user-agent', 'Mac OS X Sync Client'], - ['OCS-APIREQUEST', 'false'], - ]); - $client = new Client(); - $client->name = 'My external service'; - $client->redirectUri = 'https://example.com/redirect.php'; - $this->clientMapper - ->expects($this->once()) - ->method('getByIdentifier') - ->with('MyClientIdentifier') - ->willReturn($client); - $this->random - ->expects($this->once()) - ->method('generate') - ->with( - 64, - ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS - ) - ->willReturn('StateToken'); - $this->session - ->expects($this->once()) - ->method('set') - ->with('client.flow.state.token', 'StateToken'); - $this->session - ->expects($this->atLeastOnce()) - ->method('get') - ->with('oauth.state') - ->willReturn('OauthStateToken'); - $this->defaults - ->expects($this->once()) - ->method('getName') - ->willReturn('ExampleCloud'); - $this->request - ->expects($this->once()) - ->method('getServerHost') - ->willReturn('example.com'); - $this->request - ->method('getServerProtocol') - ->willReturn('https'); - - $initialState = []; - $this->initialState->expects($this->exactly(2)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$initialState): void { - $initialState[] = func_get_args(); - }); - - $expected = new StandaloneTemplateResponse( - 'core', - 'loginflow', - renderAs: 'guest' - ); - $csp = new ContentSecurityPolicy(); - $csp->addAllowedFormActionDomain('https://example.com/redirect.php'); - $expected->setContentSecurityPolicy($csp); - $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage('MyClientIdentifier')); - self::assertEquals([ - ['loginFlowState', 'auth'], - [ - 'loginFlowAuth', [ - 'client' => 'My external service', - 'clientIdentifier' => 'MyClientIdentifier', - 'instanceName' => 'ExampleCloud', - 'stateToken' => 'StateToken', - 'serverHost' => 'https://example.com', - 'oauthState' => 'OauthStateToken', - 'direct' => false, - 'providedRedirectUri' => '', - 'appTokenUrl' => '', - 'loginRedirectUrl' => '', - ], - ], - ], $initialState); - } - - public function testGenerateAppPasswordWithInvalidToken(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('OtherToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - - $expected = new StandaloneTemplateResponse( - 'core', - '403', - [ - 'message' => 'State token does not match', - ], - 'guest' - ); - $expected->setStatus(Http::STATUS_FORBIDDEN); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } - - public function testGenerateAppPasswordWithSessionNotAvailableException(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('MyStateToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - $this->session - ->expects($this->once()) - ->method('getId') - ->willThrowException(new SessionNotAvailableException()); - - $expected = new Response(); - $expected->setStatus(Http::STATUS_FORBIDDEN); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } - - public function testGenerateAppPasswordWithInvalidTokenException(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('MyStateToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - $this->session - ->expects($this->once()) - ->method('getId') - ->willReturn('SessionId'); - $this->tokenProvider - ->expects($this->once()) - ->method('getToken') - ->with('SessionId') - ->willThrowException(new InvalidTokenException()); - - $expected = new Response(); - $expected->setStatus(Http::STATUS_FORBIDDEN); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } - - public function testGeneratePasswordWithPassword(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('MyStateToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - $this->session - ->expects($this->once()) - ->method('getId') - ->willReturn('SessionId'); - $myToken = $this->createMock(IToken::class); - $myToken - ->expects($this->once()) - ->method('getLoginName') - ->willReturn('MyLoginName'); - $this->tokenProvider - ->expects($this->once()) - ->method('getToken') - ->with('SessionId') - ->willReturn($myToken); - $this->tokenProvider - ->expects($this->once()) - ->method('getPassword') - ->with($myToken, 'SessionId') - ->willReturn('MyPassword'); - $this->random - ->expects($this->once()) - ->method('generate') - ->with(72) - ->willReturn('MyGeneratedToken'); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->tokenProvider - ->expects($this->once()) - ->method('generateToken') - ->with( - 'MyGeneratedToken', - 'MyUid', - 'MyLoginName', - 'MyPassword', - 'unknown', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - $this->request - ->expects($this->once()) - ->method('getServerProtocol') - ->willReturn('http'); - $this->request - ->expects($this->once()) - ->method('getServerHost') - ->willReturn('example.com'); - $this->request - ->expects($this->any()) - ->method('getHeader') - ->willReturn(''); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $expected = new RedirectResponse('nc://login/server:http://example.com&user:MyLoginName&password:MyGeneratedToken'); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } - - /** - * @param string $redirectUri - * @param string $redirectUrl - * - * @testWith - * ["https://example.com/redirect.php", "https://example.com/redirect.php?state=MyOauthState&code=MyAccessCode"] - * ["https://example.com/redirect.php?hello=world", "https://example.com/redirect.php?hello=world&state=MyOauthState&code=MyAccessCode"] - * - */ - public function testGeneratePasswordWithPasswordForOauthClient($redirectUri, $redirectUrl): void { - $this->session - ->method('get') - ->willReturnMap([ - ['client.flow.state.token', 'MyStateToken'], - ['oauth.state', 'MyOauthState'], - ]); - $calls = [ - 'client.flow.state.token', - 'oauth.state', - ]; - $this->session - ->method('remove') - ->willReturnCallback(function ($key) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $key); - }); - $this->session - ->expects($this->once()) - ->method('getId') - ->willReturn('SessionId'); - $myToken = $this->createMock(IToken::class); - $myToken - ->expects($this->once()) - ->method('getLoginName') - ->willReturn('MyLoginName'); - $this->tokenProvider - ->expects($this->once()) - ->method('getToken') - ->with('SessionId') - ->willReturn($myToken); - $this->tokenProvider - ->expects($this->once()) - ->method('getPassword') - ->with($myToken, 'SessionId') - ->willReturn('MyPassword'); - $this->random - ->method('generate') - ->willReturnMap([ - [72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS, 'MyGeneratedToken'], - [128, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS, 'MyAccessCode'], - ]); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $token = $this->createMock(IToken::class); - $this->tokenProvider - ->expects($this->once()) - ->method('generateToken') - ->with( - 'MyGeneratedToken', - 'MyUid', - 'MyLoginName', - 'MyPassword', - 'My OAuth client', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ) - ->willReturn($token); - $client = new Client(); - $client->id = 42; - $client->name = 'My OAuth client'; - $client->redirectUri = $redirectUri; - $this->clientMapper - ->expects($this->once()) - ->method('getByIdentifier') - ->with('MyClientIdentifier') - ->willReturn($client); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $expected = new RedirectResponse($redirectUrl); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken', 'MyClientIdentifier')); - } - - public function testGeneratePasswordWithoutPassword(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('MyStateToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - $this->session - ->expects($this->once()) - ->method('getId') - ->willReturn('SessionId'); - $myToken = $this->createMock(IToken::class); - $myToken - ->expects($this->once()) - ->method('getLoginName') - ->willReturn('MyLoginName'); - $this->tokenProvider - ->expects($this->once()) - ->method('getToken') - ->with('SessionId') - ->willReturn($myToken); - $this->tokenProvider - ->expects($this->once()) - ->method('getPassword') - ->with($myToken, 'SessionId') - ->willThrowException(new PasswordlessTokenException()); - $this->random - ->expects($this->once()) - ->method('generate') - ->with(72) - ->willReturn('MyGeneratedToken'); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->tokenProvider - ->expects($this->once()) - ->method('generateToken') - ->with( - 'MyGeneratedToken', - 'MyUid', - 'MyLoginName', - null, - 'unknown', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - $this->request - ->expects($this->once()) - ->method('getServerProtocol') - ->willReturn('http'); - $this->request - ->expects($this->once()) - ->method('getServerHost') - ->willReturn('example.com'); - $this->request - ->expects($this->any()) - ->method('getHeader') - ->willReturn(''); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $expected = new RedirectResponse('nc://login/server:http://example.com&user:MyLoginName&password:MyGeneratedToken'); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } - - public static function dataGeneratePasswordWithHttpsProxy(): array { - return [ - [ - [ - ['X-Forwarded-Proto', 'http'], - ['X-Forwarded-Ssl', 'off'], - ['user-agent', ''], - ], - 'http', - 'http', - ], - [ - [ - ['X-Forwarded-Proto', 'http'], - ['X-Forwarded-Ssl', 'off'], - ['user-agent', ''], - ], - 'https', - 'https', - ], - [ - [ - ['X-Forwarded-Proto', 'https'], - ['X-Forwarded-Ssl', 'off'], - ['user-agent', ''], - ], - 'http', - 'https', - ], - [ - [ - ['X-Forwarded-Proto', 'https'], - ['X-Forwarded-Ssl', 'on'], - ['user-agent', ''], - ], - 'http', - 'https', - ], - [ - [ - ['X-Forwarded-Proto', 'http'], - ['X-Forwarded-Ssl', 'on'], - ['user-agent', ''], - ], - 'http', - 'https', - ], - ]; - } - - /** - * @param array $headers - * @param string $protocol - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGeneratePasswordWithHttpsProxy')] - public function testGeneratePasswordWithHttpsProxy(array $headers, $protocol, $expected): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('client.flow.state.token') - ->willReturn('MyStateToken'); - $this->session - ->expects($this->once()) - ->method('remove') - ->with('client.flow.state.token'); - $this->session - ->expects($this->once()) - ->method('getId') - ->willReturn('SessionId'); - $myToken = $this->createMock(IToken::class); - $myToken - ->expects($this->once()) - ->method('getLoginName') - ->willReturn('MyLoginName'); - $this->tokenProvider - ->expects($this->once()) - ->method('getToken') - ->with('SessionId') - ->willReturn($myToken); - $this->tokenProvider - ->expects($this->once()) - ->method('getPassword') - ->with($myToken, 'SessionId') - ->willReturn('MyPassword'); - $this->random - ->expects($this->once()) - ->method('generate') - ->with(72) - ->willReturn('MyGeneratedToken'); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->tokenProvider - ->expects($this->once()) - ->method('generateToken') - ->with( - 'MyGeneratedToken', - 'MyUid', - 'MyLoginName', - 'MyPassword', - 'unknown', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - $this->request - ->expects($this->once()) - ->method('getServerProtocol') - ->willReturn($protocol); - $this->request - ->expects($this->once()) - ->method('getServerHost') - ->willReturn('example.com'); - $this->request - ->expects($this->atLeastOnce()) - ->method('getHeader') - ->willReturnMap($headers); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped'); - - $expected = new RedirectResponse('nc://login/server:' . $expected . '://example.com&user:MyLoginName&password:MyGeneratedToken'); - $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken')); - } -} --- a/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php +++ /dev/null @@ -1,390 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->loginFlowV2Service = $this->createMock(LoginFlowV2Service::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->session = $this->createMock(ISession::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->random = $this->createMock(ISecureRandom::class); - $this->defaults = $this->createMock(Defaults::class); - $this->initialState = $this->createMock(IInitialState::class); - $this->l = $this->createMock(IL10N::class); - $this->l - ->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - $this->controller = new ClientFlowLoginV2Controller( - 'core', - $this->request, - $this->loginFlowV2Service, - $this->urlGenerator, - $this->session, - $this->userSession, - $this->random, - $this->defaults, - 'user', - $this->l, - $this->initialState, - ); - } - - public function testPollInvalid(): void { - $this->loginFlowV2Service->method('poll') - ->with('token') - ->willThrowException(new LoginFlowV2NotFoundException()); - - $result = $this->controller->poll('token'); - - $this->assertSame([], $result->getData()); - $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); - } - - public function testPollValid(): void { - $creds = new LoginFlowV2Credentials('server', 'login', 'pass'); - $this->loginFlowV2Service->method('poll') - ->with('token') - ->willReturn($creds); - - $result = $this->controller->poll('token'); - - $this->assertSame($creds->jsonSerialize(), $result->getData()); - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - } - - public function testLandingInvalid(): void { - $this->session->expects($this->never()) - ->method($this->anything()); - - $this->loginFlowV2Service->method('startLoginFlow') - ->with('token') - ->willReturn(false); - - $result = $this->controller->landing('token'); - - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - $this->assertInstanceOf(StandaloneTemplateResponse::class, $result); - } - - public function testLandingValid(): void { - $this->session->expects($this->once()) - ->method('set') - ->with('client.flow.v2.login.token', 'token'); - - $this->loginFlowV2Service->method('startLoginFlow') - ->with('token') - ->willReturn(true); - - $this->urlGenerator->method('linkToRouteAbsolute') - ->with('core.ClientFlowLoginV2.showAuthPickerPage') - ->willReturn('https://server/path'); - - $result = $this->controller->landing('token'); - - $this->assertInstanceOf(RedirectResponse::class, $result); - $this->assertSame(Http::STATUS_SEE_OTHER, $result->getStatus()); - $this->assertSame('https://server/path', $result->getRedirectURL()); - } - - public function testShowAuthPickerNoLoginToken(): void { - $this->session->method('get') - ->willReturn(null); - - $result = $this->controller->showAuthPickerPage(); - - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testShowAuthPickerInvalidLoginToken(): void { - $this->session->method('get') - ->with('client.flow.v2.login.token') - ->willReturn('loginToken'); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2NotFoundException()); - - $result = $this->controller->showAuthPickerPage(); - - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testShowAuthPickerForbiddenUserClient() { - $this->session->method('get') - ->with('client.flow.v2.login.token') - ->willReturn('loginToken'); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2ClientForbiddenException()); - - $result = $this->controller->showAuthPickerPage(); - - $this->assertInstanceOf(StandaloneTemplateResponse::class, $result); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - $this->assertSame('Please use original client', $result->getParams()['message']); - } - - public function testShowAuthPickerValidLoginToken(): void { - $this->session->method('get') - ->with('client.flow.v2.login.token') - ->willReturn('loginToken'); - - $flow = new LoginFlowV2(); - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willReturn($flow); - - $this->random->method('generate') - ->with(64, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS) - ->willReturn('random'); - $this->session->expects($this->once()) - ->method('set') - ->with('client.flow.v2.state.token', 'random'); - - $this->controller->showAuthPickerPage(); - } - - public function testGrantPageNoStateToken(): void { - $result = $this->controller->grantPage(null); - - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testGrantPageInvalidStateToken(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - return null; - }); - - $result = $this->controller->grantPage('stateToken'); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testGrantPageInvalidLoginToken(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2NotFoundException()); - - $result = $this->controller->grantPage('stateToken'); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testGrantPageForbiddenUserClient() { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2ClientForbiddenException()); - - $result = $this->controller->grantPage('stateToken'); - - $this->assertInstanceOf(StandaloneTemplateResponse::class, $result); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - $this->assertSame('Please use original client', $result->getParams()['message']); - } - - public function testGrantPageValid(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('uid'); - $user->method('getDisplayName') - ->willReturn('display name'); - $this->userSession->method('getUser') - ->willReturn($user); - - $flow = new LoginFlowV2(); - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willReturn($flow); - - $result = $this->controller->grantPage('stateToken'); - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - } - - public function testGenerateAppPasswordInvalidStateToken(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - return null; - }); - - $result = $this->controller->generateAppPassword('stateToken'); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testGenerateAppPassworInvalidLoginToken(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2NotFoundException()); - - $result = $this->controller->generateAppPassword('stateToken'); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - } - - public function testGenerateAppPasswordForbiddenUserClient() { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willThrowException(new LoginFlowV2ClientForbiddenException()); - - $result = $this->controller->generateAppPassword('stateToken'); - - $this->assertInstanceOf(StandaloneTemplateResponse::class, $result); - $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); - $this->assertSame('Please use original client', $result->getParams()['message']); - } - - public function testGenerateAppPassworValid(): void { - $this->session->method('get') - ->willReturnCallback(function ($name) { - if ($name === 'client.flow.v2.state.token') { - return 'stateToken'; - } - if ($name === 'client.flow.v2.login.token') { - return 'loginToken'; - } - return null; - }); - - $flow = new LoginFlowV2(); - $this->loginFlowV2Service->method('getByLoginToken') - ->with('loginToken') - ->willReturn($flow); - - $clearedState = false; - $clearedLogin = false; - $this->session->method('remove') - ->willReturnCallback(function ($name) use (&$clearedLogin, &$clearedState): void { - if ($name === 'client.flow.v2.state.token') { - $clearedState = true; - } - if ($name === 'client.flow.v2.login.token') { - $clearedLogin = true; - } - }); - - $this->session->method('getId') - ->willReturn('sessionId'); - - $this->loginFlowV2Service->expects($this->once()) - ->method('flowDone') - ->with( - 'loginToken', - 'sessionId', - 'https://server', - 'user' - )->willReturn(true); - - $this->request->method('getServerProtocol') - ->willReturn('https'); - $this->request->method('getRequestUri') - ->willReturn('/login/v2'); - $this->request->method('getServerHost') - ->willReturn('server'); - - $result = $this->controller->generateAppPassword('stateToken'); - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - - $this->assertTrue($clearedLogin); - $this->assertTrue($clearedState); - } -} --- a/tests/Core/Controller/ContactsMenuControllerTest.php +++ /dev/null @@ -1,128 +0,0 @@ -createMock(IRequest::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->contactsManager = $this->createMock(Manager::class); - $this->teamManager = $this->createMock(ITeamManager::class); - - $this->controller = new ContactsMenuController( - $request, - $this->userSession, - $this->contactsManager, - $this->teamManager, - ); - } - - public function testIndex(): void { - $user = $this->createMock(IUser::class); - $entries = [ - $this->createMock(IEntry::class), - $this->createMock(IEntry::class), - ]; - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->contactsManager->expects($this->once()) - ->method('getEntries') - ->with($this->equalTo($user), $this->equalTo(null)) - ->willReturn($entries); - - $response = $this->controller->index(); - - $this->assertEquals($entries, $response); - } - - public function testIndex_withTeam(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('current-user'); - - $entries = [ - $this->createMock(IEntry::class), - $this->createMock(IEntry::class), - ]; - $entries[0]->method('getProperty') - ->with('UID') - ->willReturn('member1'); - $entries[1]->method('getProperty') - ->with('UID') - ->willReturn('member2'); - - $this->userSession->expects($this->atLeastOnce()) - ->method('getUser') - ->willReturn($user); - $this->contactsManager->expects($this->once()) - ->method('getEntries') - ->with($this->equalTo($user), $this->equalTo(null)) - ->willReturn(['contacts' => $entries]); - - $this->teamManager->expects($this->once()) - ->method('getMembersOfTeam') - ->with('team-id', 'current-user') - ->willReturn(['member1' => 'Member 1', 'member3' => 'Member 3']); - - $response = $this->controller->index(teamId: 'team-id'); - - $this->assertEquals([$entries[0]], $response['contacts']); - } - - public function testFindOne(): void { - $user = $this->createMock(IUser::class); - $entry = $this->createMock(IEntry::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->contactsManager->expects($this->once()) - ->method('findOne') - ->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase')) - ->willReturn($entry); - - $response = $this->controller->findOne(42, 'test-search-phrase'); - - $this->assertEquals($entry, $response); - } - - public function testFindOne404(): void { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->contactsManager->expects($this->once()) - ->method('findOne') - ->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase')) - ->willReturn(null); - - $response = $this->controller->findOne(42, 'test-search-phrase'); - - $this->assertEquals([], $response->getData()); - $this->assertEquals(404, $response->getStatus()); - } -} --- a/tests/Core/Controller/CssControllerTest.php +++ /dev/null @@ -1,171 +0,0 @@ -createMock(Factory::class); - $this->appData = $this->createMock(AppData::class); - $factory->expects($this->once()) - ->method('get') - ->with('css') - ->willReturn($this->appData); - - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject $timeFactory */ - $timeFactory = $this->createMock(ITimeFactory::class); - $timeFactory->method('getTime') - ->willReturn(1337); - - $this->request = $this->createMock(IRequest::class); - - $this->controller = new CssController( - 'core', - $this->request, - $factory, - $timeFactory - ); - } - - public function testNoCssFolderForApp(): void { - $this->appData->method('getFolder') - ->with('myapp') - ->willThrowException(new NotFoundException()); - - $result = $this->controller->getCss('file.css', 'myapp'); - - $this->assertInstanceOf(NotFoundResponse::class, $result); - } - - public function testNoCssFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->willThrowException(new NotFoundException()); - - $result = $this->controller->getCss('file.css', 'myapp'); - - $this->assertInstanceOf(NotFoundResponse::class, $result); - } - - public function testGetFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $file->method('getName')->willReturn('my name'); - $file->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->with('file.css') - ->willReturn($file); - - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getCss('file.css', 'myapp'); - $this->assertEquals($expected, $result); - } - - public function testGetGzipFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $gzipFile = $this->createMock(ISimpleFile::class); - $gzipFile->method('getName')->willReturn('my name'); - $gzipFile->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->with('file.css.gzip') - ->willReturn($gzipFile); - - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip, deflate'); - - $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'text/css']); - $expected->addHeader('Content-Encoding', 'gzip'); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getCss('file.css', 'myapp'); - $this->assertEquals($expected, $result); - } - - public function testGetGzipFileNotFound(): void { - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $file->method('getName')->willReturn('my name'); - $file->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->willReturnCallback( - function ($fileName) use ($file) { - if ($fileName === 'file.css') { - return $file; - } - throw new NotFoundException(); - } - ); - - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip, deflate'); - - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getCss('file.css', 'myapp'); - $this->assertEquals($expected, $result); - } -} --- a/tests/Core/Controller/GuestAvatarControllerTest.php +++ /dev/null @@ -1,97 +0,0 @@ -logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $this->request = $this->getMockBuilder(IRequest::class)->getMock(); - $this->avatar = $this->getMockBuilder(IAvatar::class)->getMock(); - $this->avatarManager = $this->getMockBuilder(IAvatarManager::class)->getMock(); - $this->file = $this->getMockBuilder(ISimpleFile::class)->getMock(); - $this->file->method('getName')->willReturn('my name'); - $this->file->method('getMTime')->willReturn(42); - $this->guestAvatarController = new GuestAvatarController( - 'core', - $this->request, - $this->avatarManager, - $this->logger - ); - } - - /** - * Tests getAvatar returns the guest avatar. - */ - public function testGetAvatar(): void { - $this->avatarManager->expects($this->once()) - ->method('getGuestAvatar') - ->with('Peter') - ->willReturn($this->avatar); - - $this->avatar->expects($this->once()) - ->method('getFile') - ->with(512) - ->willReturn($this->file); - - $this->file->method('getMimeType') - ->willReturn('image/svg+xml'); - - $response = $this->guestAvatarController->getAvatar('Peter', 128); - - $this->assertGreaterThanOrEqual(201, $response->getStatus()); - $this->assertInstanceOf(FileDisplayResponse::class, $response); - } -} --- a/tests/Core/Controller/JsControllerTest.php +++ /dev/null @@ -1,171 +0,0 @@ -createMock(Factory::class); - $this->appData = $this->createMock(AppData::class); - $factory->expects($this->once()) - ->method('get') - ->with('js') - ->willReturn($this->appData); - - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject $timeFactory */ - $timeFactory = $this->createMock(ITimeFactory::class); - $timeFactory->method('getTime') - ->willReturn(1337); - - $this->request = $this->createMock(IRequest::class); - - $this->controller = new JsController( - 'core', - $this->request, - $factory, - $timeFactory - ); - } - - public function testNoCssFolderForApp(): void { - $this->appData->method('getFolder') - ->with('myapp') - ->willThrowException(new NotFoundException()); - - $result = $this->controller->getJs('file.css', 'myapp'); - - $this->assertInstanceOf(NotFoundResponse::class, $result); - } - - public function testNoCssFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->willThrowException(new NotFoundException()); - - $result = $this->controller->getJs('file.css', 'myapp'); - - $this->assertInstanceOf(NotFoundResponse::class, $result); - } - - public function testGetFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $file->method('getName')->willReturn('my name'); - $file->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->with('file.js') - ->willReturn($file); - - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getJs('file.js', 'myapp'); - $this->assertEquals($expected, $result); - } - - public function testGetGzipFile(): void { - $folder = $this->createMock(ISimpleFolder::class); - $gzipFile = $this->createMock(ISimpleFile::class); - $gzipFile->method('getName')->willReturn('my name'); - $gzipFile->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->with('file.js.gzip') - ->willReturn($gzipFile); - - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip, deflate'); - - $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); - $expected->addHeader('Content-Encoding', 'gzip'); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getJs('file.js', 'myapp'); - $this->assertEquals($expected, $result); - } - - public function testGetGzipFileNotFound(): void { - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $file->method('getName')->willReturn('my name'); - $file->method('getMTime')->willReturn(42); - $this->appData->method('getFolder') - ->with('myapp') - ->willReturn($folder); - - $folder->method('getFile') - ->willReturnCallback( - function ($fileName) use ($file) { - if ($fileName === 'file.js') { - return $file; - } - throw new NotFoundException(); - } - ); - - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip, deflate'); - - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); - $expected->addHeader('Cache-Control', 'max-age=31536000, immutable'); - $expires = new \DateTime(); - $expires->setTimestamp(1337); - $expires->add(new \DateInterval('PT31536000S')); - $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - - $result = $this->controller->getJs('file.js', 'myapp'); - $this->assertEquals($expected, $result); - } -} --- a/tests/Core/Controller/LoginControllerTest.php +++ /dev/null @@ -1,727 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->userManager = $this->createMock(\OC\User\Manager::class); - $this->config = $this->createMock(IConfig::class); - $this->session = $this->createMock(ISession::class); - $this->userSession = $this->createMock(Session::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->twoFactorManager = $this->createMock(Manager::class); - $this->defaults = $this->createMock(Defaults::class); - $this->throttler = $this->createMock(IThrottler::class); - $this->initialState = $this->createMock(IInitialState::class); - $this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class); - $this->notificationManager = $this->createMock(IManager::class); - $this->l = $this->createMock(IL10N::class); - $this->appManager = $this->createMock(IAppManager::class); - - $this->l->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - - $this->request->method('getRemoteAddress') - ->willReturn('1.2.3.4'); - $this->request->method('getHeader') - ->with('Origin') - ->willReturn('domain.example.com'); - $this->throttler->method('getDelay') - ->with( - $this->equalTo('1.2.3.4'), - $this->equalTo('') - )->willReturn(1000); - - $this->loginController = new LoginController( - 'core', - $this->request, - $this->userManager, - $this->config, - $this->session, - $this->userSession, - $this->urlGenerator, - $this->defaults, - $this->throttler, - $this->initialState, - $this->webAuthnManager, - $this->notificationManager, - $this->l, - $this->appManager, - ); - } - - public function testLogoutWithoutToken(): void { - $this->request - ->expects($this->once()) - ->method('getCookie') - ->with('nc_token') - ->willReturn(null); - $this->request - ->method('getServerProtocol') - ->willReturn('https'); - $this->request - ->expects($this->once()) - ->method('isUserAgent') - ->willReturn(false); - $this->config - ->expects($this->never()) - ->method('deleteUserValue'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.login.showLoginForm') - ->willReturn('/login'); - - $expected = new RedirectResponse('/login'); - $expected->addHeader('Clear-Site-Data', '"cache", "storage"'); - $this->assertEquals($expected, $this->loginController->logout()); - } - - public function testLogoutNoClearSiteData(): void { - $this->request - ->expects($this->once()) - ->method('getCookie') - ->with('nc_token') - ->willReturn(null); - $this->request - ->method('getServerProtocol') - ->willReturn('https'); - $this->request - ->expects($this->once()) - ->method('isUserAgent') - ->willReturn(true); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.login.showLoginForm') - ->willReturn('/login'); - - $expected = new RedirectResponse('/login'); - $this->assertEquals($expected, $this->loginController->logout()); - } - - public function testLogoutWithToken(): void { - $this->request - ->expects($this->once()) - ->method('getCookie') - ->with('nc_token') - ->willReturn('MyLoginToken'); - $this->request - ->method('getServerProtocol') - ->willReturn('https'); - $this->request - ->expects($this->once()) - ->method('isUserAgent') - ->willReturn(false); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('JohnDoe'); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->config - ->expects($this->once()) - ->method('deleteUserValue') - ->with('JohnDoe', 'login_token', 'MyLoginToken'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.login.showLoginForm') - ->willReturn('/login'); - - $expected = new RedirectResponse('/login'); - $expected->addHeader('Clear-Site-Data', '"cache", "storage"'); - $expected->addHeader('X-User-Id', 'JohnDoe'); - $this->assertEquals($expected, $this->loginController->logout()); - } - - public function testShowLoginFormForLoggedInUsers(): void { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToDefaultPageUrl') - ->willReturn('/default/foo'); - - $expectedResponse = new RedirectResponse('/default/foo'); - $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', '')); - } - - public function testShowLoginFormWithErrorsInSession(): void { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $this->session - ->expects($this->once()) - ->method('get') - ->with('loginMessages') - ->willReturn( - [ - [ - 'ErrorArray1', - 'ErrorArray2', - ], - [ - 'MessageArray1', - 'MessageArray2', - ], - ] - ); - - $calls = [ - [ - 'loginMessages', - [ - 'MessageArray1', - 'MessageArray2', - 'This community release of Nextcloud is unsupported and push notifications are limited.', - ], - ], - [ - 'loginErrors', - [ - 'ErrorArray1', - 'ErrorArray2', - ], - ], - [ - 'loginUsername', - '', - ] - ]; - $this->initialState->expects($this->exactly(14)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - if (!empty($expected)) { - $this->assertEquals($expected, func_get_args()); - } - }); - - $expectedResponse = new TemplateResponse( - 'core', - 'login', - [ - 'alt_login' => [], - 'pageTitle' => 'Login' - ], - 'guest' - ); - $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', '')); - } - - public function testShowLoginFormForFlowAuth(): void { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $calls = [ - [], [], [], - [ - 'loginAutocomplete', - false - ], - [ - 'loginCanRememberme', - false - ], - [ - 'loginRedirectUrl', - 'login/flow' - ], - ]; - $this->initialState->expects($this->exactly(15)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - if (!empty($expected)) { - $this->assertEquals($expected, func_get_args()); - } - }); - - $expectedResponse = new TemplateResponse( - 'core', - 'login', - [ - 'alt_login' => [], - 'pageTitle' => 'Login' - ], - 'guest' - ); - $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow')); - } - - /** - * @return array - */ - public static function passwordResetDataProvider(): array { - return [ - [ - true, - true, - ], - [ - false, - false, - ], - ]; - } - - #[DataProvider('passwordResetDataProvider')] - public function testShowLoginFormWithPasswordResetOption($canChangePassword, - $expectedResult): void { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $this->config - ->expects(self::once()) - ->method('getSystemValue') - ->willReturnMap([ - ['login_form_autocomplete', true, true], - ]); - $this->config - ->expects(self::once()) - ->method('getSystemValueString') - ->willReturnMap([ - ['lost_password_link', '', ''], - ]); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('canChangePassword') - ->willReturn($canChangePassword); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('LdapUser') - ->willReturn($user); - $calls = [ - [], [], - [ - 'loginUsername', - 'LdapUser' - ], - [], [], [], [], - [ - 'loginCanResetPassword', - $expectedResult - ], - ]; - $this->initialState->expects($this->exactly(14)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - if (!empty($expected)) { - $this->assertEquals($expected, func_get_args()); - } - }); - - $expectedResponse = new TemplateResponse( - 'core', - 'login', - [ - 'alt_login' => [], - 'pageTitle' => 'Login' - ], - 'guest' - ); - $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', '')); - } - - public function testShowLoginFormForUserNamed0(): void { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $this->config - ->expects(self::once()) - ->method('getSystemValue') - ->willReturnMap([ - ['login_form_autocomplete', true, true], - ]); - $this->config - ->expects(self::once()) - ->method('getSystemValueString') - ->willReturnMap([ - ['lost_password_link', '', ''], - ]); - $user = $this->createMock(IUser::class); - $user->expects($this->once()) - ->method('canChangePassword') - ->willReturn(false); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('0') - ->willReturn($user); - $calls = [ - [], [], [], - [ - 'loginAutocomplete', - true - ], - [ - 'loginCanRememberme', - false - ], - [], - [ - 'loginResetPasswordLink', - false - ], - [ - 'loginCanResetPassword', - false - ], - ]; - $this->initialState->expects($this->exactly(14)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - if (!empty($expected)) { - $this->assertEquals($expected, func_get_args()); - } - }); - - $expectedResponse = new TemplateResponse( - 'core', - 'login', - [ - 'alt_login' => [], - 'pageTitle' => 'Login' - ], - 'guest' - ); - $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', '')); - } - - public static function remembermeProvider(): array { - return [ - [ - true, - ], - [ - false, - ], - ]; - } - - #[DataProvider('remembermeProvider')] - public function testLoginWithInvalidCredentials(bool $rememberme): void { - $user = 'MyUserName'; - $password = 'secret'; - $loginPageUrl = '/login?redirect_url=/apps/files'; - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $loginData = new LoginData( - $this->request, - $user, - $password, - $rememberme, - '/apps/files' - ); - $loginResult = LoginResult::failure(LoginController::LOGIN_MSG_INVALIDPASSWORD); - $loginChain->expects($this->once()) - ->method('process') - ->with($this->equalTo($loginData)) - ->willReturn($loginResult); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.login.showLoginForm', [ - 'user' => $user, - 'redirect_url' => '/apps/files', - 'direct' => 1, - ]) - ->willReturn($loginPageUrl); - $expected = new RedirectResponse($loginPageUrl); - $expected->throttle(['user' => 'MyUserName']); - - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/files'); - - $this->assertEquals($expected, $response); - } - - #[DataProvider('remembermeProvider')] - public function testLoginWithValidCredentials(bool $rememberme): void { - $user = 'MyUserName'; - $password = 'secret'; - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $loginData = new LoginData( - $this->request, - $user, - $password, - $rememberme, - ); - $loginResult = LoginResult::success(); - $loginChain->expects($this->once()) - ->method('process') - ->with($this->equalTo($loginData)) - ->willReturn($loginResult); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToDefaultPageUrl') - ->willReturn('/default/foo'); - - $expected = new RedirectResponse('/default/foo'); - $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme)); - } - - #[DataProvider('remembermeProvider')] - public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(bool $rememberme): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('jane'); - $password = 'secret'; - $originalUrl = 'another%20url'; - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->userSession - ->method('isLoggedIn') - ->with() - ->willReturn(false); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl); - - $expected = new RedirectResponse(''); - $this->assertEquals($expected, $response); - } - - #[DataProvider('remembermeProvider')] - public function testLoginWithoutPassedCsrfCheckAndLoggedIn(bool $rememberme): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('jane'); - $password = 'secret'; - $originalUrl = 'another url'; - $redirectUrl = 'http://localhost/another url'; - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->userSession - ->method('isLoggedIn') - ->with() - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('getAbsoluteURL') - ->with(urldecode($originalUrl)) - ->willReturn($redirectUrl); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl); - - $expected = new RedirectResponse($redirectUrl); - $this->assertEquals($expected, $response); - } - - #[DataProvider('remembermeProvider')] - public function testLoginWithValidCredentialsAndRedirectUrl(bool $rememberme): void { - $user = 'MyUserName'; - $password = 'secret'; - $redirectUrl = 'https://next.cloud/apps/mail'; - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $loginData = new LoginData( - $this->request, - $user, - $password, - $rememberme, - '/apps/mail' - ); - $loginResult = LoginResult::success(); - $loginChain->expects($this->once()) - ->method('process') - ->with($this->equalTo($loginData)) - ->willReturn($loginResult); - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('getAbsoluteURL') - ->with('/apps/mail') - ->willReturn($redirectUrl); - $expected = new RedirectResponse($redirectUrl); - - $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/mail'); - - $this->assertEquals($expected, $response); - } - - #[DataProvider('remembermeProvider')] - public function testToNotLeakLoginName(bool $rememberme): void { - $loginChain = $this->createMock(LoginChain::class); - $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class); - $trustedDomainHelper->method('isTrustedUrl')->willReturn(true); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $loginPageUrl = '/login?redirect_url=/apps/files'; - $loginData = new LoginData( - $this->request, - 'john@doe.com', - 'just wrong', - $rememberme, - '/apps/files' - ); - $loginResult = LoginResult::failure(LoginController::LOGIN_MSG_INVALIDPASSWORD); - $loginChain->expects($this->once()) - ->method('process') - ->with($this->equalTo($loginData)) - ->willReturnCallback(function (LoginData $data) use ($loginResult) { - $data->setUsername('john'); - return $loginResult; - }); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.login.showLoginForm', [ - 'user' => 'john@doe.com', - 'redirect_url' => '/apps/files', - 'direct' => 1, - ]) - ->willReturn($loginPageUrl); - $expected = new RedirectResponse($loginPageUrl); - $expected->throttle(['user' => 'john']); - - $response = $this->loginController->tryLogin( - $loginChain, - $trustedDomainHelper, - 'john@doe.com', - 'just wrong', - $rememberme, - '/apps/files' - ); - - $this->assertEquals($expected, $response); - } -} --- a/tests/Core/Controller/LostControllerTest.php +++ /dev/null @@ -1,782 +0,0 @@ -existingUser = $this->createMock(IUser::class); - $this->existingUser->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('test@example.com'); - $this->existingUser->expects($this->any()) - ->method('getUID') - ->willReturn('ExistingUser'); - $this->existingUser->expects($this->any()) - ->method('getDisplayName') - ->willReturn('Existing User'); - $this->existingUser->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - $this->config = $this->createMock(IConfig::class); - $this->config->expects($this->any()) - ->method('getSystemValue') - ->willReturnCallback(function (string $key, $default = '') { - return match ($key) { - 'secret' => 'SECRET', - 'lost_password_link' => $this->lostPasswordLink, - default => $default, - }; - }); - $this->l10n = $this->createMock(IL10N::class); - $this->l10n - ->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - $this->defaults = $this->createMock(Defaults::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->mailer = $this->createMock(IMailer::class); - $this->request = $this->createMock(IRequest::class); - $this->encryptionManager = $this->createMock(IManager::class); - $this->encryptionManager->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - $this->logger = $this->createMock(LoggerInterface::class); - $this->twofactorManager = $this->createMock(Manager::class); - $this->initialState = $this->createMock(IInitialState::class); - $this->verificationToken = $this->createMock(IVerificationToken::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->limiter = $this->createMock(Limiter::class); - $this->lostController = new LostController( - 'Core', - $this->request, - $this->urlGenerator, - $this->userManager, - $this->defaults, - $this->l10n, - $this->config, - 'lostpassword-noreply@localhost', - $this->encryptionManager, - $this->mailer, - $this->logger, - $this->twofactorManager, - $this->initialState, - $this->verificationToken, - $this->eventDispatcher, - $this->limiter - ); - } - - public function testResetFormTokenError(): void { - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->once()) - ->method('check') - ->with('12345:MySecretToken', $this->existingUser, 'lostpassword', 'test@example.com') - ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR)); - - $response = $this->lostController->resetform('12345:MySecretToken', 'ValidTokenUser'); - $expectedResponse = new TemplateResponse('core', - 'error', - [ - 'errors' => [ - ['error' => 'Could not reset password because the token is invalid'], - ] - ], - 'guest'); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testResetFormTokenErrorWithDisabledLink(): void { - $this->lostPasswordLink = 'disabled'; - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->once()) - ->method('check') - ->with('12345:MySecretToken', $this->existingUser, 'lostpassword', 'test@example.com') - ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_NOT_FOUND)); - - $response = $this->lostController->resetform('12345:MySecretToken', 'ValidTokenUser'); - $expectedResponse = new TemplateResponse('core', - 'error', - [ - 'errors' => [ - ['error' => 'Password reset is disabled'], - ] - ], - 'guest'); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testResetFormValidToken(): void { - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->once()) - ->method('check') - ->with('MySecretToken', $this->existingUser, 'lostpassword', 'test@example.com'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.lost.setPassword', ['userId' => 'ValidTokenUser', 'token' => 'MySecretToken']) - ->willReturn('https://example.tld/index.php/lostpassword/set/sometoken/someuser'); - - $calls = [ - ['resetPasswordUser', 'ValidTokenUser'], - ['resetPasswordTarget', 'https://example.tld/index.php/lostpassword/set/sometoken/someuser'], - ]; - $this->initialState - ->expects($this->exactly(2)) - ->method('provideInitialState') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $response = $this->lostController->resetform('MySecretToken', 'ValidTokenUser'); - $expectedResponse = new TemplateResponse('core', - 'login', - [], - 'guest'); - $this->assertEquals($expectedResponse, $response); - } - - public function testEmailUnsuccessful(): void { - $existingUser = 'ExistingUser'; - $nonExistingUser = 'NonExistingUser'; - $this->userManager - ->expects($this->any()) - ->method('userExists') - ->willReturnMap([ - [true, $existingUser], - [false, $nonExistingUser] - ]); - - $this->logger->expects($this->exactly(0)) - ->method('error'); - $this->logger->expects($this->exactly(2)) - ->method('warning'); - - $this->userManager - ->method('getByEmail') - ->willReturn([]); - - // With a non existing user - $response = $this->lostController->email($nonExistingUser); - $expectedResponse = new JSONResponse([ - 'status' => 'success', - ]); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - - // With no mail address - $this->config - ->expects($this->any()) - ->method('getUserValue') - ->with($existingUser, 'settings', 'email') - ->willReturn(null); - $response = $this->lostController->email($existingUser); - $expectedResponse = new JSONResponse([ - 'status' => 'success', - ]); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testEmailSuccessful(): void { - $this->userManager - ->expects($this->any()) - ->method('get') - ->with('ExistingUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->once()) - ->method('create') - ->willReturn('ThisIsMaybeANotSoSecretToken!'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!']) - ->willReturn('https://example.tld/index.php/lostpassword/'); - $message = $this->getMockBuilder('\OC\Mail\Message') - ->disableOriginalConstructor()->getMock(); - $message - ->expects($this->once()) - ->method('setTo') - ->with(['test@example.com' => 'Existing User']); - $message - ->expects($this->once()) - ->method('setFrom') - ->with(['lostpassword-noreply@localhost' => null]); - - $emailTemplate = $this->createMock(IEMailTemplate::class); - $emailTemplate->expects($this->any()) - ->method('renderHtml') - ->willReturn('HTML body'); - $emailTemplate->expects($this->any()) - ->method('renderText') - ->willReturn('text body'); - - $message - ->expects($this->once()) - ->method('useTemplate') - ->with($emailTemplate); - - $this->mailer - ->expects($this->once()) - ->method('createEMailTemplate') - ->willReturn($emailTemplate); - $this->mailer - ->expects($this->once()) - ->method('createMessage') - ->willReturn($message); - $this->mailer - ->expects($this->once()) - ->method('send') - ->with($message); - - $response = $this->lostController->email('ExistingUser'); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testEmailWithMailSuccessful(): void { - $this->userManager - ->expects($this->any()) - ->method('get') - ->with('test@example.com') - ->willReturn(null); - $this->userManager - ->expects($this->any()) - ->method('getByEmail') - ->with('test@example.com') - ->willReturn([$this->existingUser]); - $this->verificationToken->expects($this->once()) - ->method('create') - ->willReturn('ThisIsMaybeANotSoSecretToken!'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!']) - ->willReturn('https://example.tld/index.php/lostpassword/'); - $message = $this->getMockBuilder('\OC\Mail\Message') - ->disableOriginalConstructor()->getMock(); - $message - ->expects($this->once()) - ->method('setTo') - ->with(['test@example.com' => 'Existing User']); - $message - ->expects($this->once()) - ->method('setFrom') - ->with(['lostpassword-noreply@localhost' => null]); - - $emailTemplate = $this->createMock(IEMailTemplate::class); - $emailTemplate->expects($this->any()) - ->method('renderHtml') - ->willReturn('HTML body'); - $emailTemplate->expects($this->any()) - ->method('renderText') - ->willReturn('text body'); - - $message - ->expects($this->once()) - ->method('useTemplate') - ->with($emailTemplate); - - $this->mailer - ->expects($this->once()) - ->method('createEMailTemplate') - ->willReturn($emailTemplate); - $this->mailer - ->expects($this->once()) - ->method('createMessage') - ->willReturn($message); - $this->mailer - ->expects($this->once()) - ->method('send') - ->with($message); - - $response = $this->lostController->email('test@example.com'); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testEmailCantSendException(): void { - $this->userManager - ->expects($this->any()) - ->method('get') - ->with('ExistingUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->once()) - ->method('create') - ->willReturn('ThisIsMaybeANotSoSecretToken!'); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRouteAbsolute') - ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!']) - ->willReturn('https://example.tld/index.php/lostpassword/'); - $message = $this->createMock(Message::class); - $message - ->expects($this->once()) - ->method('setTo') - ->with(['test@example.com' => 'Existing User']); - $message - ->expects($this->once()) - ->method('setFrom') - ->with(['lostpassword-noreply@localhost' => null]); - - $emailTemplate = $this->createMock(IEMailTemplate::class); - $emailTemplate->expects($this->any()) - ->method('renderHtml') - ->willReturn('HTML body'); - $emailTemplate->expects($this->any()) - ->method('renderText') - ->willReturn('text body'); - - $message - ->expects($this->once()) - ->method('useTemplate') - ->with($emailTemplate); - - $this->mailer - ->expects($this->once()) - ->method('createEMailTemplate') - ->willReturn($emailTemplate); - $this->mailer - ->expects($this->once()) - ->method('createMessage') - ->willReturn($message); - $this->mailer - ->expects($this->once()) - ->method('send') - ->with($message) - ->willThrowException(new \Exception()); - - $this->logger->expects($this->exactly(1)) - ->method('error'); - - $response = $this->lostController->email('ExistingUser'); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testSetPasswordUnsuccessful(): void { - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('encryptedData'); - $this->existingUser->method('getLastLogin') - ->willReturn(12344); - $this->existingUser->expects($this->once()) - ->method('setPassword') - ->with('NewPassword') - ->willReturn(false); - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $beforePasswordResetEvent = new BeforePasswordResetEvent($this->existingUser, 'NewPassword'); - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatchTyped') - ->with($beforePasswordResetEvent); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true); - $expectedResponse = ['status' => 'error', 'msg' => '']; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSetPasswordSuccessful(): void { - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('encryptedData'); - $this->existingUser->method('getLastLogin') - ->willReturn(12344); - $this->existingUser->expects($this->once()) - ->method('setPassword') - ->with('NewPassword') - ->willReturn(true); - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - - $calls = [ - [new BeforePasswordResetEvent($this->existingUser, 'NewPassword')], - [new PasswordResetEvent($this->existingUser, 'NewPassword')], - ]; - $this->eventDispatcher - ->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword'); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true); - $expectedResponse = ['user' => 'ValidTokenUser', 'status' => 'success']; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSetPasswordExpiredToken(): void { - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('encryptedData'); - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->atLeastOnce()) - ->method('check') - ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_EXPIRED)); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true); - $expectedResponse = [ - 'status' => 'error', - 'msg' => 'Could not reset password because the token is expired', - ]; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSetPasswordInvalidDataInDb(): void { - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('invalidEncryptedData'); - $this->userManager - ->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->atLeastOnce()) - ->method('check') - ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT)); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true); - $expectedResponse = [ - 'status' => 'error', - 'msg' => 'Could not reset password because the token is invalid', - ]; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testIsSetPasswordWithoutTokenFailing(): void { - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('aValidtoken'); - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->verificationToken->expects($this->atLeastOnce()) - ->method('check') - ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_MISMATCH)); - - $response = $this->lostController->setPassword('', 'ValidTokenUser', 'NewPassword', true); - $expectedResponse = [ - 'status' => 'error', - 'msg' => 'Could not reset password because the token is invalid' - ]; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSetPasswordForDisabledUser(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('isEnabled') - ->willReturn(false); - $user->expects($this->never()) - ->method('setPassword'); - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('random@example.org'); - - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('encryptedData'); - $this->userManager->method('get') - ->with('DisabledUser') - ->willReturn($user); - - $this->verificationToken->expects($this->atLeastOnce()) - ->method('check') - ->willThrowException(new InvalidTokenException(InvalidTokenException::USER_UNKNOWN)); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'DisabledUser', 'NewPassword', true); - $expectedResponse = [ - 'status' => 'error', - 'msg' => 'Could not reset password because the token is invalid' - ]; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSendEmailNoEmail(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - $this->userManager->method('userExists') - ->with('ExistingUser') - ->willReturn(true); - $this->userManager->method('get') - ->with('ExistingUser') - ->willReturn($user); - - $this->logger->expects($this->exactly(0)) - ->method('error'); - $this->logger->expects($this->once()) - ->method('warning'); - - $response = $this->lostController->email('ExistingUser'); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testSetPasswordEncryptionDontProceedPerUserKey(): void { - /** @var IEncryptionModule|MockObject $encryptionModule */ - $encryptionModule = $this->createMock(IEncryptionModule::class); - $encryptionModule->expects($this->once())->method('needDetailedAccessList')->willReturn(true); - $this->encryptionManager->expects($this->once())->method('getEncryptionModules') - ->willReturn([0 => ['callback' => function () use ($encryptionModule) { - return $encryptionModule; - }]]); - $response = $this->lostController->setPassword('myToken', 'user', 'newpass', false); - $expectedResponse = ['status' => 'error', 'msg' => '', 'encryption' => true]; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testSetPasswordDontProceedMasterKey(): void { - $encryptionModule = $this->createMock(IEncryptionModule::class); - $encryptionModule->expects($this->once())->method('needDetailedAccessList')->willReturn(false); - $this->encryptionManager->expects($this->once())->method('getEncryptionModules') - ->willReturn([0 => ['callback' => function () use ($encryptionModule) { - return $encryptionModule; - }]]); - $this->config->method('getUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword', null) - ->willReturn('encryptedData'); - $this->existingUser->method('getLastLogin') - ->willReturn(12344); - $this->existingUser->expects($this->once()) - ->method('setPassword') - ->with('NewPassword') - ->willReturn(true); - $this->userManager->method('get') - ->with('ValidTokenUser') - ->willReturn($this->existingUser); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('ValidTokenUser', 'core', 'lostpassword'); - - $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', false); - $expectedResponse = ['user' => 'ValidTokenUser', 'status' => 'success']; - $this->assertSame($expectedResponse, $response->getData()); - } - - public function testTwoUsersWithSameEmail(): void { - $user1 = $this->createMock(IUser::class); - $user1->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('test@example.com'); - $user1->expects($this->any()) - ->method('getUID') - ->willReturn('User1'); - $user1->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - $user2 = $this->createMock(IUser::class); - $user2->expects($this->any()) - ->method('getEMailAddress') - ->willReturn('test@example.com'); - $user2->expects($this->any()) - ->method('getUID') - ->willReturn('User2'); - $user2->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - $this->userManager - ->method('get') - ->willReturn(null); - - $this->userManager - ->method('getByEmail') - ->willReturn([$user1, $user2]); - - $this->logger->expects($this->exactly(0)) - ->method('error'); - $this->logger->expects($this->once()) - ->method('warning'); - - // request password reset for test@example.com - $response = $this->lostController->email('test@example.com'); - - $expectedResponse = new JSONResponse([ - 'status' => 'success' - ]); - $expectedResponse->throttle(); - - $this->assertEquals($expectedResponse, $response); - } - - /** - * @return array - */ - public static function dataTwoUsersWithSameEmailOneDisabled(): array { - return [ - ['userEnabled1' => true, 'userEnabled2' => false], - ['userEnabled1' => false, 'userEnabled2' => true] - ]; - } - - /** - * @param bool $userEnabled1 - * @param bool $userEnabled2 - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTwoUsersWithSameEmailOneDisabled')] - public function testTwoUsersWithSameEmailOneDisabled(bool $userEnabled1, bool $userEnabled2): void { - $user1 = $this->createMock(IUser::class); - $user1->method('getEMailAddress') - ->willReturn('test@example.com'); - $user1->method('getUID') - ->willReturn('User1'); - $user1->method('isEnabled') - ->willReturn($userEnabled1); - - $user2 = $this->createMock(IUser::class); - $user2->method('getEMailAddress') - ->willReturn('test@example.com'); - $user2->method('getUID') - ->willReturn('User2'); - $user2->method('isEnabled') - ->willReturn($userEnabled2); - - $this->userManager - ->method('get') - ->willReturn(null); - - $this->userManager - ->method('getByEmail') - ->willReturn([$user1, $user2]); - - $result = self::invokePrivate($this->lostController, 'findUserByIdOrMail', ['test@example.com']); - $this->assertInstanceOf(IUser::class, $result); - } - - public function testTrimEmailInput(): void { - $this->userManager - ->expects($this->once()) - ->method('getByEmail') - ->with('test@example.com') - ->willReturn([$this->existingUser]); - - $this->mailer - ->expects($this->once()) - ->method('send'); - - $response = $this->lostController->email(' test@example.com '); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } - - public function testUsernameInput(): void { - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('ExistingUser') - ->willReturn($this->existingUser); - - $this->mailer - ->expects($this->once()) - ->method('send'); - - $response = $this->lostController->email(' ExistingUser '); - $expectedResponse = new JSONResponse(['status' => 'success']); - $expectedResponse->throttle(); - $this->assertEquals($expectedResponse, $response); - } -} --- a/tests/Core/Controller/NavigationControllerTest.php +++ /dev/null @@ -1,139 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->navigationManager = $this->createMock(INavigationManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - - $this->controller = new NavigationController( - 'core', - $this->request, - $this->navigationManager, - $this->urlGenerator - ); - } - - public static function dataGetNavigation(): array { - return [ - [false], - [true], - ]; - } - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetNavigation')] - public function testGetAppNavigation(bool $absolute): void { - $this->navigationManager->expects($this->once()) - ->method('getAll') - ->with('link') - ->willReturn(['files' => ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ] ]); - if ($absolute) { - $this->urlGenerator->expects($this->any()) - ->method('getBaseURL') - ->willReturn('http://localhost/'); - $this->urlGenerator->expects($this->exactly(2)) - ->method('getAbsoluteURL') - ->willReturnMap([ - ['/index.php/apps/files', 'http://localhost/index.php/apps/files'], - ['icon', 'http://localhost/icon'], - ]); - $actual = $this->controller->getAppsNavigation($absolute); - $this->assertInstanceOf(DataResponse::class, $actual); - $this->assertEquals('http://localhost/index.php/apps/files', $actual->getData()[0]['href']); - $this->assertEquals('http://localhost/icon', $actual->getData()[0]['icon']); - } else { - $actual = $this->controller->getAppsNavigation($absolute); - $this->assertInstanceOf(DataResponse::class, $actual); - $this->assertEquals('/index.php/apps/files', $actual->getData()[0]['href']); - $this->assertEquals('icon', $actual->getData()[0]['icon']); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetNavigation')] - public function testGetSettingsNavigation(bool $absolute): void { - $this->navigationManager->expects($this->once()) - ->method('getAll') - ->with('settings') - ->willReturn(['settings' => ['id' => 'settings', 'href' => '/index.php/settings/user', 'icon' => '/core/img/settings.svg'] ]); - if ($absolute) { - $this->urlGenerator->expects($this->any()) - ->method('getBaseURL') - ->willReturn('http://localhost/'); - $this->urlGenerator->expects($this->exactly(2)) - ->method('getAbsoluteURL') - ->willReturnMap([ - ['/index.php/settings/user', 'http://localhost/index.php/settings/user'], - ['/core/img/settings.svg', 'http://localhost/core/img/settings.svg'] - ]); - $actual = $this->controller->getSettingsNavigation($absolute); - $this->assertInstanceOf(DataResponse::class, $actual); - $this->assertEquals('http://localhost/index.php/settings/user', $actual->getData()[0]['href']); - $this->assertEquals('http://localhost/core/img/settings.svg', $actual->getData()[0]['icon']); - } else { - $actual = $this->controller->getSettingsNavigation($absolute); - $this->assertInstanceOf(DataResponse::class, $actual); - $this->assertEquals('/index.php/settings/user', $actual->getData()[0]['href']); - $this->assertEquals('/core/img/settings.svg', $actual->getData()[0]['icon']); - } - } - - public function testEtagIgnoresLogout(): void { - $navigation1 = [ - ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ], - ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=abcd', 'icon' => 'icon' ], - ]; - $navigation2 = [ - ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ], - ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=1234', 'icon' => 'icon' ], - ]; - $navigation3 = [ - ['id' => 'files', 'href' => '/index.php/apps/files/test', 'icon' => 'icon' ], - ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=1234', 'icon' => 'icon' ], - ]; - $this->navigationManager->expects($this->exactly(3)) - ->method('getAll') - ->with('link') - ->willReturnOnConsecutiveCalls( - $navigation1, - $navigation2, - $navigation3, - ); - - // Changes in the logout url should not change the ETag - $request1 = $this->controller->getAppsNavigation(); - $request2 = $this->controller->getAppsNavigation(); - $this->assertEquals($request1->getETag(), $request2->getETag()); - - // Changes in non-logout urls should result in a different ETag - $request3 = $this->controller->getAppsNavigation(); - $this->assertNotEquals($request2->getETag(), $request3->getETag()); - } -} --- a/tests/Core/Controller/OCSControllerTest.php +++ /dev/null @@ -1,220 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->capabilitiesManager = $this->createMock(CapabilitiesManager::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->keyManager = $this->createMock(Manager::class); - $serverVersion = Server::get(ServerVersion::class); - - $this->controller = new OCSController( - 'core', - $this->request, - $this->capabilitiesManager, - $this->userSession, - $this->userManager, - $this->keyManager, - $serverVersion - ); - } - - public function testGetConfig() { - $this->request->method('getServerHost') - ->willReturn('awesomehost.io'); - - $data = [ - 'version' => '1.7', - 'website' => 'Nextcloud', - 'host' => 'awesomehost.io', - 'contact' => '', - 'ssl' => 'false', - ]; - - $expected = new DataResponse($data); - $this->assertEquals($expected, $this->controller->getConfig()); - - return new DataResponse($data); - } - - public function testGetCapabilities(): void { - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - - $serverVersion = Server::get(ServerVersion::class); - - $result = []; - $result['version'] = [ - 'major' => $serverVersion->getMajorVersion(), - 'minor' => $serverVersion->getMinorVersion(), - 'micro' => $serverVersion->getPatchVersion(), - 'string' => $serverVersion->getVersionString(), - 'edition' => '', - 'extendedSupport' => false - ]; - - $capabilities = [ - 'foo' => 'bar', - 'a' => [ - 'b' => true, - 'c' => 11, - ] - ]; - $this->capabilitiesManager->method('getCapabilities') - ->willReturn($capabilities); - - $result['capabilities'] = $capabilities; - - $expected = new DataResponse($result); - $expected->setETag(md5(json_encode($result))); - $this->assertEquals($expected, $this->controller->getCapabilities()); - } - - public function testGetCapabilitiesPublic(): void { - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $serverVersion = Server::get(ServerVersion::class); - - $result = []; - $result['version'] = [ - 'major' => $serverVersion->getMajorVersion(), - 'minor' => $serverVersion->getMinorVersion(), - 'micro' => $serverVersion->getPatchVersion(), - 'string' => $serverVersion->getVersionString(), - 'edition' => '', - 'extendedSupport' => false - ]; - - $capabilities = [ - 'foo' => 'bar', - 'a' => [ - 'b' => true, - 'c' => 11, - ] - ]; - $this->capabilitiesManager->method('getCapabilities') - ->with(true) - ->willReturn($capabilities); - - $result['capabilities'] = $capabilities; - - $expected = new DataResponse($result); - $expected->setETag(md5(json_encode($result))); - $this->assertEquals($expected, $this->controller->getCapabilities()); - } - - public function testPersonCheckValid(): void { - $this->userManager->method('checkPassword') - ->with( - $this->equalTo('user'), - $this->equalTo('pass') - )->willReturn($this->createMock(IUser::class)); - - $expected = new DataResponse([ - 'person' => [ - 'personid' => 'user' - ] - ]); - $this->assertEquals($expected, $this->controller->personCheck('user', 'pass')); - } - - public function testPersonInvalid(): void { - $this->userManager->method('checkPassword') - ->with( - $this->equalTo('user'), - $this->equalTo('wrongpass') - )->willReturn(false); - - $expected = new DataResponse([], 102); - $expected->throttle(); - $this->assertEquals($expected, $this->controller->personCheck('user', 'wrongpass')); - } - - public function testPersonNoLogin(): void { - $this->userManager->method('checkPassword') - ->with( - $this->equalTo('user'), - $this->equalTo('wrongpass') - )->willReturn(false); - - $expected = new DataResponse([], 101); - $this->assertEquals($expected, $this->controller->personCheck('', '')); - } - - public function testGetIdentityProofWithNotExistingUser(): void { - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('NotExistingUser') - ->willReturn(null); - - $expected = new DataResponse(['Account not found'], 404); - $this->assertEquals($expected, $this->controller->getIdentityProof('NotExistingUser')); - } - - public function testGetIdentityProof(): void { - $user = $this->createMock(IUser::class); - $key = $this->createMock(Key::class); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('ExistingUser') - ->willReturn($user); - $this->keyManager - ->expects($this->once()) - ->method('getKey') - ->with($user) - ->willReturn($key); - $key - ->expects($this->once()) - ->method('getPublic') - ->willReturn('Existing Users public key'); - - $expected = new DataResponse([ - 'public' => 'Existing Users public key', - ]); - $this->assertEquals($expected, $this->controller->getIdentityProof('ExistingUser')); - } -} --- a/tests/Core/Controller/OpenMetricsControllerTest.php +++ /dev/null @@ -1,111 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->request->method('getRemoteAddress') - ->willReturn('192.168.1.1'); - $this->config = $this->createMock(IConfig::class); - $this->exporterManager = $this->createMock(ExporterManager::class); - $this->exporterManager->method('export')->willReturnCallback([$this, 'getFakeMetrics']); - $this->logger = $this->createMock(LoggerInterface::class); - $this->controller = new OpenMetricsController('core', $this->request, $this->config, $this->exporterManager, $this->logger); - } - - public function getFakeMetrics(): Generator { - $metric = $this->createMock(IMetricFamily::class); - $metric->method('type')->willReturn(MetricType::gauge); - $metric->method('unit')->willReturn('fake'); - $metric->method('name')->willReturn('fake_count'); - $metric->method('help')->willReturn('A fake count used for tests'); - $metric->method('metrics')->willReturnCallback(function () { - yield new Metric(42, ['type' => 'used']); - yield new Metric(24, ['type' => 'unused']); - }); - yield $metric; - } - public function testGetMetrics(): void { - $output = $this->createMock(IOutput::class); - $fullOutput = ''; - $output->method('setOutput') - ->willReturnCallback(function ($output) use (&$fullOutput): void { - $fullOutput .= $output; - }); - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with('openmetrics_allowed_clients') - ->willReturn(['192.168.0.0/16']); - $response = $this->controller->export(); - $this->assertInstanceOf(StreamTraversableResponse::class, $response); - $this->assertEquals('200', $response->getStatus()); - $this->assertEquals('application/openmetrics-text; version=1.0.0; charset=utf-8', $response->getHeaders()['Content-Type']); - $expected = <<callback($output); - $this->assertStringMatchesFormat($expected, $fullOutput); - } - - public function testMetricRejectsInvalidLabelNames(): void { - $this->expectException(\InvalidArgumentException::class); - new Metric(1, ['hide-photos' => '1.0.0']); - } - - public function testMetricAcceptsValidLabelNames(): void { - $metric = new Metric(1, ['hide_photos' => '1.0.0', 'normal_app' => '2.0.0']); - $this->assertEquals('1.0.0', $metric->label('hide_photos')); - $this->assertEquals('2.0.0', $metric->label('normal_app')); - } - - public function testGetMetricsFromForbiddenIp(): void { - $this->config->expects($this->once()) - ->method('getSystemValue') - ->with('openmetrics_allowed_clients') - ->willReturn(['1.2.3.4']); - $response = $this->controller->export(); - $this->assertInstanceOf(Response::class, $response); - $this->assertEquals('403', $response->getStatus()); - } -} --- a/tests/Core/Controller/PreviewControllerTest.php +++ /dev/null @@ -1,368 +0,0 @@ -userId = 'user'; - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->previewManager = $this->createMock(IPreview::class); - $this->request = $this->createMock(IRequest::class); - - $this->controller = new PreviewController( - 'core', - $this->request, - $this->previewManager, - $this->rootFolder, - $this->userId, - $this->createMock(IMimeIconProvider::class) - ); - } - - public function testInvalidFile(): void { - $res = $this->controller->getPreview(''); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); - - $this->assertEquals($expected, $res); - } - - public function testInvalidWidth(): void { - $res = $this->controller->getPreview('file', 0); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); - - $this->assertEquals($expected, $res); - } - - public function testInvalidHeight(): void { - $res = $this->controller->getPreview('file', 10, 0); - $expected = new DataResponse([], Http::STATUS_BAD_REQUEST); - - $this->assertEquals($expected, $res); - } - - public function testFileNotFound(): void { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willThrowException(new NotFoundException()); - - $res = $this->controller->getPreview('file'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); - - $this->assertEquals($expected, $res); - } - - public function testNotAFile(): void { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $folder = $this->createMock(Folder::class); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($folder); - - $res = $this->controller->getPreview('file'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); - - $this->assertEquals($expected, $res); - } - - public function testNoPreviewAndNoIcon(): void { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(false); - - $res = $this->controller->getPreview('file', 10, 10, true, false); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); - - $this->assertEquals($expected, $res); - } - - public function testNoPreview() { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $storage = $this->createMock(IStorage::class); - $file->method('getStorage') - ->willReturn($storage); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - $file->method('isReadable') - ->willReturn(true); - - $this->previewManager->method('getPreview') - ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode')) - ->willThrowException(new NotFoundException()); - - $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode'); - $expected = new DataResponse([], Http::STATUS_NOT_FOUND); - - $this->assertEquals($expected, $res); - } - public function testFileWithoutReadPermission() { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - $file->method('isReadable') - ->willReturn(false); - - $res = $this->controller->getPreview('file', 10, 10, true, true); - $expected = new DataResponse([], Http::STATUS_FORBIDDEN); - - $this->assertEquals($expected, $res); - } - - public function testFileWithoutDownloadPermission() { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(123); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - $share = $this->createMock(IShare::class); - $share->method('canSeeContent') - ->willReturn(false); - - $storage = $this->createMock(ISharedStorage::class); - $storage->method('instanceOfStorage') - ->with(ISharedStorage::class) - ->willReturn(true); - $storage->method('getShare') - ->willReturn($share); - - $file->method('getStorage') - ->willReturn($storage); - $file->method('isReadable') - ->willReturn(true); - - $this->request->method('getHeader')->willReturn(''); - - $res = $this->controller->getPreview('file', 10, 10, true, true); - $expected = new DataResponse([], Http::STATUS_FORBIDDEN); - - $this->assertEquals($expected, $res); - } - - public function testFileWithoutDownloadPermissionButHeader() { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(123); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - $share = $this->createMock(IShare::class); - $share->method('canSeeContent') - ->willReturn(false); - - $storage = $this->createMock(ISharedStorage::class); - $storage->method('instanceOfStorage') - ->with(ISharedStorage::class) - ->willReturn(true); - $storage->method('getShare') - ->willReturn($share); - - $file->method('getStorage') - ->willReturn($storage); - $file->method('isReadable') - ->willReturn(true); - - $this->request - ->method('getHeader') - ->with('x-nc-preview') - ->willReturn('true'); - - $preview = $this->createMock(ISimpleFile::class); - $preview->method('getName')->willReturn('my name'); - $preview->method('getMTime')->willReturn(42); - $this->previewManager->method('getPreview') - ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode')) - ->willReturn($preview); - $preview->method('getMimeType') - ->willReturn('myMime'); - - $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode'); - - $this->assertEquals('myMime', $res->getHeaders()['Content-Type']); - $this->assertEquals(Http::STATUS_OK, $res->getStatus()); - $this->assertEquals($preview, $this->invokePrivate($res, 'file')); - } - - public function testValidPreview(): void { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(123); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - $file->method('isReadable') - ->willReturn(true); - - $storage = $this->createMock(IStorage::class); - $file->method('getStorage') - ->willReturn($storage); - - $preview = $this->createMock(ISimpleFile::class); - $preview->method('getName')->willReturn('my name'); - $preview->method('getMTime')->willReturn(42); - $this->previewManager->method('getPreview') - ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode')) - ->willReturn($preview); - $preview->method('getMimeType') - ->willReturn('myMime'); - - $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode'); - - $this->assertEquals('myMime', $res->getHeaders()['Content-Type']); - $this->assertEquals(Http::STATUS_OK, $res->getStatus()); - $this->assertEquals($preview, $this->invokePrivate($res, 'file')); - } - - public function testValidPreviewOfShare() { - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo($this->userId)) - ->willReturn($userFolder); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(123); - $userFolder->method('get') - ->with($this->equalTo('file')) - ->willReturn($file); - - $this->previewManager->method('isAvailable') - ->with($this->equalTo($file)) - ->willReturn(true); - - // No attributes set -> download permitted - $share = $this->createMock(IShare::class); - $share->method('canSeeContent') - ->willReturn(true); - - $storage = $this->createMock(ISharedStorage::class); - $storage->method('instanceOfStorage') - ->with(ISharedStorage::class) - ->willReturn(true); - $storage->method('getShare') - ->willReturn($share); - - $file->method('getStorage') - ->willReturn($storage); - $file->method('isReadable') - ->willReturn(true); - - $this->request - ->method('getHeader') - ->willReturn(''); - - $preview = $this->createMock(ISimpleFile::class); - $preview->method('getName')->willReturn('my name'); - $preview->method('getMTime')->willReturn(42); - $this->previewManager->method('getPreview') - ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode')) - ->willReturn($preview); - $preview->method('getMimeType') - ->willReturn('myMime'); - - $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode'); - - $this->assertEquals('myMime', $res->getHeaders()['Content-Type']); - $this->assertEquals(Http::STATUS_OK, $res->getStatus()); - $this->assertEquals($preview, $this->invokePrivate($res, 'file')); - } -} --- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php +++ /dev/null @@ -1,451 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->twoFactorManager = $this->createMock(Manager::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->session = $this->createMock(ISession::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->controller = $this->getMockBuilder(TwoFactorChallengeController::class) - ->setConstructorArgs([ - 'core', - $this->request, - $this->twoFactorManager, - $this->userSession, - $this->session, - $this->urlGenerator, - $this->logger, - ]) - ->onlyMethods(['getLogoutUrl']) - ->getMock(); - $this->controller->expects($this->any()) - ->method('getLogoutUrl') - ->willReturn('logoutAttribute'); - } - - public function testSelectChallenge(): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - $p1 = $this->createMock(IActivatableAtLogin::class); - $p1->method('getId')->willReturn('p1'); - $backupProvider = $this->createMock(IProvider::class); - $backupProvider->method('getId')->willReturn('backup_codes'); - $providerSet = new ProviderSet([$p1, $backupProvider], true); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($user) - ->willReturn([$p1]); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); - - $expected = new StandaloneTemplateResponse('core', 'twofactorselectchallenge', [ - 'providers' => [ - $p1, - ], - 'providerMissing' => true, - 'backupProvider' => $backupProvider, - 'redirect_url' => '/some/url', - 'logout_url' => 'logoutAttribute', - 'hasSetupProviders' => true, - ], 'guest'); - - $this->assertEquals($expected, $this->controller->selectChallenge('/some/url')); - } - - public function testShowChallenge(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $provider->method('getId')->willReturn('myprovider'); - $backupProvider = $this->createMock(IProvider::class); - $backupProvider->method('getId')->willReturn('backup_codes'); - $tmpl = $this->createMock(ITemplate::class); - $providerSet = new ProviderSet([$provider, $backupProvider], true); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('u2f'); - $backupProvider->expects($this->once()) - ->method('getId') - ->willReturn('backup_codes'); - - $this->session->expects($this->once()) - ->method('exists') - ->with('two_factor_auth_error') - ->willReturn(true); - $this->session->expects($this->exactly(2)) - ->method('remove') - ->with($this->logicalOr($this->equalTo('two_factor_auth_error'), $this->equalTo('two_factor_auth_error_message'))); - $provider->expects($this->once()) - ->method('getTemplate') - ->with($user) - ->willReturn($tmpl); - $tmpl->expects($this->once()) - ->method('fetchPage') - ->willReturn(''); - - $expected = new StandaloneTemplateResponse('core', 'twofactorshowchallenge', [ - 'error' => true, - 'provider' => $provider, - 'backupProvider' => $backupProvider, - 'logout_url' => 'logoutAttribute', - 'template' => '', - 'redirect_url' => '/re/dir/ect/url', - 'error_message' => null, - ], 'guest'); - - $this->assertEquals($expected, $this->controller->showChallenge('myprovider', '/re/dir/ect/url')); - } - - public function testShowInvalidChallenge(): void { - $user = $this->createMock(IUser::class); - $providerSet = new ProviderSet([], false); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.selectChallenge') - ->willReturn('select/challenge/url'); - - $expected = new RedirectResponse('select/challenge/url'); - - $this->assertEquals($expected, $this->controller->showChallenge('myprovider', 'redirect/url')); - } - - public function testSolveChallenge(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProvider') - ->with($user, 'myprovider') - ->willReturn($provider); - - $this->twoFactorManager->expects($this->once()) - ->method('verifyChallenge') - ->with('myprovider', $user, 'token') - ->willReturn(true); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToDefaultPageUrl') - ->willReturn('/default/foo'); - - $expected = new RedirectResponse('/default/foo'); - $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token')); - } - - public function testSolveValidChallengeAndRedirect(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProvider') - ->with($user, 'myprovider') - ->willReturn($provider); - - $this->twoFactorManager->expects($this->once()) - ->method('verifyChallenge') - ->with('myprovider', $user, 'token') - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('getAbsoluteURL') - ->with('redirect url') - ->willReturn('redirect/url'); - - $expected = new RedirectResponse('redirect/url'); - $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', 'redirect%20url')); - } - - public function testSolveChallengeInvalidProvider(): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProvider') - ->with($user, 'myprovider') - ->willReturn(null); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.selectChallenge') - ->willReturn('select/challenge/url'); - - $expected = new RedirectResponse('select/challenge/url'); - - $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token')); - } - - public function testSolveInvalidChallenge(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('myuser'); - $provider = $this->createMock(IProvider::class); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProvider') - ->with($user, 'myprovider') - ->willReturn($provider); - - $this->twoFactorManager->expects($this->once()) - ->method('verifyChallenge') - ->with('myprovider', $user, 'token') - ->willReturn(false); - $this->session->expects($this->once()) - ->method('set') - ->with('two_factor_auth_error', true); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.showChallenge', [ - 'challengeProviderId' => 'myprovider', - 'redirect_url' => '/url', - ]) - ->willReturn('files/index/url'); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('myprovider'); - - $expected = new RedirectResponse('files/index/url'); - $expected->throttle(['user' => 'myuser', 'provider' => 'myprovider']); - $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', '/url')); - } - - public function testSolveChallengeTwoFactorException(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('myuser'); - $provider = $this->createMock(IProvider::class); - $exception = new TwoFactorException('2FA failed'); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('getProvider') - ->with($user, 'myprovider') - ->willReturn($provider); - - $this->twoFactorManager->expects($this->once()) - ->method('verifyChallenge') - ->with('myprovider', $user, 'token') - ->willThrowException($exception); - $calls = [ - ['two_factor_auth_error_message', '2FA failed'], - ['two_factor_auth_error', true], - ]; - $this->session->expects($this->exactly(2)) - ->method('set') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.showChallenge', [ - 'challengeProviderId' => 'myprovider', - 'redirect_url' => '/url', - ]) - ->willReturn('files/index/url'); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('myprovider'); - - $expected = new RedirectResponse('files/index/url'); - $expected->throttle(['user' => 'myuser', 'provider' => 'myprovider']); - $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', '/url')); - } - - public function testSetUpProviders(): void { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $provider = $this->createMock(IActivatableAtLogin::class); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($user) - ->willReturn([ - $provider, - ]); - $expected = new StandaloneTemplateResponse( - 'core', - 'twofactorsetupselection', - [ - 'providers' => [ - $provider, - ], - 'logout_url' => 'logoutAttribute', - 'redirect_url' => null, - ], - 'guest' - ); - - $response = $this->controller->setupProviders(); - - $this->assertEquals($expected, $response); - } - - public function testSetUpInvalidProvider(): void { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $provider = $this->createMock(IActivatableAtLogin::class); - $provider->expects($this->any()) - ->method('getId') - ->willReturn('prov1'); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($user) - ->willReturn([ - $provider, - ]); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.selectChallenge') - ->willReturn('2fa/select/page'); - $expected = new RedirectResponse('2fa/select/page'); - - $response = $this->controller->setupProvider('prov2'); - - $this->assertEquals($expected, $response); - } - - public function testSetUpProvider(): void { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $provider = $this->createMock(IActivatableAtLogin::class); - $provider->expects($this->any()) - ->method('getId') - ->willReturn('prov1'); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($user) - ->willReturn([ - $provider, - ]); - $loginSetup = $this->createMock(ILoginSetupProvider::class); - $provider->expects($this->any()) - ->method('getLoginSetup') - ->with($user) - ->willReturn($loginSetup); - $tmpl = $this->createMock(ITemplate::class); - $loginSetup->expects($this->once()) - ->method('getBody') - ->willReturn($tmpl); - $tmpl->expects($this->once()) - ->method('fetchPage') - ->willReturn('tmpl'); - $expected = new StandaloneTemplateResponse( - 'core', - 'twofactorsetupchallenge', - [ - 'provider' => $provider, - 'logout_url' => 'logoutAttribute', - 'template' => 'tmpl', - 'redirect_url' => null, - ], - 'guest' - ); - - $response = $this->controller->setupProvider('prov1'); - - $this->assertEquals($expected, $response); - } - - public function testConfirmProviderSetup(): void { - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.showChallenge', - [ - 'challengeProviderId' => 'totp', - 'redirect_url' => null, - ]) - ->willReturn('2fa/select/page'); - $expected = new RedirectResponse('2fa/select/page'); - - $response = $this->controller->confirmProviderSetup('totp'); - - $this->assertEquals($expected, $response); - } -} --- a/tests/Core/Controller/UserControllerTest.php +++ /dev/null @@ -1,61 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->controller = new UserController( - 'core', - $this->createMock(IRequest::class), - $this->userManager - ); - } - - public function testGetDisplayNames(): void { - $user = $this->createMock(IUser::class); - $user->method('getDisplayName') - ->willReturn('FooDisplay Name'); - - $this->userManager - ->method('get') - ->willReturnCallback(function ($uid) use ($user) { - if ($uid === 'foo') { - return $user; - } - return null; - }); - - $expected = new JSONResponse([ - 'users' => [ - 'foo' => 'FooDisplay Name', - 'bar' => 'bar', - ], - 'status' => 'success' - ]); - - $result = $this->controller->getDisplayNames(['foo', 'bar']); - $this->assertEquals($expected, $result); - } -} --- a/tests/Core/Controller/WellKnownControllerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->manager = $this->createMock(RequestManager::class); - - $this->controller = new WellKnownController( - $this->request, - $this->manager, - ); - } - - public function testHandleNotProcessed(): void { - $httpResponse = $this->controller->handle('nodeinfo'); - - self::assertInstanceOf(JSONResponse::class, $httpResponse); - self::assertArrayHasKey('X-NEXTCLOUD-WELL-KNOWN', $httpResponse->getHeaders()); - } - - public function testHandle(): void { - $response = $this->createMock(IResponse::class); - $jsonResponse = $this->createMock(JSONResponse::class); - $response->expects(self::once()) - ->method('toHttpResponse') - ->willReturn($jsonResponse); - $this->manager->expects(self::once()) - ->method('process') - ->with( - 'nodeinfo', - $this->request - )->willReturn($response); - $jsonResponse->expects(self::once()) - ->method('addHeader') - ->willReturnSelf(); - - $httpResponse = $this->controller->handle('nodeinfo'); - - self::assertInstanceOf(JSONResponse::class, $httpResponse); - } -} --- a/tests/Core/Controller/WipeControllerTest.php +++ /dev/null @@ -1,102 +0,0 @@ -remoteWipe = $this->createMock(RemoteWipe::class); - $this->controller = new WipeController( - 'core', - $this->createMock(IRequest::class), - $this->remoteWipe); - } - - public static function dataTest(): array { - return [ - // valid token, could perform operation, valid result - [ true, true, true], - [ true, false, false], - [false, true, false], - [false, false, false], - ]; - } - - /** - * @param bool $valid - * @param bool $couldPerform - * @param bool $result - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')] - public function testCheckWipe(bool $valid, bool $couldPerform, bool $result): void { - if (!$valid) { - $this->remoteWipe->method('start') - ->with('mytoken') - ->willThrowException(new InvalidTokenException()); - } else { - $this->remoteWipe->method('start') - ->with('mytoken') - ->willReturn($couldPerform); - } - - $result = $this->controller->checkWipe('mytoken'); - - if (!$valid || !$couldPerform) { - $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); - $this->assertSame([], $result->getData()); - } else { - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - $this->assertSame(['wipe' => true], $result->getData()); - } - } - - /** - * @param bool $valid - * @param bool $couldPerform - * @param bool $result - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')] - public function testWipeDone(bool $valid, bool $couldPerform, bool $result): void { - if (!$valid) { - $this->remoteWipe->method('finish') - ->with('mytoken') - ->willThrowException(new InvalidTokenException()); - } else { - $this->remoteWipe->method('finish') - ->with('mytoken') - ->willReturn($couldPerform); - } - - $result = $this->controller->wipeDone('mytoken'); - - if (!$valid || !$couldPerform) { - $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); - $this->assertSame([], $result->getData()); - } else { - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - $this->assertSame([], $result->getData()); - } - } -} --- a/tests/Core/Data/LoginFlowV2CredentialsTest.php +++ /dev/null @@ -1,48 +0,0 @@ -fixture = new LoginFlowV2Credentials('server', 'login', 'pass'); - } - - public function testImplementsJsonSerializable(): void { - $this->assertTrue($this->fixture instanceof JsonSerializable); - } - - /** - * Test getter functions. - */ - public function testGetter(): void { - $this->assertEquals('server', $this->fixture->getServer()); - $this->assertEquals('login', $this->fixture->getLoginName()); - $this->assertEquals('pass', $this->fixture->getAppPassword()); - } - - public function testJsonSerialize(): void { - $this->assertEquals( - [ - 'server' => 'server', - 'loginName' => 'login', - 'appPassword' => 'pass', - ], - $this->fixture->jsonSerialize() - ); - } -} --- a/tests/Core/Listener/RestrictInteractionListenerTest.php +++ /dev/null @@ -1,223 +0,0 @@ -createUser('user', 'password'); - $this->assertNotFalse($user); - $this->user = $user; - - Server::get(ISetupManager::class)->setupForUser($user); - - /** @var CappedMemoryCache $cache */ - $cache = self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache'); - $cache->clear(); - } - - #[\Override] - protected function tearDown(): void { - Server::get(ISetupManager::class)->tearDown(); - - $this->assertTrue($this->user->delete()); - - parent::tearDown(); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionApiDisabled(): void { - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_enabled', 'no'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), []); - $this->assertEquals('Sharing is not allowed.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_enabled'); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionDisabledForUser(): void { - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - $group->addUser($this->user); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_exclude_groups', 'yes'); - $config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), []); - $this->assertEquals('Sharing is not allowed for you.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_exclude_groups'); - $config->deleteAppValue('core', 'shareapi_exclude_groups_list'); - - $this->assertTrue($group->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionUserReceiverGroupMembersOnly(): void { - $user2 = Server::get(IUserManager::class)->createUser('user2', 'password'); - $this->assertNotFalse($user2); - - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - $group->addUser($this->user); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new UserReceiver($user2->getUID(), $user2)]); - $this->assertEquals('Sharing is only allowed with group members.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); - - $this->assertTrue($group->delete()); - $this->assertTrue($user2->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionUserReceiverGroupMembersOnlyExclude(): void { - $user2 = Server::get(IUserManager::class)->createUser('user2', 'password'); - $this->assertNotFalse($user2); - - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - $group->addUser($this->user); - $group->addUser($user2); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); - $config->setAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new UserReceiver($user2->getUID(), $user2)]); - $this->assertEquals('Sharing is only allowed with group members.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list'); - - $this->assertTrue($group->delete()); - $this->assertTrue($user2->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionGroupReceiverGroupMembersOnly(): void { - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new GroupReceiver($group->getGID(), $group)]); - $this->assertEquals('Sharing is only allowed within your own groups.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); - - $this->assertTrue($group->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionGroupReceiverGroupMembersOnlyExclude(): void { - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - $group->addUser($this->user); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); - $config->setAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new GroupReceiver($group->getGID(), $group)]); - $this->assertEquals('Sharing is only allowed within your own groups.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); - $config->deleteAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list'); - - $this->assertTrue($group->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionGroupReceiverGroupSharingDisabled(): void { - $group = Server::get(IGroupManager::class)->createGroup('group'); - $this->assertNotNull($group); - $group->addUser($this->user); - - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_allow_group_sharing', 'no'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new GroupReceiver($group->getGID(), $group)]); - $this->assertEquals('Group sharing is not allowed.', $event->isInteractionRestricted()); - - $config->deleteAppValue('core', 'shareapi_allow_group_sharing'); - - $this->assertTrue($group->delete()); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionLinkEmailReceiverLinkSharingDisabled(): void { - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'shareapi_allow_links', 'no'); - - foreach ([ - new LinkReceiver(), - new EmailReceiver(''), - ] as $receiver) { - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [$receiver]); - $this->assertEquals('Public link sharing is not allowed.', $event->isInteractionRestricted()); - } - - $config->deleteAppValue('core', 'shareapi_allow_links'); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionRemoteUserReceiverServer2ServerSharingDisabled(): void { - $config = Server::get(IConfig::class); - $config->setAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'no'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new RemoteUserReceiver('')]); - $this->assertEquals('Sharing to remote users is not allowed.', $event->isInteractionRestricted()); - - $config->deleteAppValue('files_sharing', 'outgoing_server2server_share_enabled'); - } - - /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ - public function testShareActionRemoteGroupReceiverServer2ServerGroupSharingDisabled(): void { - $config = Server::get(IConfig::class); - $config->setAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no'); - - $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, [], new ShareAction(), [new RemoteGroupReceiver('')]); - $this->assertEquals('Sharing to remote groups is not allowed.', $event->isInteractionRestricted()); - - $config->deleteAppValue('files_sharing', 'outgoing_server2server_group_share_enabled'); - } -} --- a/tests/Core/Middleware/TwoFactorMiddlewareTest.php +++ /dev/null @@ -1,305 +0,0 @@ -twoFactorManager = $this->getMockBuilder(Manager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->userSession = $this->getMockBuilder(Session::class) - ->disableOriginalConstructor() - ->getMock(); - $this->session = $this->createMock(ISession::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => 'test/url' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - - $this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator, $this->reflector, $this->request); - } - - public function testBeforeControllerNotLoggedIn(): void { - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - - $this->userSession->expects($this->never()) - ->method('getUser'); - - $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($controller, 'index'); - } - - public function testBeforeSetupController(): void { - $user = $this->createMock(IUser::class); - $this->userSession->expects($this->any()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('needsSecondFactor') - ->willReturn(true); - $this->userSession->expects($this->never()) - ->method('isLoggedIn'); - - $this->middleware->beforeController(new LoginSetupController('foo', $this->request), 'index'); - } - - public function testBeforeControllerNoTwoFactorCheckNeeded(): void { - $user = $this->createMock(IUser::class); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->willReturn(false); - - $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($controller, 'index'); - } - - public function testBeforeControllerTwoFactorAuthRequired(): void { - $this->expectException(TwoFactorAuthRequiredException::class); - - $user = $this->createMock(IUser::class); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('needsSecondFactor') - ->with($user) - ->willReturn(true); - - $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($controller, 'index'); - } - - public function testBeforeControllerUserAlreadyLoggedIn(): void { - $this->expectException(UserAlreadyLoggedInException::class); - - $user = $this->createMock(IUser::class); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('needsSecondFactor') - ->with($user) - ->willReturn(false); - - $twoFactorChallengeController = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($twoFactorChallengeController, 'index'); - } - - public function testAfterExceptionTwoFactorAuthRequired(): void { - $ex = new TwoFactorAuthRequiredException(); - - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.selectChallenge') - ->willReturn('test/url'); - $expected = new RedirectResponse('test/url'); - - $controller = new HasTwoFactorAnnotationController('foo', $this->request); - $this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex)); - } - - public function testAfterException(): void { - $ex = new UserAlreadyLoggedInException(); - - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('files.view.index') - ->willReturn('redirect/url'); - $expected = new RedirectResponse('redirect/url'); - - $controller = new HasTwoFactorAnnotationController('foo', $this->request); - $this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex)); - } - - public function testRequires2FASetupDoneAnnotated(): void { - $user = $this->createMock(IUser::class); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->method('getUser') - ->willReturn($user); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('needsSecondFactor') - ->with($user) - ->willReturn(false); - - $this->expectException(UserAlreadyLoggedInException::class); - - $controller = $this->getMockBuilder(HasTwoFactorSetUpDoneAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($controller, 'index'); - } - - public static function dataRequires2FASetupDone(): array { - return [ - [false, false, false], - [false, true, true], - [true, false, true], - [true, true, true], - ]; - } - - #[DataProvider('dataRequires2FASetupDone')] - public function testRequires2FASetupDone(bool $hasProvider, bool $missingProviders, bool $expectEception): void { - if ($hasProvider) { - $provider = $this->createMock(IProvider::class); - $provider->method('getId') - ->willReturn('2FAftw'); - $providers = [$provider]; - } else { - $providers = []; - } - - $user = $this->createMock(IUser::class); - - $this->userSession - ->method('getUser') - ->willReturn($user); - $providerSet = new ProviderSet($providers, $missingProviders); - $this->twoFactorManager->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); - $this->userSession - ->method('isLoggedIn') - ->willReturn(false); - - if ($expectEception) { - $this->expectException(TwoFactorAuthRequiredException::class); - } else { - // hack to make phpunit shut up. Since we don't expect an exception here... - $this->assertTrue(true); - } - - $controller = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class) - ->disableOriginalConstructor() - ->getMock(); - $this->middleware->beforeController($controller, 'index'); - } -} --- a/tests/Core/Notification/CoreNotifierTest.php +++ /dev/null @@ -1,55 +0,0 @@ -createMock(IL10N::class); - $l10n->method('t')->willReturnArgument(0); - $l10n->method('n')->willReturnCallback( - fn (string $singular, string $plural, int $count, array $parameters = []): string - => vsprintf(str_replace('%n', (string)$count, $count === 1 ? $singular : $plural), $parameters) - ); - $factory = $this->createMock(IFactory::class); - $factory->method('get')->with('core', 'de')->willReturn($l10n); - - $url = $this->createMock(IURLGenerator::class); - $url->method('linkToRouteAbsolute') - ->with('settings.AdminSettings.index', ['section' => 'overview']) - ->willReturn('https://cloud.example.com/settings/admin/overview'); - $url->method('getAbsoluteURL')->willReturn('https://cloud.example.com/core/img/actions/error.svg'); - - $notification = new Notification($this->createMock(IValidator::class), $this->createMock(IRichTextFormatter::class)); - $notification->setApp('core') - ->setSubject('code_integrity_changed', ['files' => 1, 'unverified' => ['core']]); - - $notifier = new CoreNotifier($this->createMock(IConfig::class), $factory, $url); - $notifier->prepare($notification, 'de'); - - $this->assertSame('The code integrity check result has changed', $notification->getParsedSubject()); - $this->assertSame( - "1 file does not match the signed release. It may have been modified or added without authorization.\n" - . "The signature of core is missing or invalid, so it could not be verified.\n" - . 'Review the results in the administration overview.', - $notification->getParsedMessage(), - ); - $this->assertSame('https://cloud.example.com/settings/admin/overview', $notification->getLink()); - } -} --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ /dev/null @@ -1,449 +0,0 @@ -setupSubjectUnderTest(); - } - - /** - * Setup subject under test with mocked constructor arguments. - * - * Code was moved to separate function to keep setUp function small and clear. - */ - private function setupSubjectUnderTest(): void { - $this->config = $this->createMock(IConfig::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->mapper = $this->createMock(LoginFlowV2Mapper::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->secureRandom = $this->createMock(ISecureRandom::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - - $this->subjectUnderTest = new LoginFlowV2Service( - $this->mapper, - $this->secureRandom, - $this->timeFactory, - $this->config, - $this->crypto, - $this->logger, - $this->tokenProvider - ); - } - - /** - * Generates for a given password required OpenSSL parts. - * - * @return array Array contains encrypted password, private key and public key. - */ - private function getOpenSSLEncryptedPublicAndPrivateKey(string $appPassword): array { - // Create the private and public key - $res = openssl_pkey_new([ - 'digest_alg' => 'md5', // take fast algorithm for testing purposes - 'private_key_bits' => 512, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - - // Extract the private key from $res - openssl_pkey_export($res, $privateKey); - - // Extract the public key from $res - $publicKey = openssl_pkey_get_details($res); - $publicKey = $publicKey['key']; - - // Encrypt the data to $encrypted using the public key - openssl_public_encrypt($appPassword, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); - - return [$encrypted, $privateKey, $publicKey]; - } - - /* - * Tests for poll - */ - public function testPollPrivateKeyCouldNotBeDecrypted(): void { - $this->expectException(LoginFlowV2NotFoundException::class); - $this->expectExceptionMessage('Apptoken could not be decrypted'); - - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willThrowException(new \Exception('HMAC mismatch')); - - /* - * Cannot be mocked, because functions like getLoginName are magic functions. - * To be able to set internal properties, we have to use the real class here. - */ - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setLoginName('test'); - $loginFlowV2->setServer('test'); - $loginFlowV2->setAppPassword('test'); - $loginFlowV2->setPrivateKey('test'); - - $this->mapper->expects($this->once()) - ->method('getByPollToken') - ->willReturn($loginFlowV2); - - $this->subjectUnderTest->poll(''); - } - - public function testPollApptokenCouldNotBeDecrypted(): void { - $this->expectException(LoginFlowV2NotFoundException::class); - $this->expectExceptionMessage('Apptoken could not be decrypted'); - - /* - * Cannot be mocked, because functions like getLoginName are magic functions. - * To be able to set internal properties, we have to use the real class here. - */ - [$encrypted, $privateKey,] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test'); - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setLoginName('test'); - $loginFlowV2->setServer('test'); - $loginFlowV2->setAppPassword('broken#' . $encrypted); - $loginFlowV2->setPrivateKey('encrypted(test)'); - - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willReturn($privateKey); - - $this->mapper->expects($this->once()) - ->method('getByPollToken') - ->willReturn($loginFlowV2); - - $this->subjectUnderTest->poll('test'); - } - - public function testPollInvalidToken(): void { - $this->expectException(LoginFlowV2NotFoundException::class); - $this->expectExceptionMessage('Invalid token'); - - $this->mapper->expects($this->once()) - ->method('getByPollToken') - ->willThrowException(new DoesNotExistException('')); - - $this->subjectUnderTest->poll(''); - } - - public function testPollTokenNotYetReady(): void { - $this->expectException(LoginFlowV2NotFoundException::class); - $this->expectExceptionMessage('Token not yet ready'); - - $this->subjectUnderTest->poll(''); - } - - public function testPollRemoveDataFromDb(): void { - [$encrypted, $privateKey] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass'); - - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willReturn($privateKey); - - /* - * Cannot be mocked, because functions like getLoginName are magic functions. - * To be able to set internal properties, we have to use the real class here. - */ - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setLoginName('test_login'); - $loginFlowV2->setServer('test_server'); - $loginFlowV2->setAppPassword(base64_encode($encrypted)); - $loginFlowV2->setPrivateKey($privateKey); - - $this->mapper->expects($this->once()) - ->method('delete') - ->with($this->equalTo($loginFlowV2)); - - $this->mapper->expects($this->once()) - ->method('getByPollToken') - ->willReturn($loginFlowV2); - - $credentials = $this->subjectUnderTest->poll(''); - - $this->assertTrue($credentials instanceof LoginFlowV2Credentials); - $this->assertEquals( - [ - 'server' => 'test_server', - 'loginName' => 'test_login', - 'appPassword' => 'test_pass', - ], - $credentials->jsonSerialize() - ); - } - - /* - * Tests for getByLoginToken - */ - - public function testGetByLoginToken(): void { - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setLoginName('test_login'); - $loginFlowV2->setServer('test_server'); - $loginFlowV2->setAppPassword('test'); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willReturn($loginFlowV2); - - $result = $this->subjectUnderTest->getByLoginToken('test_token'); - - $this->assertTrue($result instanceof LoginFlowV2); - $this->assertEquals('test_server', $result->getServer()); - $this->assertEquals('test_login', $result->getLoginName()); - $this->assertEquals('test', $result->getAppPassword()); - } - - public function testGetByLoginTokenLoginTokenInvalid(): void { - $this->expectException(LoginFlowV2NotFoundException::class); - $this->expectExceptionMessage('Login token invalid'); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willThrowException(new DoesNotExistException('')); - - $this->subjectUnderTest->getByLoginToken('test_token'); - } - - public function testGetByLoginTokenClientForbidden() { - $this->expectException(LoginFlowV2ClientForbiddenException::class); - $this->expectExceptionMessage('Client not allowed'); - - $allowedClients = [ - '/Custom Allowed Client/i' - ]; - - $this->config->expects($this->exactly(1)) - ->method('getSystemValue') - ->willReturnCallback(function ($key) use ($allowedClients) { - // Note: \OCP\IConfig::getSystemValue returns either an array or string. - return $key == 'core.login_flow_v2.allowed_user_agents' ? $allowedClients : ''; - }); - - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setClientName('Rogue Curl Client/1.0'); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willReturn($loginFlowV2); - - $this->subjectUnderTest->getByLoginToken('test_token'); - } - - public function testGetByLoginTokenClientAllowed() { - $allowedClients = [ - '/Foo Allowed Client/i', - '/Custom Allowed Client/i' - ]; - - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setClientName('Custom Allowed Client Curl Client/1.0'); - - $this->config->expects($this->exactly(1)) - ->method('getSystemValue') - ->willReturnCallback(function ($key) use ($allowedClients) { - // Note: \OCP\IConfig::getSystemValue returns either an array or string. - return $key == 'core.login_flow_v2.allowed_user_agents' ? $allowedClients : ''; - }); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willReturn($loginFlowV2); - - $result = $this->subjectUnderTest->getByLoginToken('test_token'); - - $this->assertTrue($result instanceof LoginFlowV2); - $this->assertEquals('Custom Allowed Client Curl Client/1.0', $result->getClientName()); - } - - /* - * Tests for startLoginFlow - */ - - public function testStartLoginFlow(): void { - $loginFlowV2 = new LoginFlowV2(); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willReturn($loginFlowV2); - - $this->mapper->expects($this->once()) - ->method('update'); - - $this->assertTrue($this->subjectUnderTest->startLoginFlow('test_token')); - } - - public function testStartLoginFlowDoesNotExistException(): void { - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willThrowException(new DoesNotExistException('')); - - $this->assertFalse($this->subjectUnderTest->startLoginFlow('test_token')); - } - - /** - * If an exception not of type DoesNotExistException is thrown, - * it is expected that it is not being handled by startLoginFlow. - */ - public function testStartLoginFlowException(): void { - $this->expectException(Exception::class); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willThrowException(new Exception('')); - - $this->subjectUnderTest->startLoginFlow('test_token'); - } - - /* - * Tests for flowDone - */ - - public function testFlowDone(): void { - [,, $publicKey] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass'); - - $loginFlowV2 = new LoginFlowV2(); - $loginFlowV2->setPublicKey($publicKey); - $loginFlowV2->setClientName('client_name'); - - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willReturn($loginFlowV2); - - $this->mapper->expects($this->once()) - ->method('update'); - - $this->secureRandom->expects($this->once()) - ->method('generate') - ->with(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS) - ->willReturn('test_pass'); - - // session token - $sessionToken = $this->getMockBuilder(IToken::class)->disableOriginalConstructor()->getMock(); - $sessionToken->expects($this->once()) - ->method('getLoginName') - ->willReturn('login_name'); - - $this->tokenProvider->expects($this->once()) - ->method('getPassword') - ->willReturn('test_pass'); - - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->willReturn($sessionToken); - - $this->tokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'test_pass', - 'user_id', - 'login_name', - 'test_pass', - 'client_name', - IToken::PERMANENT_TOKEN, - IToken::DO_NOT_REMEMBER - ); - - $result = $this->subjectUnderTest->flowDone( - 'login_token', - 'session_id', - 'server', - 'user_id' - ); - $this->assertTrue($result); - - // app password is encrypted and must look like: - // ZACZOOzxTpKz4+KXL5kZ/gCK0xvkaVi/8yzupAn6Ui6+5qCSKvfPKGgeDRKs0sivvSLzk/XSp811SZCZmH0Y3g== - $this->assertMatchesRegularExpression('/[a-zA-Z\/0-9+=]+/', $loginFlowV2->getAppPassword()); - - $this->assertEquals('server', $loginFlowV2->getServer()); - } - - public function testFlowDoneDoesNotExistException(): void { - $this->mapper->expects($this->once()) - ->method('getByLoginToken') - ->willThrowException(new DoesNotExistException('')); - - $result = $this->subjectUnderTest->flowDone( - 'login_token', - 'session_id', - 'server', - 'user_id' - ); - $this->assertFalse($result); - } - - public function testFlowDonePasswordlessTokenException(): void { - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->willThrowException(new InvalidTokenException('')); - - $result = $this->subjectUnderTest->flowDone( - 'login_token', - 'session_id', - 'server', - 'user_id' - ); - $this->assertFalse($result); - } - - /* - * Tests for createTokens - */ - - public function testCreateTokens(): void { - $this->config->expects($this->exactly(2)) - ->method('getSystemValue') - ->willReturnCallback(function ($key) { - // Note: \OCP\IConfig::getSystemValue returns either an array or string. - return $key == 'openssl' ? [] : ''; - }); - - $this->mapper->expects($this->once()) - ->method('insert'); - - $this->secureRandom->expects($this->exactly(2)) - ->method('generate'); - - $token = $this->subjectUnderTest->createTokens('user_agent'); - $this->assertTrue($token instanceof LoginFlowV2Tokens); - } -} --- a/tests/Core/Sharing/Property/ExpirationDateSharePropertyTypeTest.php +++ /dev/null @@ -1,224 +0,0 @@ -createUser('user', 'password'); - $this->assertNotFalse($user); - $this->user = $user; - - $this->propertyType = Server::get(ExpirationDateSharePropertyType::class); - - $this->tokenRecipient = new ShareRecipient(TokenShareRecipientType::class, 'token', null); - $this->emailRecipient = new ShareRecipient(EmailShareRecipientType::class, 'example@example.com', null); - $this->remoteRecipient = new ShareRecipient(UserShareRecipientType::class, 'user', 'https://example.com'); - $this->localNonTokenAndEmailRecipient = new ShareRecipient(UserShareRecipientType::class, 'user', null); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - - $this->user->delete(); - } - - private function createDummyShare(ShareProperty $property): Share { - return new Share( - '123', - new ShareUser($this->user->getUID(), null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [$property->class => $property], - [], - ); - } - - /** - * @return list - */ - public static function dataConfigKeys(): array { - return [ - ['shareapi_default_expire_date', 'shareapi_enforce_expire_date', 'shareapi_expire_after_n_days'], - ['shareapi_default_remote_expire_date', 'shareapi_enforce_remote_expire_date', 'shareapi_remote_expire_after_n_days'], - ['shareapi_default_internal_expire_date', 'shareapi_enforce_internal_expire_date', 'shareapi_internal_expire_after_n_days'], - ]; - } - - /** @psalm-suppress DeprecatedMethod The configs are only partly migrated to IAppConfig, so using deprecated IConfig is easier for now. */ - #[DataProvider('dataConfigKeys')] - public function testGetRequired(string $defaultEnabledKey, string $defaultEnforcedKey, string $defaultValueKey): void { - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [ - $this->tokenRecipient, - $this->emailRecipient, - $this->remoteRecipient, - $this->localNonTokenAndEmailRecipient, - ], - [], - [], - ); - - $config = Server::get(IConfig::class); - foreach (array_merge(...self::dataConfigKeys()) as $key) { - $config->deleteAppValue(Application::APP_ID, $key); - } - - $config->setAppValue(Application::APP_ID, $defaultEnabledKey, 'yes'); - - $this->assertFalse($this->propertyType->isRequired($share)); - - $config->setAppValue(Application::APP_ID, $defaultEnforcedKey, 'yes'); - $this->assertTrue($this->propertyType->isRequired($share)); - - $config->deleteAppValue(Application::APP_ID, $defaultEnabledKey); - $config->deleteAppValue(Application::APP_ID, $defaultEnforcedKey); - } - - /** @psalm-suppress DeprecatedMethod The configs are only partly migrated to IAppConfig, so using deprecated IConfig is easier for now. */ - #[DataProvider('dataConfigKeys')] - public function testGetDefaultValue(string $defaultEnabledKey, string $defaultEnforcedKey, string $defaultValueKey): void { - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [ - $this->tokenRecipient, - $this->emailRecipient, - $this->remoteRecipient, - $this->localNonTokenAndEmailRecipient, - ], - [], - [], - ); - - /** @var DateTimeImmutable $now */ - $now = self::invokePrivate($this->propertyType, 'now'); - - $config = Server::get(IConfig::class); - foreach (array_merge(...self::dataConfigKeys()) as $key) { - $config->deleteAppValue(Application::APP_ID, $key); - } - - $this->assertNull($this->propertyType->getDefaultValue($share)); - - $config->setAppValue(Application::APP_ID, $defaultEnabledKey, 'yes'); - $this->assertEquals($now->add(new DateInterval('P7D'))->format(DateTimeInterface::ATOM), $this->propertyType->getDefaultValue($share)); - $config->deleteAppValue(Application::APP_ID, $defaultEnabledKey); - } - - /** @psalm-suppress DeprecatedMethod The configs are only partly migrated to IAppConfig, so using deprecated IConfig is easier for now. */ - #[DataProvider('dataConfigKeys')] - public function testGetMinMaxDate(string $defaultEnabledKey, string $defaultEnforcedKey, string $defaultValueKey): void { - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [ - $this->tokenRecipient, - $this->emailRecipient, - $this->remoteRecipient, - $this->localNonTokenAndEmailRecipient, - ], - [], - [], - ); - - /** @var DateTimeImmutable $now */ - $now = self::invokePrivate($this->propertyType, 'now'); - - $config = Server::get(IConfig::class); - foreach (array_merge(...self::dataConfigKeys()) as $key) { - $config->deleteAppValue(Application::APP_ID, $key); - } - - $this->assertEquals($now->add(new DateInterval('PT5M')), $this->propertyType->getMinDate($share)); - $this->assertNull($this->propertyType->getMaxDate($share)); - - $config->setAppValue(Application::APP_ID, $defaultEnabledKey, 'yes'); - - $this->assertEquals($now->add(new DateInterval('PT5M')), $this->propertyType->getMinDate($share)); - $this->assertNull($this->propertyType->getMaxDate($share)); - - $config->setAppValue(Application::APP_ID, $defaultEnforcedKey, 'yes'); - $config->setAppValue(Application::APP_ID, $defaultValueKey, '123'); - - $this->assertEquals($now->add(new DateInterval('PT5M')), $this->propertyType->getMinDate($share)); - $this->assertEquals($now->add(new DateInterval('P123DT5M')), $this->propertyType->getMaxDate($share)); - - $config->deleteAppValue(Application::APP_ID, $defaultEnabledKey); - $config->deleteAppValue(Application::APP_ID, $defaultEnforcedKey); - $config->deleteAppValue(Application::APP_ID, $defaultValueKey); - } - - public function testIsFiltered(): void { - /** @var DateTimeImmutable $now */ - $now = self::invokePrivate($this->propertyType, 'now'); - $future = $now->add(new DateInterval('PT1M'))->format(DateTimeInterface::ATOM); - $past = $now->sub(new DateInterval('PT1M'))->format(DateTimeInterface::ATOM); - - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(), $this->createDummyShare(new ShareProperty($this->propertyType::class, $future)))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(), $this->createDummyShare(new ShareProperty($this->propertyType::class, $now->format(DateTimeInterface::ATOM))))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(), $this->createDummyShare(new ShareProperty($this->propertyType::class, $past)))); - } -} --- a/tests/Core/Sharing/Property/PasswordSharePropertyTypeTest.php +++ /dev/null @@ -1,153 +0,0 @@ -createUser('user', 'password'); - $this->assertNotFalse($user); - $this->user = $user; - - $this->propertyType = Server::get(PasswordSharePropertyType::class); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - - $this->user->delete(); - } - - private function createDummyShare(?ShareProperty $property): Share { - $properties = []; - if ($property instanceof ShareProperty) { - $properties[$property->class] = $property; - } - - return new Share( - '123', - new ShareUser($this->user->getUID(), null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - $properties, - [], - ); - } - - public function testGetDefaultValue(): void { - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - - $appConfig = Server::get(IAppConfig::class); - $appConfig->deleteKey(Application::APP_ID, ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED); - - $this->assertNull($this->propertyType->getDefaultValue($share)); - - $appConfig->setValueBool(Application::APP_ID, ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED, true); - - $value = $this->propertyType->getDefaultValue($share); - $this->assertNotNull($value); - /** @psalm-suppress RedundantCastGivenDocblockType psalm:strict and rector:strict fight over the cast -_- */ - $this->assertGreaterThan(1, strlen((string)$value)); - $this->assertTrue($this->propertyType->validateValue(Server::get(IFactory::class), $share, $value)); - - $appConfig->deleteKey(Application::APP_ID, ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED); - } - - public function testIsFiltered(): void { - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(arguments: [$this->propertyType::class => '123']), $this->createDummyShare(new ShareProperty($this->propertyType::class, Server::get(IHasher::class)->hash('123'))))); - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(arguments: [$this->propertyType::class => '123']), $this->createDummyShare(new ShareProperty($this->propertyType::class, null)))); - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(arguments: [$this->propertyType::class => '123']), $this->createDummyShare(null))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(arguments: [$this->propertyType::class => '456']), $this->createDummyShare(new ShareProperty($this->propertyType::class, Server::get(IHasher::class)->hash('123'))))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(arguments: [$this->propertyType::class => null]), $this->createDummyShare(new ShareProperty($this->propertyType::class, Server::get(IHasher::class)->hash('123'))))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(), $this->createDummyShare(new ShareProperty($this->propertyType::class, Server::get(IHasher::class)->hash('123'))))); - } - - public function testIsExpiredEmailPasswordFiltered(): void { - /** @var DateTimeImmutable $now */ - $now = self::invokePrivate($this->propertyType, 'now'); - - $password = '123'; - $hashedPassword = Server::get(IHasher::class)->hash($password); - - $secret = 'abc'; - - $createShare = fn (DateTimeImmutable $lastUpdated): Share => new Share( - '456', - new ShareUser('user', null), - $lastUpdated, - ShareState::Active, - null, - [], - [ - new ShareRecipient(EmailShareRecipientType::class, 'test@example.com', null, $secret), - ], - [ - $this->propertyType::class => new ShareProperty($this->propertyType::class, $hashedPassword), - ], - [], - ); - - $expirationIntervalSeconds = 10; - $config = Server::get(IConfig::class); - $config->setSystemValue('sharing.enable_mail_link_password_expiration', true); - $config->setSystemValue('sharing.mail_link_password_expiration_interval', $expirationIntervalSeconds); - - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(secret: $secret, arguments: [$this->propertyType::class => $password]), $createShare($now))); - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(secret: $secret, arguments: [$this->propertyType::class => $password]), $createShare($now->sub(new DateInterval('PT' . ($expirationIntervalSeconds - 1) . 'S'))))); - $this->assertFalse($this->propertyType->isFiltered(new ShareAccessContext(secret: $secret, arguments: [$this->propertyType::class => $password]), $createShare($now->sub(new DateInterval('PT' . ($expirationIntervalSeconds) . 'S'))))); - $this->assertTrue($this->propertyType->isFiltered(new ShareAccessContext(secret: $secret, arguments: [$this->propertyType::class => $password]), $createShare($now->sub(new DateInterval('PT' . ($expirationIntervalSeconds + 1) . 'S'))))); - - $config->deleteSystemValue('sharing.enable_mail_link_password_expiration'); - $config->deleteSystemValue('sharing.mail_link_password_expiration_interval'); - } -} --- a/tests/Core/Sharing/Recipient/EmailShareRecipientTypeTest.php +++ /dev/null @@ -1,124 +0,0 @@ -createUser($uid, $password); - $this->assertNotFalse($user); - return $user; - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $userManager = Server::get(IUserManager::class); - $userManager->clearBackends(); - $userManager->registerBackend(new Database()); - - $this->user1 = $this->createUser($userManager, 'user1', 'password'); - - self::loginAsUser($this->user1->getUID()); - - $this->recipientType = Server::get(EmailShareRecipientType::class); - } - - #[\Override] - protected function tearDown(): void { - $this->user1->delete(); - - parent::tearDown(); - } - - public function testValidateRecipient(): void { - $this->assertTrue($this->recipientType->validateRecipient('example@example.com')); - $this->assertFalse($this->recipientType->validateRecipient('example')); - $this->assertFalse($this->recipientType->validateRecipient('example@example')); - $this->assertFalse($this->recipientType->validateRecipient('example.com')); - $this->assertFalse($this->recipientType->validateRecipient('@')); - } - - public function testGetRecipientDisplayName(): void { - $this->assertNull($this->recipientType->getRecipientDisplayName('example@example.com', null)); - $this->assertNull($this->recipientType->getRecipientDisplayName('example@example.com', 'https://example.com')); - - $cardDavBackend = Server::get(CardDavBackend::class); - $contactsManager = Server::get(IManager::class); - $addressBookId = $cardDavBackend->createAddressBook('principals/users/' . $this->user1->getUID(), 'Personal', []); - Server::get(ContactsManager::class)->setupContactsProvider($contactsManager, $this->user1->getUID(), Server::get(IURLGenerator::class)); - - $contactsManager->createOrUpdate(['EMAIL' => 'example@example.com', 'FN' => 'Email 1'], (string)$addressBookId); - - $this->assertEquals('Email 1', $this->recipientType->getRecipientDisplayName('example@example.com', null)); - $this->assertNull($this->recipientType->getRecipientDisplayName('example@example.com', 'https://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName('example@example.org', null)); - - $cardDavBackend->deleteAddressBook($addressBookId); - } - - public function testSearchRecipients(): void { - if (Server::get(IDBConnection::class)->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) { - $this->markTestSkipped('PostgreSQL has unstable test results'); - } - - $cardDavBackend = Server::get(CardDavBackend::class); - $contactsManager = Server::get(IManager::class); - $addressBookId = $cardDavBackend->createAddressBook('principals/users/' . $this->user1->getUID(), 'Personal', []); - Server::get(ContactsManager::class)->setupContactsProvider($contactsManager, $this->user1->getUID(), Server::get(IURLGenerator::class)); - - foreach (['email1@example.com', 'email2@example.com', 'email3@example.com', 'email4@example.com'] as $email) { - $contactsManager->createOrUpdate(['EMAIL' => $email], (string)$addressBookId); - } - - $accessContext = new ShareAccessContext(currentUser: $this->user1); - - /** @psalm-suppress ArgumentTypeCoercion */ - $generateRecipient = static fn (string $email): ShareRecipient => new ShareRecipient( - EmailShareRecipientType::class, - $email, - null, - ); - - $this->assertEquals(array_map($generateRecipient(...), ['email1@example.com', 'email2@example.com', 'email3@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 3, 0)); - $this->assertEquals(array_map($generateRecipient(...), ['email2@example.com', 'email3@example.com', 'email4@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 3, 1)); - $this->assertEquals(array_map($generateRecipient(...), ['email3@example.com', 'email4@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 4, 2)); - - $this->assertEquals(array_map($generateRecipient(...), ['email1@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 1, 0)); - $this->assertEquals(array_map($generateRecipient(...), ['email1@example.com', 'email2@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 2, 0)); - $this->assertEquals(array_map($generateRecipient(...), ['email2@example.com', 'email3@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 2, 1)); - $this->assertEquals(array_map($generateRecipient(...), ['email3@example.com', 'email4@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 2, 2)); - $this->assertEquals(array_map($generateRecipient(...), ['email4@example.com']), $this->recipientType->searchRecipients($accessContext, 'email', 2, 3)); - - $this->assertEquals(array_map($generateRecipient(...), ['email1@example.com']), $this->recipientType->searchRecipients($accessContext, 'email1', 2, 0)); - - $cardDavBackend->deleteAddressBook($addressBookId); - } -} --- a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php +++ /dev/null @@ -1,181 +0,0 @@ - 'Group 1', - 'group2' => 'Group 2', - 'group3' => 'Group 3', - ]; - - private function createUser(IUserManager $userManager, string $uid, string $password): IUser { - $user = $userManager->createUser($uid, $password); - $this->assertNotFalse($user); - return $user; - } - - private function createGroup(IGroupManager $groupManager, string $gid): IGroup { - $group = $groupManager->createGroup($gid); - $this->assertNotNull($group); - $this->assertTrue($group->setDisplayName(self::DISPLAY_NAMES[$gid])); - return $group; - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->dbConnection = Server::get(IDBConnection::class); - - $this->manager = Server::get(ISharingManager::class); - - $userManager = Server::get(IUserManager::class); - $this->user1 = $this->createUser($userManager, 'user1', 'password'); - - $groupManager = Server::get(IGroupManager::class); - $groupManager->clearBackends(); - $groupManager->addBackend(new Database()); - - $this->group1 = $this->createGroup($groupManager, 'group1'); - $this->group2 = $this->createGroup($groupManager, 'group2'); - $this->group3 = $this->createGroup($groupManager, 'group3'); - - $this->group1->addUser($this->user1); - - $this->recipientType = new GroupShareRecipientType(Server::get(IEventDispatcher::class), $this->dbConnection, $groupManager, $this->manager); - } - - #[\Override] - protected function tearDown(): void { - $this->user1->delete(); - $this->group1->delete(); - $this->group2->delete(); - $this->group3->delete(); - - parent::tearDown(); - } - - public function testValidateRecipient(): void { - $this->assertTrue($this->recipientType->validateRecipient('group1')); - $this->assertFalse($this->recipientType->validateRecipient('invalid')); - } - - public function testGetRecipientValues(): void { - $this->assertEquals(['group1'], $this->recipientType->getRecipients($this->user1, null)); - } - - public function testGetRecipientDisplayName(): void { - // Clear display name cache, because setting the display name on the group doesn't update it in the cache of the manager - self::invokePrivate(self::invokePrivate(Server::get(IGroupManager::class), 'displayNameCache'), 'clear'); - - $this->assertEquals('Group 1', $this->recipientType->getRecipientDisplayName($this->group1->getGID(), null)); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->group1->getGID(), 'https://example.com')); - - $cardDavBackend = Server::get(CardDavBackend::class); - $contactsManager = Server::get(IManager::class); - $addressBookId = $cardDavBackend->createAddressBook('principals/users/' . $this->user1->getUID(), 'Personal', []); - Server::get(ContactsManager::class)->setupContactsProvider($contactsManager, $this->user1->getUID(), Server::get(IURLGenerator::class)); - - $contactsManager->createOrUpdate(['CLOUD' => $this->group1->getGID() . '@https://example.com', 'FN' => 'Remote Group 1'], (string)$addressBookId); - - $this->assertEquals('Remote Group 1', $this->recipientType->getRecipientDisplayName($this->group1->getGID(), 'https://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->group2->getGID(), 'https://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->group1->getGID(), 'https://example.org')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->group1->getGID(), 'http://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->group1->getGID(), 'example.com')); - - $cardDavBackend->deleteAddressBook($addressBookId); - } - - public function testSearchRecipients(): void { - $accessContext = new ShareAccessContext(currentUser: $this->user1); - self::loginAsUser($this->user1->getUID()); - - $generateRecipient = static fn (IGroup $group): ShareRecipient => new ShareRecipient( - GroupShareRecipientType::class, - $group->getGID(), - null, - ); - - $this->assertEquals(array_map($generateRecipient(...), [$this->group1, $this->group2, $this->group3]), $this->recipientType->searchRecipients($accessContext, 'group', 3, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->group1]), $this->recipientType->searchRecipients($accessContext, 'group', 1, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->group2, $this->group3]), $this->recipientType->searchRecipients($accessContext, 'group', 3, 1)); - $this->assertEquals(array_map($generateRecipient(...), [$this->group2]), $this->recipientType->searchRecipients($accessContext, 'group', 1, 1)); - - $this->assertEquals(array_map($generateRecipient(...), [$this->group1]), $this->recipientType->searchRecipients($accessContext, 'group1', 1, 0)); - - $this->assertEquals([new ShareRecipient(GroupShareRecipientType::class, 'example', 'example.com')], $this->recipientType->searchRecipients($accessContext, 'example@example.com', 1, 0)); - $this->assertEquals([new ShareRecipient(GroupShareRecipientType::class, 'example', 'https://example.com')], $this->recipientType->searchRecipients($accessContext, 'example@https://example.com', 1, 0)); - } - - public function testDelete(): void { - $registry = Server::get(ISharingRegistry::class); - $registry->clear(); - $registry->registerRecipientType($this->recipientType); - - $accessContext = new ShareAccessContext(currentUser: $this->user1); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient($this->recipientType::class, $this->group1->getGID(), null)); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $this->group1->delete(); - $after = $this->manager->getTime(); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->getShare($accessContext, $share->id); - $this->assertGreaterThanOrEqual(SharingManager::timeToMs($before), SharingManager::timeToMs($share->lastUpdated)); - $this->assertLessThanOrEqual(SharingManager::timeToMs($after), SharingManager::timeToMs($share->lastUpdated)); - $this->assertEquals([], $share->recipients); - - $this->manager->deleteShare($accessContext, $share); - $this->dbConnection->commit(); - $registry->clear(); - } -} --- a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php +++ /dev/null @@ -1,188 +0,0 @@ - 'Team 1', - 'team2' => 'Team 2', - 'team3' => 'Team 3', - ]; - - private function createUser(IUserManager $userManager, string $uid, string $password): IUser { - $user = $userManager->createUser($uid, $password); - $this->assertNotFalse($user); - return $user; - } - - private function createTeam(ITeamManager $teamManager, string $name): Team { - $circlesManager = Server::get(CirclesManager::class); - $circlesManager->startSession($circlesManager->getLocalFederatedUser($this->user1->getUID())); - - $circle = $circlesManager->createCircle($name); - Server::get(CircleService::class)->updateName($circle->getSingleId(), self::DISPLAY_NAMES[$name]); - - $team = $teamManager->getTeam($circle->getSingleId(), $this->user1->getUID()); - $this->assertNotNull($team); - return $team; - } - - #[\Override] - public static function setUpBeforeClass(): void { - parent::setUpBeforeClass(); - - if (!class_exists(CirclesManager::class)) { - self::markTestSkipped('Circles app is not installed'); - } - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->dbConnection = Server::get(IDBConnection::class); - - $this->manager = Server::get(ISharingManager::class); - - $userManager = Server::get(IUserManager::class); - $this->user1 = $this->createUser($userManager, 'user1', 'password'); - - $teamManager = Server::get(ITeamManager::class); - - $this->team1 = $this->createTeam($teamManager, 'team1'); - $this->team2 = $this->createTeam($teamManager, 'team2'); - $this->team3 = $this->createTeam($teamManager, 'team3'); - - $this->recipientType = new TeamShareRecipientType(Server::get(IEventDispatcher::class), $this->dbConnection, $this->manager); - } - - #[\Override] - protected function tearDown(): void { - $circlesManager = Server::get(CirclesManager::class); - $circlesManager->startSession($circlesManager->getLocalFederatedUser($this->user1->getUID())); - try { - $circlesManager->destroyCircle($this->team1->getId()); - } catch (CircleNotFoundException) { - } - - $circlesManager->destroyCircle($this->team2->getId()); - $circlesManager->destroyCircle($this->team3->getId()); - - $this->user1->delete(); - - parent::tearDown(); - } - - /** @psalm-suppress UnevaluatedCode Test is skipped */ - public function testValidateRecipient(): void { - $this->markTestSkipped('This test is broken if run together with other test suites because circles 🤷‍♀️'); - $this->assertTrue($this->recipientType->validateRecipient($this->team1->getId())); - $this->assertFalse($this->recipientType->validateRecipient('invalid')); - } - - public function testGetRecipientValues(): void { - $this->assertContains($this->team1->getId(), $this->recipientType->getRecipients($this->user1, null)); - } - - /** @psalm-suppress UnevaluatedCode Test is skipped */ - public function testGetRecipientDisplayName(): void { - $this->markTestSkipped('This test is broken if run together with other test suites because circles 🤷‍♀️'); - $this->assertEquals('Team 1', $this->recipientType->getRecipientDisplayName($this->team1->getId())); - } - - public function testSearchRecipients(): void { - $accessContext = new ShareAccessContext(currentUser: $this->user1); - self::loginAsUser($this->user1->getUID()); - - $generateRecipient = static fn (Team $team): ShareRecipient => new ShareRecipient( - TeamShareRecipientType::class, - $team->getId(), - null, - ); - - $this->assertEquals(array_map($generateRecipient(...), [$this->team1, $this->team2, $this->team3]), $this->recipientType->searchRecipients($accessContext, 'Team', 3, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->team1]), $this->recipientType->searchRecipients($accessContext, 'Team', 1, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->team2, $this->team3]), $this->recipientType->searchRecipients($accessContext, 'Team', 3, 1)); - $this->assertEquals(array_map($generateRecipient(...), [$this->team2]), $this->recipientType->searchRecipients($accessContext, 'Team', 1, 1)); - - $this->assertEquals(array_map($generateRecipient(...), [$this->team1]), $this->recipientType->searchRecipients($accessContext, 'Team 1', 1, 0)); - } - - /** @psalm-suppress UnevaluatedCode Test is skipped */ - public function testDelete(): void { - $this->markTestSkipped('This test is broken if run together with other test suites because circles 🤷‍♀️'); - - $registry = Server::get(ISharingRegistry::class); - $registry->clear(); - $registry->registerRecipientType($this->recipientType); - - $accessContext = new ShareAccessContext(currentUser: $this->user1); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient($this->recipientType::class, $this->team1->getId(), null)); - $this->dbConnection->commit(); - - $circlesManager = Server::get(CirclesManager::class); - $circlesManager->startSession($circlesManager->getLocalFederatedUser($this->user1->getUID())); - - $before = $this->manager->getTime(); - $circlesManager->destroyCircle($this->team1->getId()); - $after = $this->manager->getTime(); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->getShare($accessContext, $share->id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated->getTimestamp()); - $this->assertLessThanOrEqual($after, $share->lastUpdated->getTimestamp()); - $this->assertEquals([], $share->recipients); - - $this->manager->deleteShare($accessContext, $share); - $this->dbConnection->commit(); - $registry->clear(); - } -} --- a/tests/Core/Sharing/Recipient/TokenShareRecipientTypeTest.php +++ /dev/null @@ -1,34 +0,0 @@ -recipientType = Server::get(TokenShareRecipientType::class); - } - - public function testValidateRecipient(): void { - $this->assertTrue($this->recipientType->validateRecipient(str_repeat('a', 32))); - $this->assertFalse($this->recipientType->validateRecipient(str_repeat('a', 32 - 1))); - $this->assertTrue($this->recipientType->validateRecipient(str_repeat('a', 255))); - $this->assertFalse($this->recipientType->validateRecipient(str_repeat('a', 255 + 1))); - } -} --- a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php +++ /dev/null @@ -1,190 +0,0 @@ - 'User 1', - 'user2' => 'User 2', - 'user3' => 'User 3', - 'user4' => 'User 4', - ]; - - private function createUser(IUserManager $userManager, string $uid, string $password): IUser { - $user = $userManager->createUser($uid, $password); - $this->assertNotFalse($user); - $this->assertTrue($user->setDisplayName(self::DISPLAY_NAMES[$uid])); - $user->setSystemEMailAddress($uid . '@example.com'); - return $user; - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->dbConnection = Server::get(IDBConnection::class); - - $this->manager = Server::get(ISharingManager::class); - - $userManager = Server::get(IUserManager::class); - $userManager->clearBackends(); - $userManager->registerBackend(new Database()); - - $this->user1 = $this->createUser($userManager, 'user1', 'password'); - $this->user2 = $this->createUser($userManager, 'user2', 'password'); - $this->user3 = $this->createUser($userManager, 'user3', 'password'); - $this->user4 = $this->createUser($userManager, 'user4', 'password'); - - $this->recipientType = new UserShareRecipientType(Server::get(IEventDispatcher::class), $this->dbConnection, $userManager, $this->manager); - } - - #[\Override] - protected function tearDown(): void { - $this->user1->delete(); - $this->user2->delete(); - $this->user3->delete(); - $this->user4->delete(); - - parent::tearDown(); - } - - public function testValidateRecipient(): void { - $this->assertTrue($this->recipientType->validateRecipient('user1')); - $this->assertFalse($this->recipientType->validateRecipient('invalid')); - } - - public function testGetRecipientValues(): void { - $this->assertEquals(['user1'], $this->recipientType->getRecipients($this->user1, null)); - } - - public function testGetRecipientDisplayName(): void { - $this->assertEquals('User 1', $this->recipientType->getRecipientDisplayName($this->user1->getUID(), null)); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->user1->getUID(), 'https://example.com')); - - $cardDavBackend = Server::get(CardDavBackend::class); - $contactsManager = Server::get(IManager::class); - $addressBookId = $cardDavBackend->createAddressBook('principals/users/' . $this->user1->getUID(), 'Personal', []); - Server::get(ContactsManager::class)->setupContactsProvider($contactsManager, $this->user1->getUID(), Server::get(IURLGenerator::class)); - - $contactsManager->createOrUpdate(['CLOUD' => $this->user1->getUID() . '@https://example.com', 'FN' => 'Remote User 1'], (string)$addressBookId); - - $this->assertEquals('Remote User 1', $this->recipientType->getRecipientDisplayName($this->user1->getUID(), 'https://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->user2->getUID(), 'https://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->user1->getUID(), 'https://example.org')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->user1->getUID(), 'http://example.com')); - $this->assertNull($this->recipientType->getRecipientDisplayName($this->user1->getUID(), 'example.com')); - - $cardDavBackend->deleteAddressBook($addressBookId); - } - - public function testGetRecipientIcon(): void { - $this->assertEquals(new ShareIconURL('http://localhost/index.php/avatar/user1/64', 'http://localhost/index.php/avatar/user1/64/dark'), $this->recipientType->getRecipientIcon($this->user1->getUID(), null)); - $this->assertEquals(new ShareIconURL('http://localhost/index.php/avatar/user1@https%253A%2F%2Fexample.com/64', 'http://localhost/index.php/avatar/user1@https%253A%2F%2Fexample.com/64/dark'), $this->recipientType->getRecipientIcon($this->user1->getUID(), 'https://example.com')); - } - - public function testSearchRecipients(): void { - if (in_array(Server::get(IDBConnection::class)->getDatabaseProvider(), [IDBConnection::PLATFORM_POSTGRES, IDBConnection::PLATFORM_ORACLE], true)) { - $this->markTestSkipped('PostgreSQL and Oracle have unstable test results'); - } - - $accessContext = new ShareAccessContext(currentUser: $this->user1); - self::loginAsUser($this->user1->getUID()); - - $generateRecipient = static fn (IUser $user): ShareRecipient => new ShareRecipient( - UserShareRecipientType::class, - $user->getUID(), - null, - ); - - // The UserPlugin already removes the current user (user1 here), leading to one result less than requested. - // This is an issue of the Collaborators API and can't be easily fixed. - // If the following tests fail, because different numbers of results are returned: congratulations, you fixed the problem! - - $this->assertEquals(array_map($generateRecipient(...), [$this->user2, $this->user3, $this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 3, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user2, $this->user3, $this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 4, 0)); - // Wrong: Offset not applied correctly - $this->assertEquals(array_map($generateRecipient(...), [$this->user2, $this->user3, $this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 4, 1)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user3, $this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 4, 2)); - - $this->assertEquals(array_map($generateRecipient(...), [$this->user2]), $this->recipientType->searchRecipients($accessContext, 'user', 1, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user2, $this->user3]), $this->recipientType->searchRecipients($accessContext, 'user', 2, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user2, $this->user3]), $this->recipientType->searchRecipients($accessContext, 'user', 2, 1)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user3, $this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 2, 2)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user4]), $this->recipientType->searchRecipients($accessContext, 'user', 2, 3)); - - $this->assertEquals(array_map($generateRecipient(...), [$this->user2]), $this->recipientType->searchRecipients($accessContext, 'user2', 2, 0)); - $this->assertEquals(array_map($generateRecipient(...), [$this->user2]), $this->recipientType->searchRecipients($accessContext, 'user2@example.com', 2, 0)); - - $this->assertEquals([new ShareRecipient(UserShareRecipientType::class, 'example', 'example.com')], $this->recipientType->searchRecipients($accessContext, 'example@example.com', 1, 0)); - $this->assertEquals([new ShareRecipient(UserShareRecipientType::class, 'example', 'https://example.com')], $this->recipientType->searchRecipients($accessContext, 'example@https://example.com', 1, 0)); - } - - public function testDelete(): void { - $registry = Server::get(ISharingRegistry::class); - $registry->clear(); - $registry->registerRecipientType($this->recipientType); - - $accessContext = new ShareAccessContext(currentUser: $this->user1); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient($this->recipientType::class, $this->user2->getUID(), null)); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $this->user2->delete(); - $after = $this->manager->getTime(); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->getShare($accessContext, $share->id); - $this->assertGreaterThanOrEqual(SharingManager::timeToMs($before), SharingManager::timeToMs($share->lastUpdated)); - $this->assertLessThanOrEqual(SharingManager::timeToMs($after), SharingManager::timeToMs($share->lastUpdated)); - $this->assertEquals([], $share->recipients); - - $this->manager->deleteShare($accessContext, $share); - $this->dbConnection->commit(); - $registry->clear(); - } -} --- a/tests/apps/testapp_dependency_missing/appinfo/info.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - testapp_infoxml - Test app - A b c - A b c - 1.2.3 - agpl - Jane - games - https://example.org - --- a/tests/apps/testapp_infoxml/appinfo/info.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - testapp_infoxml - Test app - A b c - A b c - 1.2.3 - agpl - Jane - games - https://example.org - - - - --- a/tests/apps/testapp_name_missing/appinfo/info.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - testapp_name_missing - A b c - A b c - 1.2.3 - agpl - Jane - games - https://example.org - - - - --- a/tests/apps/testapp_version/appinfo/info.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - testapp_version - Test app - A b c - A b c - agpl - Jane - games - https://example.org - - - - --- a/tests/autoload.php +++ /dev/null @@ -1,16 +0,0 @@ -addPsr4('Test\\', OC::$SERVERROOT . '/tests/lib/', true); --- a/tests/blueprints/basic.toml +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -# Used for performance testing, see .github/workflows/performance.yml - -[[user]] -id = "test" -groups = ["test_group"] -files = [ - "test.txt", - "foo/sub.png", - "empty", -] - -[[user]] -id = "test2" -groups = ["test"] -files = [ - "many_files/file_[1..100].txt" -] - -[[share]] -from = "test2" -to = "test" -file = "many_files" --- a/tests/bootstrap.php +++ /dev/null @@ -1,50 +0,0 @@ -addPsr4('Tests\\Core\\', OC::$SERVERROOT . '/tests/Core/', true); - -$dontLoadApps = getenv('TEST_DONT_LOAD_APPS'); -if (!$dontLoadApps) { - // load all apps - $appManager = Server::get(IAppManager::class); - foreach (new \DirectoryIterator(__DIR__ . '/../apps/') as $file) { - if ($file->isDot()) { - continue; - } - $appManager->loadApp($file->getFilename()); - } -} - -// Recorded on CI by default; set TEST_LOG_HTTP to a path for a manual run. -$logHttp = getenv('TEST_LOG_HTTP') ?: (getenv('CI') ? OC::$SERVERROOT . '/http-requests.log' : ''); -if ($logHttp !== '') { - \Test\HttpRequestLogger::install($logHttp); -} - -OC_Hook::clear(); - -set_include_path( - get_include_path() . PATH_SEPARATOR - . '/usr/share/php' . PATH_SEPARATOR - . __DIR__ . '/..' -); --- a/tests/data/REUSE.toml +++ /dev/null @@ -1,124 +0,0 @@ -version = 1 -SPDX-PackageName = "nextcloud" -SPDX-PackageSupplier = "Nextcloud " -SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server" - -[[annotations]] -path = ["integritycheck/htaccessWithValidModifiedContent/.htaccess"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc., 2019 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["block-aligned-plus-one.txt", "block-aligned.txt", "data.tar.gz", "data.zip", "desktopapp.png", "desktopapp.svg", "certificates/badCertificate.crt", "certificates/expiredCertificate.crt", "certificates/goodCertificate.crt", "certificates/openSslTrustedCertificate.crt", "integritycheck/app/AnotherFile.txt", "integritycheck/app/subfolder/file.txt", "integritycheck/appWithInvalidData/AnotherFile.txt", "integritycheck/appWithInvalidData/UnecessaryFile"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["integritycheck/htaccessUnmodified/.htaccess", "integritycheck/htaccessUnmodified/subfolder/.htaccess", "integritycheck/htaccessWithInvalidModifiedContent/.htaccess", "integritycheck/htaccessWithValidModifiedContent/subfolder/.htaccess"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["l10n/cs.json", "l10n/de.json", "l10n/ru.json"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2021 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["lorem.txt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2012 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["testavatar.png", "testimage.gif", "testimage.jpg", "testimage.png"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2013 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["lorem-big.txt", "strängé**filename**(duplicate**#2).txt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2014 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = "testimagelarge.svg" -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["setUploadLimit/htaccess", "setUploadLimit/user.ini"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc., 2018 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["testimage-wide.png", "testimage.eps", "testimage.mp3", "testimage.mp4", "testimage.odt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2015 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["testapp.zip", "testapp2.zip"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 ownCloud, Inc." -SPDX-License-Identifier = "AGPL-3.0-only" - -[[annotations]] -path = ["testimage.webp", "broken-video.webm"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016-2024 Nextcloud GmbH" -SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" - -[[annotations]] -path = ["testapp.0.8.tar.gz", "testapp.tar.gz", "testapp1.tar.gz"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2016 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["certificates/goodCertificate.crt", "emails/new-account-email-custom-text-alternative.txt", "emails/new-account-email-custom.html", "emails/new-account-email-custom.txt", "emails/new-account-email-single-button.html", "emails/new-account-email-single-button.txt", "emails/new-account-email.html", "emails/new-account-email.txt"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2017 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["testimage.heic"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2018 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["integritycheck/mimetypeListModified/core/js/mimetypelist.js", "integritycheck/mimetypeListModified/core/signature.json", "guest_avatar_einstein_32.png", "guest_avatar_einstein_32.svg", "test.pdf"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["testimage-badheader.jpg"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["testimage-large.jpg"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = ["testvideo-remote-file.mp4"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors" -SPDX-License-Identifier = "AGPL-3.0-or-later" - -[[annotations]] -path = "images/image.jpg" -precedence = "aggregate" -SPDX-FileCopyrightText = "2019 Tom Gainor " -SPDX-License-Identifier = "LicenseRef-Unsplash" --- a/tests/data/app/appinfo-attributes-once.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "id": "attributes_once", - "name": { - "@attributes": { - "lang": "en" - }, - "@value": "Attributes Once" - }, - "summary": { - "@attributes": { - "lang": "en" - }, - "@value": "Single occurrence with attributes set on allowed elements." - }, - "description": { - "@attributes": { - "lang": "en" - }, - "@value": "Fixture that sets attributes where allowed (e.g., lang, type, min-version, for)." - }, - "version": "1.2.3", - "licence": "agpl", - "author": { - "@attributes": { - "homepage": "http://example.com", - "mail": "jane@example.com" - }, - "@value": "Jane Doe" - }, - "types": [ - "filesystem" - ], - "documentation": { - "user": "https://example.test/attributes-once/user" - }, - "category": [ - "tools" - ], - "website": "https://example.test/attributes-once", - "bugs": "https://example.com/issues", - "repository": { - "@attributes": { - "type": "git" - }, - "@value": "https://example.test/attributes-once.git" - }, - "screenshot": { - "@attributes": { - "small-thumbnail": "https://example.test/attributes-once-small.png" - }, - "@value": "https://example.test/attributes-once.png" - }, - "dependencies": { - "php": { - "@attributes": { - "min-version": "8.2" - } - }, - "database": { - "@attributes": { - "min-version": "2.0" - }, - "@value": "pgsql" - }, - "lib": { - "@attributes": { - "min-version": "1.5" - }, - "@value": "curl" - }, - "owncloud": { - "@attributes": { - "min-version": "1.0", - "max-version": "2.0" - } - }, - "nextcloud": { - "@attributes": { - "min-version": "30.0", - "max-version": "31.0" - } - }, - "backend": [ - "caldav" - ] - }, - "background-jobs": { - "job": "OCA\\AttributesOnce\\BackgroundJob\\Job" - }, - "repair-steps": { - "install": { - "step": "OCA\\AttributesOnce\\RepairStep\\Install" - }, - "pre-migration": [], - "post-migration": [], - "live-migration": [], - "uninstall": [] - }, - "commands": { - "command": "OCA\\AttributesOnce\\Command\\Run" - }, - "settings": { - "admin": [ - "OCA\\AttributesOnce\\Settings\\Admin" - ], - "admin-section": [], - "personal": [], - "personal-section": [] - }, - "activity": { - "providers": { - "provider": "OCA\\AttributesOnce\\Activity\\Provider" - }, - "filters": [], - "settings": [] - }, - "navigations": { - "navigation": [ - { - "@attributes": { - "role": "admin" - }, - "name": "Attributes", - "route": "attributes.once.route", - "icon": "attributes-once.svg", - "order": "5" - } - ] - }, - "collaboration": { - "plugins": { - "@attributes": { - "type": "collaborator-search" - }, - "@value": "OCA\\AttributesOnce\\Collaboration\\Plugin" - } - }, - "sabre": { - "plugins": { - "plugin": "OCA\\AttributesOnce\\Sabre\\Plugin" - } - }, - "trash": { - "backend": { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\AttributesOnce\\Trash\\Backend" - } - }, - "versions": { - "backend": { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\AttributesOnce\\Versions\\Backend" - } - }, - "remote": [], - "public": [], - "two-factor-providers": [] -} --- a/tests/data/app/appinfo-attributes-once.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/app/appinfo-attributes-once.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - attributes_once - Attributes Once - Single occurrence with attributes set on allowed elements. - Fixture that sets attributes where allowed (e.g., lang, type, min-version, for). - 1.2.3 - agpl - Jane Doe - - - - - https://example.test/attributes-once/user - - tools - https://example.test/attributes-once - https://example.com/issues - https://example.test/attributes-once.git - https://example.test/attributes-once.png - - - pgsql - curl - - - caldav - - - OCA\AttributesOnce\BackgroundJob\Job - - - - OCA\AttributesOnce\RepairStep\Install - - - - OCA\AttributesOnce\Command\Run - - - OCA\AttributesOnce\Settings\Admin - - - - OCA\AttributesOnce\Activity\Provider - - - - - Attributes - attributes.once.route - attributes-once.svg - 5 - - - - - OCA\AttributesOnce\Collaboration\Plugin - - - - - OCA\AttributesOnce\Sabre\Plugin - - - - OCA\AttributesOnce\Trash\Backend - - - OCA\AttributesOnce\Versions\Backend - - --- a/tests/data/app/appinfo-multi-once.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "id": "multi_once", - "name": "Multi Once", - "summary": "Every repeatable element is used exactly once.", - "description": "Fixture that exercises the single-item normalization path.", - "version": "1.0.0", - "licence": "agpl", - "author": [ - "Jane Doe" - ], - "types": [ - "filesystem", - "logging" - ], - "documentation": { - "user": "https://example.test/multi-once/user", - "admin": "https://example.test/multi-once/admin", - "developer": "https://example.test/multi-once/developer" - }, - "category": [ - "monitoring" - ], - "website": "https://example.test/multi-once", - "discussion": "https://example.test/multi-once/discussion", - "bugs": "https://example.test/multi-once/issues", - "repository": "https://example.test/multi-once.git", - "screenshot": [ - "https://example.test/multi-once.png" - ], - "donation": "https://example.test/donate", - "dependencies": { - "database": "sqlite", - "command": "awk", - "lib": { - "@attributes": { - "min-version": "1.0" - }, - "@value": "curl" - }, - "nextcloud": { - "@attributes": { - "min-version": "30.0", - "max-version": "31.0" - } - }, - "architecture": "x86_64", - "backend": [ - "caldav" - ] - }, - "background-jobs": { - "job": "OCA\\MultiOnce\\BackgroundJob\\Cleanup" - }, - "repair-steps": { - "pre-migration": { - "step": "OCA\\MultiOnce\\RepairStep\\PreMigration" - }, - "post-migration": { - "step": "OCA\\MultiOnce\\RepairStep\\PostMigration" - }, - "live-migration": { - "step": "OCA\\MultiOnce\\RepairStep\\LiveMigration" - }, - "install": { - "step": "OCA\\MultiOnce\\RepairStep\\Install" - }, - "uninstall": { - "step": "OCA\\MultiOnce\\RepairStep\\Uninstall" - } - }, - "two-factor-providers": { - "provider": "OCA\\MultiOnce\\TwoFactor\\Provider" - }, - "commands": { - "command": "OCA\\MultiOnce\\Command\\Migrate" - }, - "settings": { - "admin": [ - "OCA\\MultiOnce\\Settings\\Admin" - ], - "admin-section": [ - "OCA\\MultiOnce\\Settings\\AdminSection" - ], - "personal": [ - "OCA\\MultiOnce\\Settings\\Personal" - ], - "personal-section": [ - "OCA\\MultiOnce\\Settings\\PersonalSection" - ], - "admin-delegation": [ - "OCA\\MultiOnce\\Settings\\AdminDelegation" - ], - "admin-delegation-section": [ - "OCA\\MultiOnce\\Settings\\AdminDelegationSection" - ] - }, - "activity": { - "settings": { - "setting": "OCA\\MultiOnce\\Activity\\Setting" - }, - "filters": { - "filter": "OCA\\MultiOnce\\Activity\\Filter" - }, - "providers": { - "provider": "OCA\\MultiOnce\\Activity\\Provider" - } - }, - "dashboard": { - "widget": "OCA\\MultiOnce\\Dashboard\\Widget" - }, - "fulltextsearch": { - "platform": "OCA\\MultiOnce\\Search\\Platform", - "provider": "OCA\\MultiOnce\\Search\\Provider" - }, - "navigations": { - "navigation": [ - { - "name": "Multi Once", - "route": "multi.once.route", - "icon": "multi-once.svg", - "order": "1" - } - ] - }, - "contactsmenu": { - "provider": "OCA\\MultiOnce\\ContactsMenu\\Provider" - }, - "collaboration": { - "plugins": { - "@attributes": { - "type": "collaborator-search" - }, - "@value": "OCA\\MultiOnce\\Collaboration\\Plugin" - } - }, - "openmetrics": { - "exporter": [ - "OCA\\MultiOnce\\OpenMetrics\\Exporter" - ] - }, - "sabre": { - "collections": { - "collection": "OCA\\MultiOnce\\Sabre\\Collection" - }, - "plugins": { - "plugin": "OCA\\MultiOnce\\Sabre\\Plugin" - }, - "address-book-plugins": { - "plugin": "OCA\\MultiOnce\\Sabre\\AddressBookPlugin" - }, - "calendar-plugins": { - "plugin": "OCA\\MultiOnce\\Sabre\\CalendarPlugin" - } - }, - "trash": { - "backend": { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiOnce\\Trash\\Backend" - } - }, - "versions": { - "backend": { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiOnce\\Versions\\Backend" - } - }, - "external-app": { - "docker-install": { - "registry": "registry.example.test", - "image": "multi-once", - "image-tag": "1.0.0" - }, - "scopes": { - "value": "scope-one" - }, - "system": "true", - "environment-variables": { - "variable": { - "name": "MULTI_ONCE_ONE", - "display-name": "Multi Once One", - "description": "First variable", - "default": "one" - } - } - }, - "remote": [], - "public": [] -} --- a/tests/data/app/appinfo-multi-once.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/tests/data/app/appinfo-multi-once.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - multi_once - Multi Once - Every repeatable element is used exactly once. - Fixture that exercises the single-item normalization path. - 1.0.0 - agpl - Jane Doe - - - - - - https://example.test/multi-once/user - https://example.test/multi-once/admin - https://example.test/multi-once/developer - - monitoring - https://example.test/multi-once - https://example.test/multi-once/discussion - https://example.test/multi-once/issues - https://example.test/multi-once.git - https://example.test/multi-once.png - https://example.test/donate - - sqlite - awk - curl - - x86_64 - caldav - - - OCA\MultiOnce\BackgroundJob\Cleanup - - - - OCA\MultiOnce\RepairStep\PreMigration - - - OCA\MultiOnce\RepairStep\PostMigration - - - OCA\MultiOnce\RepairStep\LiveMigration - - - OCA\MultiOnce\RepairStep\Install - - - OCA\MultiOnce\RepairStep\Uninstall - - - - OCA\MultiOnce\TwoFactor\Provider - - - OCA\MultiOnce\Command\Migrate - - - OCA\MultiOnce\Settings\Admin - OCA\MultiOnce\Settings\AdminSection - OCA\MultiOnce\Settings\Personal - OCA\MultiOnce\Settings\PersonalSection - OCA\MultiOnce\Settings\AdminDelegation - OCA\MultiOnce\Settings\AdminDelegationSection - - - - OCA\MultiOnce\Activity\Setting - - - OCA\MultiOnce\Activity\Filter - - - OCA\MultiOnce\Activity\Provider - - - - OCA\MultiOnce\Dashboard\Widget - - - OCA\MultiOnce\Search\Platform - OCA\MultiOnce\Search\Provider - - - - Multi Once - multi.once.route - multi-once.svg - 1 - - - - OCA\MultiOnce\ContactsMenu\Provider - - - - OCA\MultiOnce\Collaboration\Plugin - - - - OCA\MultiOnce\OpenMetrics\Exporter - - - - OCA\MultiOnce\Sabre\Collection - - - OCA\MultiOnce\Sabre\Plugin - - - OCA\MultiOnce\Sabre\AddressBookPlugin - - - OCA\MultiOnce\Sabre\CalendarPlugin - - - - OCA\MultiOnce\Trash\Backend - - - OCA\MultiOnce\Versions\Backend - - - - registry.example.test - multi-once - 1.0.0 - - - scope-one - - true - - - MULTI_ONCE_ONE - Multi Once One - First variable - one - - - - \ No newline at end of file --- a/tests/data/app/appinfo-multi-twice.json +++ /dev/null @@ -1,325 +0,0 @@ -{ - "id": "multi_twice", - "name": "Multi Twice", - "summary": "Every repeatable element is used exactly twice.", - "description": "Fixture that exercises the list normalization path.", - "version": "1.0.0", - "licence": [ - "agpl", - "mit" - ], - "author": [ - "Jane Doe", - "John Doe" - ], - "types": [ - "filesystem", - "logging" - ], - "documentation": { - "user": "https://example.test/multi-twice/user", - "admin": "https://example.test/multi-twice/admin", - "developer": "https://example.test/multi-twice/developer" - }, - "category": [ - "monitoring", - "social" - ], - "website": "https://example.test/multi-twice", - "discussion": "https://example.test/multi-twice/discussion", - "bugs": "https://example.test/multi-twice/issues", - "repository": { - "@attributes": { - "type": "git" - }, - "@value": "https://example.test/multi-twice.git" - }, - "screenshot": [ - "https://example.test/multi-twice-1.png", - "https://example.test/multi-twice-2.png" - ], - "donation": [ - "https://example.test/donate/1", - "https://example.test/donate/2" - ], - "dependencies": { - "php": { - "@attributes": { - "min-version": "8.2" - } - }, - "database": [ - { - "@attributes": { - "min-version": "1.0" - }, - "@value": "sqlite" - }, - { - "@attributes": { - "min-version": "1.0" - }, - "@value": "mysql" - } - ], - "command": [ - "awk", - "grep" - ], - "lib": [ - { - "@attributes": { - "min-version": "1.0" - }, - "@value": "curl" - }, - { - "@attributes": { - "min-version": "1.0" - }, - "@value": "intl" - } - ], - "owncloud": { - "@attributes": { - "min-version": "1.0", - "max-version": "2.0" - } - }, - "nextcloud": { - "@attributes": { - "min-version": "30.0", - "max-version": "31.0" - } - }, - "architecture": [ - "x86_64", - "aarch64" - ], - "backend": [ - "caldav", - "caldav" - ] - }, - "background-jobs": [ - "OCA\\MultiTwice\\BackgroundJob\\CleanupOne", - "OCA\\MultiTwice\\BackgroundJob\\CleanupTwo" - ], - "repair-steps": { - "pre-migration": [ - "OCA\\MultiTwice\\RepairStep\\PreMigrationOne", - "OCA\\MultiTwice\\RepairStep\\PreMigrationTwo" - ], - "post-migration": [ - "OCA\\MultiTwice\\RepairStep\\PostMigrationOne", - "OCA\\MultiTwice\\RepairStep\\PostMigrationTwo" - ], - "live-migration": [ - "OCA\\MultiTwice\\RepairStep\\LiveMigrationOne", - "OCA\\MultiTwice\\RepairStep\\LiveMigrationTwo" - ], - "install": [ - "OCA\\MultiTwice\\RepairStep\\InstallOne", - "OCA\\MultiTwice\\RepairStep\\InstallTwo" - ], - "uninstall": [ - "OCA\\MultiTwice\\RepairStep\\UninstallOne", - "OCA\\MultiTwice\\RepairStep\\UninstallTwo" - ] - }, - "two-factor-providers": [ - "OCA\\MultiTwice\\TwoFactor\\ProviderOne", - "OCA\\MultiTwice\\TwoFactor\\ProviderTwo" - ], - "commands": [ - "OCA\\MultiTwice\\Command\\MigrateOne", - "OCA\\MultiTwice\\Command\\MigrateTwo" - ], - "settings": { - "admin": [ - "OCA\\MultiTwice\\Settings\\AdminOne", - "OCA\\MultiTwice\\Settings\\AdminTwo" - ], - "admin-section": [ - "OCA\\MultiTwice\\Settings\\AdminSectionOne", - "OCA\\MultiTwice\\Settings\\AdminSectionTwo" - ], - "personal": [ - "OCA\\MultiTwice\\Settings\\PersonalOne", - "OCA\\MultiTwice\\Settings\\PersonalTwo" - ], - "personal-section": [ - "OCA\\MultiTwice\\Settings\\PersonalSectionOne", - "OCA\\MultiTwice\\Settings\\PersonalSectionTwo" - ], - "admin-delegation": [ - "OCA\\MultiTwice\\Settings\\AdminDelegationOne", - "OCA\\MultiTwice\\Settings\\AdminDelegationTwo" - ], - "admin-delegation-section": [ - "OCA\\MultiTwice\\Settings\\AdminDelegationSectionOne", - "OCA\\MultiTwice\\Settings\\AdminDelegationSectionTwo" - ] - }, - "activity": { - "settings": [ - "OCA\\MultiTwice\\Activity\\SettingOne", - "OCA\\MultiTwice\\Activity\\SettingTwo" - ], - "filters": [ - "OCA\\MultiTwice\\Activity\\FilterOne", - "OCA\\MultiTwice\\Activity\\FilterTwo" - ], - "providers": [ - "OCA\\MultiTwice\\Activity\\ProviderOne", - "OCA\\MultiTwice\\Activity\\ProviderTwo" - ] - }, - "dashboard": { - "widget": [ - "OCA\\MultiTwice\\Dashboard\\WidgetOne", - "OCA\\MultiTwice\\Dashboard\\WidgetTwo" - ] - }, - "fulltextsearch": { - "platform": [ - "OCA\\MultiTwice\\Search\\PlatformOne", - "OCA\\MultiTwice\\Search\\PlatformTwo" - ], - "provider": [ - "OCA\\MultiTwice\\Search\\ProviderOne", - "OCA\\MultiTwice\\Search\\ProviderTwo" - ] - }, - "navigations": { - "navigation": [ - { - "name": "Multi Twice One", - "route": "multi.twice.one", - "icon": "multi-twice-1.svg", - "order": "1" - }, - { - "name": "Multi Twice Two", - "route": "multi.twice.two", - "icon": "multi-twice-2.svg", - "order": "2" - } - ] - }, - "contactsmenu": { - "provider": "OCA\\MultiTwice\\ContactsMenu\\Provider" - }, - "collaboration": { - "plugins": [ - { - "@attributes": { - "type": "collaborator-search" - }, - "@value": "OCA\\MultiTwice\\Collaboration\\PluginOne" - }, - { - "@attributes": { - "type": "autocomplete-sort" - }, - "@value": "OCA\\MultiTwice\\Collaboration\\PluginTwo" - } - ] - }, - "openmetrics": { - "exporter": [ - "OCA\\MultiTwice\\OpenMetrics\\ExporterOne", - "OCA\\MultiTwice\\OpenMetrics\\ExporterTwo" - ] - }, - "sabre": { - "collections": { - "collection": [ - "OCA\\MultiTwice\\Sabre\\CollectionOne", - "OCA\\MultiTwice\\Sabre\\CollectionTwo" - ] - }, - "plugins": { - "plugin": [ - "OCA\\MultiTwice\\Sabre\\PluginOne", - "OCA\\MultiTwice\\Sabre\\PluginTwo" - ] - }, - "address-book-plugins": { - "plugin": [ - "OCA\\MultiTwice\\Sabre\\AddressBookPluginOne", - "OCA\\MultiTwice\\Sabre\\AddressBookPluginTwo" - ] - }, - "calendar-plugins": { - "plugin": [ - "OCA\\MultiTwice\\Sabre\\CalendarPluginOne", - "OCA\\MultiTwice\\Sabre\\CalendarPluginTwo" - ] - } - }, - "trash": { - "backend": [ - { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiTwice\\Trash\\BackendOne" - }, - { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiTwice\\Trash\\BackendTwo" - } - ] - }, - "versions": { - "backend": [ - { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiTwice\\Versions\\BackendOne" - }, - { - "@attributes": { - "for": "files" - }, - "@value": "OCA\\MultiTwice\\Versions\\BackendTwo" - } - ] - }, - "external-app": { - "docker-install": { - "registry": "registry.example.test", - "image": "multi-twice", - "image-tag": "2.0.0" - }, - "scopes": { - "value": [ - "scope-one", - "scope-two" - ] - }, - "system": "true", - "environment-variables": { - "variable": [ - { - "name": "MULTI_TWICE_ONE", - "display-name": "Multi Twice One", - "description": "First variable", - "default": "one" - }, - { - "name": "MULTI_TWICE_TWO", - "display-name": "Multi Twice Two", - "description": "Second variable", - "default": "two" - } - ] - } - }, - "remote": [], - "public": [] -} --- a/tests/data/app/appinfo-multi-twice.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file --- a/tests/data/app/appinfo-multi-twice.xml +++ /dev/null @@ -1,202 +0,0 @@ - - - - multi_twice - Multi Twice - Every repeatable element is used exactly twice. - Fixture that exercises the list normalization path. - 1.0.0 - agpl - mit - Jane Doe - John Doe - - - - - - https://example.test/multi-twice/user - https://example.test/multi-twice/admin - https://example.test/multi-twice/developer - - monitoring - social - https://example.test/multi-twice - https://example.test/multi-twice/discussion - https://example.test/multi-twice/issues - https://example.test/multi-twice.git - https://example.test/multi-twice-1.png - https://example.test/multi-twice-2.png - https://example.test/donate/1 - https://example.test/donate/2 - - - sqlite - mysql - awk - grep - curl - intl - - - x86_64 - aarch64 - caldav - caldav - - - OCA\MultiTwice\BackgroundJob\CleanupOne - OCA\MultiTwice\BackgroundJob\CleanupTwo - - - - OCA\MultiTwice\RepairStep\PreMigrationOne - OCA\MultiTwice\RepairStep\PreMigrationTwo - - - OCA\MultiTwice\RepairStep\PostMigrationOne - OCA\MultiTwice\RepairStep\PostMigrationTwo - - - OCA\MultiTwice\RepairStep\LiveMigrationOne - OCA\MultiTwice\RepairStep\LiveMigrationTwo - - - OCA\MultiTwice\RepairStep\InstallOne - OCA\MultiTwice\RepairStep\InstallTwo - - - OCA\MultiTwice\RepairStep\UninstallOne - OCA\MultiTwice\RepairStep\UninstallTwo - - - - OCA\MultiTwice\TwoFactor\ProviderOne - OCA\MultiTwice\TwoFactor\ProviderTwo - - - OCA\MultiTwice\Command\MigrateOne - OCA\MultiTwice\Command\MigrateTwo - - - OCA\MultiTwice\Settings\AdminOne - OCA\MultiTwice\Settings\AdminTwo - OCA\MultiTwice\Settings\AdminSectionOne - OCA\MultiTwice\Settings\AdminSectionTwo - OCA\MultiTwice\Settings\PersonalOne - OCA\MultiTwice\Settings\PersonalTwo - OCA\MultiTwice\Settings\PersonalSectionOne - OCA\MultiTwice\Settings\PersonalSectionTwo - OCA\MultiTwice\Settings\AdminDelegationOne - OCA\MultiTwice\Settings\AdminDelegationTwo - OCA\MultiTwice\Settings\AdminDelegationSectionOne - OCA\MultiTwice\Settings\AdminDelegationSectionTwo - - - - OCA\MultiTwice\Activity\SettingOne - OCA\MultiTwice\Activity\SettingTwo - - - OCA\MultiTwice\Activity\FilterOne - OCA\MultiTwice\Activity\FilterTwo - - - OCA\MultiTwice\Activity\ProviderOne - OCA\MultiTwice\Activity\ProviderTwo - - - - OCA\MultiTwice\Dashboard\WidgetOne - OCA\MultiTwice\Dashboard\WidgetTwo - - - OCA\MultiTwice\Search\PlatformOne - OCA\MultiTwice\Search\PlatformTwo - OCA\MultiTwice\Search\ProviderOne - OCA\MultiTwice\Search\ProviderTwo - - - - Multi Twice One - multi.twice.one - multi-twice-1.svg - 1 - - - Multi Twice Two - multi.twice.two - multi-twice-2.svg - 2 - - - - OCA\MultiTwice\ContactsMenu\Provider - - - - OCA\MultiTwice\Collaboration\PluginOne - OCA\MultiTwice\Collaboration\PluginTwo - - - - OCA\MultiTwice\OpenMetrics\ExporterOne - OCA\MultiTwice\OpenMetrics\ExporterTwo - - - - OCA\MultiTwice\Sabre\CollectionOne - OCA\MultiTwice\Sabre\CollectionTwo - - - OCA\MultiTwice\Sabre\PluginOne - OCA\MultiTwice\Sabre\PluginTwo - - - OCA\MultiTwice\Sabre\AddressBookPluginOne - OCA\MultiTwice\Sabre\AddressBookPluginTwo - - - OCA\MultiTwice\Sabre\CalendarPluginOne - OCA\MultiTwice\Sabre\CalendarPluginTwo - - - - OCA\MultiTwice\Trash\BackendOne - OCA\MultiTwice\Trash\BackendTwo - - - OCA\MultiTwice\Versions\BackendOne - OCA\MultiTwice\Versions\BackendTwo - - - - registry.example.test - multi-twice - 2.0.0 - - - scope-one - scope-two - - true - - - MULTI_TWICE_ONE - Multi Twice One - First variable - one - - - MULTI_TWICE_TWO - Multi Twice Two - Second variable - two - - - - \ No newline at end of file --- a/tests/data/app/expected-info.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "remote": [], - "public": [], - "id": "files_encryption", - "name": "Server-side Encryption", - "description": "\n\tThis application encrypts all files accessed by ownCloud at rest, wherever they are stored. As an example, with this application enabled, external cloud based Amazon S3 storage will be encrypted, protecting this data on storage outside of the control of the Admin. When this application is enabled for the first time, all files are encrypted as users log in and are prompted for their password. The recommended recovery key option enables recovery of files in case the key is lost. \n\tNote that this app encrypts all files that are touched by ownCloud, so external storage providers and applications such as SharePoint will see new files encrypted when they are accessed. Encryption is based on AES 128 or 256 bit keys. More information is available in the Encryption documentation \n\t", - "licence": "AGPL", - "author": [ - "Sam Tuke, Bjoern Schiessle, Florin Peter" - ], - "requiremin": "4", - "shipped": "true", - "documentation": { - "user": "user-encryption", - "admin": "admin-encryption" - }, - "types": ["filesystem"], - "dependencies": { - "php": { - "@attributes" : { - "min-version": "5.4", - "max-version": "5.5" - } - }, - "database": [ - { - "@attributes" : { - "min-version": "3.0" - }, - "@value": "sqlite"}, - "mysql" - ], - "command": [ - { - "@attributes" : { - "os": "linux" - }, - "@value": "grep" - }, - { - "@attributes" : { - "os": "windows" - }, - "@value": "notepad.exe" - } - ], - "lib": [ - { - "@attributes" : { - "min-version": "1.2" - }, - "@value": "xml" - }, - { - "@attributes" : { - "max-version": "2.0" - }, - "@value": "intl" - }, - "curl" - ], - "os": "Linux", - "owncloud": { - "@attributes" : { - "min-version": "7.0.1", - "max-version": "8" - } - }, - "backend": [ - "caldav" - ] - }, - "repair-steps": { - "install": [], - "pre-migration": [], - "post-migration": [], - "live-migration": [], - "uninstall": [] - }, - "background-jobs": [], - "two-factor-providers": [], - "commands": [], - "activity": { - "filters": [], - "settings": [], - "providers": [] - }, - "settings": { - "admin": [], - "admin-section": [], - "personal": [], - "personal-section": [] - }, - "openmetrics": { - "exporter": [ - "OC\\OpenMetrics\\Exporters\\ActiveSessions", - "OC\\OpenMetrics\\Exporters\\ActiveUsers" - ] - } -} --- a/tests/data/app/expected-info.json.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors -SPDX-FileCopyrightText: 2016 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/app/invalid-info.xml +++ /dev/null @@ -1,25 +0,0 @@ - - -files_encryption - Server-side Encryption - - This application encrypts all files accessed by ownCloud at rest, wherever they are stored. As an example, with this application enabled, external cloud based Amazon S3 storage will be encrypted, protecting this data on storage outside of the control of the Admin. When this application is enabled for the first time, all files are encrypted as users log in and are prompted for their password. The recommended recovery key option enables recovery of files in case the key is lost. - Note that this app encrypts all files that are touched by ownCloud, so external storage providers and applications such as SharePoint will see new files encrypted when they are accessed. Encryption is based on AES 128 or 256 bit keys. More information is available in the Encryption documentation - - AGPL - Sam Tuke, Bjoern Schiessle, Florin Peter - 4 - true - - user-encryption - admin-encryption - - - - - --- a/tests/data/app/navigation-one-item.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "id": "activity", - "name": "Activity", - "summary": "This application enables users to view actions related to their files in Nextcloud.", - "description": "\n\t\tThis application enables users to view actions related to their files in Nextcloud.\n\t\tOnce enabled, users will see a new icon \u201cActivity\u201d in their apps menu.\n\t\tWhen clicked, a new page appears for users to track the activity related to files \u2013 from new files, to deleted files, move, rename, updates and shared activity.\n\t\tThe user can configure their individual activity settings in their personal menu.\n\t\tThis sets the type of activity to record, as well as whether to the user sees their own activities,\n\t\twhether these are only available online, and whether they get an email digest on a regular basis.\n\t\tMore information is available in the Activity documentation.\n\t", - "version": "2.9.0", - "licence": "agpl", - "author": [ - "Frank Karlitschek", - "Joas Schilling" - ], - "default_enable": "", - "types": [ - "filesystem" - ], - "documentation": { - "admin": "https:\/\/docs.nextcloud.org\/server\/14\/admin_manual\/configuration_server\/activity_configuration.html" - }, - "category": [ - "monitoring", - "social" - ], - "website": "https:\/\/github.com\/nextcloud\/activity\/", - "bugs": "https:\/\/github.com\/nextcloud\/activity\/issues", - "repository": "https:\/\/github.com\/nextcloud\/activity.git", - "dependencies": { - "nextcloud": { - "@attributes": { - "min-version": "16", - "max-version": "16" - } - }, - "backend": [] - }, - "background-jobs": [ - "OCA\\Activity\\BackgroundJob\\EmailNotification", - "OCA\\Activity\\BackgroundJob\\ExpireActivities" - ], - "commands": { - "command": "OCA\\Activity\\Command\\SendEmails" - }, - "settings": { - "admin": [ - "OCA\\Activity\\Settings\\Admin" - ], - "admin-section": [ - "OCA\\Activity\\Settings\\AdminSection" - ], - "personal": [ - "OCA\\Activity\\Settings\\Personal" - ], - "personal-section": [ - "OCA\\Activity\\Settings\\PersonalSection" - ] - }, - "activity": { - "filters": [ - "OCA\\Activity\\Filter\\AllFilter", - "OCA\\Activity\\Filter\\SelfFilter", - "OCA\\Activity\\Filter\\ByFilter" - ], - "settings": [], - "providers": [] - }, - "navigations": { - "navigation": [ - { - "name": "Activity", - "route": "activity.Activities.showList", - "icon": "activity.svg", - "order": "1" - } - ] - }, - "remote": [], - "public": [], - "repair-steps": { - "install": [], - "pre-migration": [], - "post-migration": [], - "live-migration": [], - "uninstall": [] - }, - "two-factor-providers": [] -} --- a/tests/data/app/navigation-one-item.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/app/navigation-one-item.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - activity - Activity - This application enables users to view actions related to their files in Nextcloud. - - This application enables users to view actions related to their files in Nextcloud. - Once enabled, users will see a new icon “Activity” in their apps menu. - When clicked, a new page appears for users to track the activity related to files – from new files, to deleted files, move, rename, updates and shared activity. - The user can configure their individual activity settings in their personal menu. - This sets the type of activity to record, as well as whether to the user sees their own activities, - whether these are only available online, and whether they get an email digest on a regular basis. - More information is available in the Activity documentation. - - - 2.9.0 - agpl - Frank Karlitschek - Joas Schilling - - - - - - - - https://docs.nextcloud.org/server/14/admin_manual/configuration_server/activity_configuration.html - - - monitoring - social - - https://github.com/nextcloud/activity/ - https://github.com/nextcloud/activity/issues - https://github.com/nextcloud/activity.git - - - - - - - OCA\Activity\BackgroundJob\EmailNotification - OCA\Activity\BackgroundJob\ExpireActivities - - - - OCA\Activity\Command\SendEmails - - - - OCA\Activity\Settings\Admin - OCA\Activity\Settings\AdminSection - OCA\Activity\Settings\Personal - OCA\Activity\Settings\PersonalSection - - - - - OCA\Activity\Filter\AllFilter - OCA\Activity\Filter\SelfFilter - OCA\Activity\Filter\ByFilter - - - - - - Activity - activity.Activities.showList - activity.svg - 1 - - - --- a/tests/data/app/navigation-two-items.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "id": "activity", - "name": "Activity", - "summary": "This application enables users to view actions related to their files in Nextcloud.", - "description": "\n\t\tThis application enables users to view actions related to their files in Nextcloud.\n\t\tOnce enabled, users will see a new icon \u201cActivity\u201d in their apps menu.\n\t\tWhen clicked, a new page appears for users to track the activity related to files \u2013 from new files, to deleted files, move, rename, updates and shared activity.\n\t\tThe user can configure their individual activity settings in their personal menu.\n\t\tThis sets the type of activity to record, as well as whether to the user sees their own activities,\n\t\twhether these are only available online, and whether they get an email digest on a regular basis.\n\t\tMore information is available in the Activity documentation.\n\t", - "version": "2.9.0", - "licence": "agpl", - "author": [ - "Frank Karlitschek", - "Joas Schilling" - ], - "default_enable": "", - "types": [ - "filesystem" - ], - "documentation": { - "admin": "https:\/\/docs.nextcloud.org\/server\/14\/admin_manual\/configuration_server\/activity_configuration.html" - }, - "category": [ - "monitoring", - "social" - ], - "website": "https:\/\/github.com\/nextcloud\/activity\/", - "bugs": "https:\/\/github.com\/nextcloud\/activity\/issues", - "repository": "https:\/\/github.com\/nextcloud\/activity.git", - "dependencies": { - "nextcloud": { - "@attributes": { - "min-version": "16", - "max-version": "16" - } - }, - "backend": [] - }, - "background-jobs": [ - "OCA\\Activity\\BackgroundJob\\EmailNotification", - "OCA\\Activity\\BackgroundJob\\ExpireActivities" - ], - "commands": { - "command": "OCA\\Activity\\Command\\SendEmails" - }, - "settings": { - "admin": [ - "OCA\\Activity\\Settings\\Admin" - ], - "admin-section": [ - "OCA\\Activity\\Settings\\AdminSection" - ], - "personal": [ - "OCA\\Activity\\Settings\\Personal" - ], - "personal-section": [ - "OCA\\Activity\\Settings\\PersonalSection" - ] - }, - "activity": { - "filters": [ - "OCA\\Activity\\Filter\\AllFilter", - "OCA\\Activity\\Filter\\SelfFilter", - "OCA\\Activity\\Filter\\ByFilter" - ], - "settings": [], - "providers": [] - }, - "navigations": { - "navigation": [ - { - "name": "Activity", - "route": "activity.Activities.showList", - "icon": "activity.svg", - "order": "1" - }, - { - "name": "Activity-Test", - "route": "activity.Activities.showList", - "icon": "activity.svg", - "order": "2" - } - ] - }, - "remote": [], - "public": [], - "repair-steps": { - "install": [], - "pre-migration": [], - "post-migration": [], - "live-migration": [], - "uninstall": [] - }, - "two-factor-providers": [] -} --- a/tests/data/app/navigation-two-items.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/app/navigation-two-items.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - activity - Activity - This application enables users to view actions related to their files in Nextcloud. - - This application enables users to view actions related to their files in Nextcloud. - Once enabled, users will see a new icon “Activity” in their apps menu. - When clicked, a new page appears for users to track the activity related to files – from new files, to deleted files, move, rename, updates and shared activity. - The user can configure their individual activity settings in their personal menu. - This sets the type of activity to record, as well as whether to the user sees their own activities, - whether these are only available online, and whether they get an email digest on a regular basis. - More information is available in the Activity documentation. - - - 2.9.0 - agpl - Frank Karlitschek - Joas Schilling - - - - - - - - https://docs.nextcloud.org/server/14/admin_manual/configuration_server/activity_configuration.html - - - monitoring - social - - https://github.com/nextcloud/activity/ - https://github.com/nextcloud/activity/issues - https://github.com/nextcloud/activity.git - - - - - - - OCA\Activity\BackgroundJob\EmailNotification - OCA\Activity\BackgroundJob\ExpireActivities - - - - OCA\Activity\Command\SendEmails - - - - OCA\Activity\Settings\Admin - OCA\Activity\Settings\AdminSection - OCA\Activity\Settings\Personal - OCA\Activity\Settings\PersonalSection - - - - - OCA\Activity\Filter\AllFilter - OCA\Activity\Filter\SelfFilter - OCA\Activity\Filter\ByFilter - - - - - - Activity - activity.Activities.showList - activity.svg - 1 - - - Activity-Test - activity.Activities.showList - activity.svg - 2 - - - --- a/tests/data/app/valid-info.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - files_encryption - Server-side Encryption - - This application encrypts all files accessed by ownCloud at rest, wherever they are stored. As an example, with this application enabled, external cloud based Amazon S3 storage will be encrypted, protecting this data on storage outside of the control of the Admin. When this application is enabled for the first time, all files are encrypted as users log in and are prompted for their password. The recommended recovery key option enables recovery of files in case the key is lost. - Note that this app encrypts all files that are touched by ownCloud, so external storage providers and applications such as SharePoint will see new files encrypted when they are accessed. Encryption is based on AES 128 or 256 bit keys. More information is available in the Encryption documentation - - AGPL - Sam Tuke, Bjoern Schiessle, Florin Peter - 4 - true - - user-encryption - admin-encryption - - - - - - - sqlite - mysql - grep - notepad.exe - xml - intl - curl - Linux - - caldav - - - - OC\OpenMetrics\Exporters\ActiveSessions - OC\OpenMetrics\Exporters\ActiveUsers - - --- a/tests/data/app/various-single-item.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "id": "notifications", - "name": "Notifications", - "description": "A single screenshot should be an array", - "version": "1.0.0", - "licence": "agpl", - "author": [ - "Joas Schilling" - ], - "dependencies": { - "nextcloud": { - "@attributes": { - "min-version": "16", - "max-version": "16" - } - }, - "backend": [] - }, - "screenshot": [ - "https://raw.githubusercontent.com/nextcloud/notifications/refs/heads/master/docs/screenshot.png" - ], - "category": [ - "monitoring" - ], - "background-jobs": [], - "activity": { - "filters": [], - "settings": [], - "providers": [] - }, - "commands": [], - "remote": [], - "public": [], - "repair-steps": { - "install": [], - "pre-migration": [], - "post-migration": [], - "live-migration": [], - "uninstall": [] - }, - "settings": { - "admin": [], - "admin-section": [], - "personal": [], - "personal-section": [] - }, - "two-factor-providers": [], - "types": [], - "openmetrics": { - "exporter": [ - "OC\\OpenMetrics\\Exporters\\ActiveUsers" - ] - } -} --- a/tests/data/app/various-single-item.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/app/various-single-item.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - notifications - Notifications - A single screenshot should be an array - 1.0.0 - agpl - Joas Schilling - - monitoring - - https://raw.githubusercontent.com/nextcloud/notifications/refs/heads/master/docs/screenshot.png - - - - - - - OC\OpenMetrics\Exporters\ActiveUsers - - --- a/tests/data/block-aligned-plus-one.txt +++ /dev/null @@ -1,25 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse id hendrerit felis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed dictum dolor nec lobortis sagittis. Aliquam blandit lobortis ullamcorper. Donec malesuada ante ante, vel cursus purus suscipit ut. Sed a eros vitae lorem tristique hendrerit a a odio. Cras interdum mi lectus, sed molestie mauris tempus sit amet. Praesent auctor neque venenatis diam tincidunt sodales. Maecenas tincidunt ligula et ipsum dignissim, et molestie dui hendrerit. Nunc lobortis mauris vel tempor tristique. Sed consectetur bibendum nunc. Pellentesque augue velit, interdum ac posuere eu, eleifend et massa. Vestibulum ac mi eu mi viverra iaculis. Sed consequat leo adipiscing dui varius, sagittis vestibulum massa vehicula. - -Praesent imperdiet erat vitae dui placerat, sit amet vulputate tortor viverra. In laoreet elit lorem, at luctus est tempor ac. Nam elementum scelerisque nisl vitae accumsan. Curabitur cursus rutrum est, ut adipiscing ante elementum ut. Ut lectus sem, luctus ut massa non, dignissim vestibulum ipsum. Suspendisse non convallis enim, aliquam adipiscing velit. Donec pharetra non turpis hendrerit porttitor. Aliquam erat volutpat. - -Cras eget arcu eu dolor faucibus accumsan non eget orci. Integer quis quam tincidunt, faucibus neque in, imperdiet lacus. Aenean dui turpis, lacinia quis ligula non, semper adipiscing tellus. Curabitur vitae dui quis nisl malesuada commodo. Nunc eleifend metus enim, eu aliquet dui semper sit amet. Sed a odio sapien. Suspendisse vitae ante id sapien semper accumsan. Sed vestibulum erat quis laoreet pellentesque. Nullam placerat ligula eu odio faucibus, eget dictum orci tristique. Quisque sit amet pulvinar velit, lacinia dictum sem. - -Phasellus id sagittis lacus. Vivamus facilisis convallis metus, sit amet ultricies purus gravida quis. Vivamus eget egestas arcu, a euismod risus. Fusce metus arcu, molestie ut lacinia at, commodo eu nibh. In leo tortor, feugiat aliquet semper in, malesuada ac nulla. Fusce tempor ultricies blandit. Pellentesque et lorem quam. Suspendisse eros eros, mattis ut porttitor vitae, fermentum eget augue. Aliquam vitae justo sed est dictum lobortis. Cras lacinia commodo ligula sed ornare. Donec ut eros semper, sodales tellus quis, cursus quam. Quisque elementum ullamcorper pellentesque. Integer in rutrum ante, vel condimentum metus. Aliquam erat volutpat. - -Quisque convallis tempus eros, sit amet mattis ipsum eleifend in. Mauris volutpat, urna ut commodo tempus, est quam scelerisque erat, eget consequat nulla arcu id eros. Nam elementum nibh quis tincidunt adipiscing. Fusce tristique pretium mollis. Nulla quis ornare felis, vel lacinia diam. In et dui et mauris vestibulum commodo. In hac habitasse platea dictumst. Vestibulum viverra pellentesque tortor, tempor vehicula orci. Praesent rutrum turpis ipsum, ac commodo nisi vehicula ac. - -Nulla gravida ultrices mauris. Ut congue purus nec dolor euismod, vitae auctor elit condimentum. Nullam elementum velit lectus, fringilla pharetra sem hendrerit sit amet. Cras quis lobortis nisi, id interdum massa. Proin eget porttitor sapien. Vivamus quam odio, consectetur at rhoncus vel, posuere in nisi. Donec hendrerit urna ac massa tristique, pellentesque consequat orci dapibus. Mauris mi erat, aliquet id adipiscing ultricies, ultrices nec lacus. Proin imperdiet elit magna, ut suscipit metus faucibus eget. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at leo lorem. Donec mi nibh, blandit ut mi quis, dictum adipiscing ante. Curabitur sodales blandit odio id viverra. Fusce sagittis rutrum nibh. Mauris risus lacus, placerat at dictum in, varius volutpat quam. - -Ut accumsan tincidunt dui, non semper lorem sodales nec. Curabitur pellentesque velit nec erat ultrices, in convallis diam suscipit. Curabitur pellentesque convallis mi. Phasellus iaculis orci quis nisi semper auctor. Vestibulum eleifend tortor quis odio feugiat, pulvinar congue turpis blandit. Sed viverra diam risus, vel mattis orci dignissim ut. Cras pellentesque dapibus dolor. Morbi tempor eleifend magna. Ut sodales ut sapien sed elementum. In hac habitasse platea dictumst. - -Nullam eget sagittis tellus. Donec a tellus id ligula viverra pellentesque. Pellentesque nulla turpis, pretium vel sem vel, placerat ultricies diam. Donec felis nibh, rhoncus id ornare at, lobortis vel massa. Nullam eu luctus ipsum. Mauris consequat dictum aliquam. Suspendisse ullamcorper aliquam mauris, viverra tincidunt augue suscipit at. Sed luctus scelerisque justo ut semper. Phasellus massa mauris, molestie at nulla eget, dictum ultricies nisi. Aliquam rhoncus nisl sed urna egestas, vel luctus lectus euismod. Mauris sagittis dapibus leo. Donec vitae dignissim risus, ac ultricies augue. Ut vulputate tortor sed dui consectetur placerat. Nullam viverra non diam vel dignissim. - -Suspendisse potenti. Integer facilisis neque vitae euismod adipiscing. Aenean dictum leo commodo dui sodales, sit amet volutpat mauris gravida. Proin tempus convallis eros at consectetur. Proin nisl purus, dictum vel euismod ut, imperdiet quis est. Etiam sollicitudin lobortis neque eget pulvinar. Etiam venenatis vel sem ut posuere. Aliquam consectetur rhoncus facilisis. Morbi a viverra orci. - -Praesent ut vehicula orci, vel convallis risus. Suspendisse consectetur varius interdum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin vel sodales enim. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur suscipit tristique justo vel dapibus. Sed tincidunt mattis massa a auctor. Donec in tincidunt elit. Curabitur interdum neque at ante fringilla tempus. In hac habitasse platea dictumst. Vivamus luctus ligula ut nisl fermentum egestas. Praesent pulvinar accumsan neque. Sed nec leo sit amet arcu vehicula vehicula non ac diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; - -Morbi vel arcu quis neque dictum ullamcorper. Pellentesque condimentum consequat lacinia. Vestibulum eleifend placerat erat, eu hendrerit dui pulvinar eget. Vestibulum accumsan, lectus id vehicula fringilla, erat eros dictum massa, ut tristique libero mi eu ante. In at ante nunc. Praesent sodales ullamcorper porta. Curabitur egestas odio elit. Praesent et libero malesuada, venenatis lorem vitae, eleifend nisl. Donec dapibus euismod turpis, nec porttitor turpis dapibus ac. Fusce iaculis lacus eget dictum aliquam. Sed dictum eu enim ac posuere. Ut accumsan, ipsum et laoreet consectetur, ipsum quam dapibus diam, ac molestie ligula dui id massa. Nulla aliquam mauris congue nibh vestibulum imperdiet. - -Aenean ultricies, orci vel consectetur suscipit, ante nisi eleifend est, vitae suscipit risus erat dictum dui. Ut nisi diam, tristique sed nisl nec, aliquam gravida orci. Vestibulum in molestie sem. Ut eu molestie mi. Suspendisse potenti. Nunc sagittis lorem ut est vehicula, vitae imperdiet leo rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec pharetra urna pretium, faucibus orci cursus, sodales purus. Duis iaculis dignissim augue, non aliquet ligula elementum quis. Sed tempus a nisi et faucibus. Donec mattis ligula ac sem aliquam, sed sodales est ullamcorper. Cras vel orci est. Integer eget ultricies nisi. - -Quisque rhoncus, nisl vel auctor consectetur, sapien augue iaculis urna, quis suscipit arcu dolor ac est. Fusce sit amet quam lacinia, malesuada lorem vitae, dapibus justo. Mauris eget massa nec dolor volutpat tempor ut eu ante. Donec risus mi, aliquam at euismod vulputate, ullamcorper non leo. Etiam ac elit velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus sollicitudin enim sed felis pharetra, a convallis mi venenatis. Donec convallis non velit venenatis suscipit. Sed massa risus, dictum quis aliquam ut, placerat quis arcu. Donec at nisi neque. Nullam porta et mi vel ultricies. Donec vel aliquam sem. Mauris varius, ipsum interdum mattis rhoncus, nunc nisl vehicula tortor, quis condimentum nibh nisl vel ante. Sed vel pretium dui. Lorem ipsum dolor sit amet, consectetur adipisX --- a/tests/data/block-aligned.txt +++ /dev/null @@ -1,25 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse id hendrerit felis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed dictum dolor nec lobortis sagittis. Aliquam blandit lobortis ullamcorper. Donec malesuada ante ante, vel cursus purus suscipit ut. Sed a eros vitae lorem tristique hendrerit a a odio. Cras interdum mi lectus, sed molestie mauris tempus sit amet. Praesent auctor neque venenatis diam tincidunt sodales. Maecenas tincidunt ligula et ipsum dignissim, et molestie dui hendrerit. Nunc lobortis mauris vel tempor tristique. Sed consectetur bibendum nunc. Pellentesque augue velit, interdum ac posuere eu, eleifend et massa. Vestibulum ac mi eu mi viverra iaculis. Sed consequat leo adipiscing dui varius, sagittis vestibulum massa vehicula. - -Praesent imperdiet erat vitae dui placerat, sit amet vulputate tortor viverra. In laoreet elit lorem, at luctus est tempor ac. Nam elementum scelerisque nisl vitae accumsan. Curabitur cursus rutrum est, ut adipiscing ante elementum ut. Ut lectus sem, luctus ut massa non, dignissim vestibulum ipsum. Suspendisse non convallis enim, aliquam adipiscing velit. Donec pharetra non turpis hendrerit porttitor. Aliquam erat volutpat. - -Cras eget arcu eu dolor faucibus accumsan non eget orci. Integer quis quam tincidunt, faucibus neque in, imperdiet lacus. Aenean dui turpis, lacinia quis ligula non, semper adipiscing tellus. Curabitur vitae dui quis nisl malesuada commodo. Nunc eleifend metus enim, eu aliquet dui semper sit amet. Sed a odio sapien. Suspendisse vitae ante id sapien semper accumsan. Sed vestibulum erat quis laoreet pellentesque. Nullam placerat ligula eu odio faucibus, eget dictum orci tristique. Quisque sit amet pulvinar velit, lacinia dictum sem. - -Phasellus id sagittis lacus. Vivamus facilisis convallis metus, sit amet ultricies purus gravida quis. Vivamus eget egestas arcu, a euismod risus. Fusce metus arcu, molestie ut lacinia at, commodo eu nibh. In leo tortor, feugiat aliquet semper in, malesuada ac nulla. Fusce tempor ultricies blandit. Pellentesque et lorem quam. Suspendisse eros eros, mattis ut porttitor vitae, fermentum eget augue. Aliquam vitae justo sed est dictum lobortis. Cras lacinia commodo ligula sed ornare. Donec ut eros semper, sodales tellus quis, cursus quam. Quisque elementum ullamcorper pellentesque. Integer in rutrum ante, vel condimentum metus. Aliquam erat volutpat. - -Quisque convallis tempus eros, sit amet mattis ipsum eleifend in. Mauris volutpat, urna ut commodo tempus, est quam scelerisque erat, eget consequat nulla arcu id eros. Nam elementum nibh quis tincidunt adipiscing. Fusce tristique pretium mollis. Nulla quis ornare felis, vel lacinia diam. In et dui et mauris vestibulum commodo. In hac habitasse platea dictumst. Vestibulum viverra pellentesque tortor, tempor vehicula orci. Praesent rutrum turpis ipsum, ac commodo nisi vehicula ac. - -Nulla gravida ultrices mauris. Ut congue purus nec dolor euismod, vitae auctor elit condimentum. Nullam elementum velit lectus, fringilla pharetra sem hendrerit sit amet. Cras quis lobortis nisi, id interdum massa. Proin eget porttitor sapien. Vivamus quam odio, consectetur at rhoncus vel, posuere in nisi. Donec hendrerit urna ac massa tristique, pellentesque consequat orci dapibus. Mauris mi erat, aliquet id adipiscing ultricies, ultrices nec lacus. Proin imperdiet elit magna, ut suscipit metus faucibus eget. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at leo lorem. Donec mi nibh, blandit ut mi quis, dictum adipiscing ante. Curabitur sodales blandit odio id viverra. Fusce sagittis rutrum nibh. Mauris risus lacus, placerat at dictum in, varius volutpat quam. - -Ut accumsan tincidunt dui, non semper lorem sodales nec. Curabitur pellentesque velit nec erat ultrices, in convallis diam suscipit. Curabitur pellentesque convallis mi. Phasellus iaculis orci quis nisi semper auctor. Vestibulum eleifend tortor quis odio feugiat, pulvinar congue turpis blandit. Sed viverra diam risus, vel mattis orci dignissim ut. Cras pellentesque dapibus dolor. Morbi tempor eleifend magna. Ut sodales ut sapien sed elementum. In hac habitasse platea dictumst. - -Nullam eget sagittis tellus. Donec a tellus id ligula viverra pellentesque. Pellentesque nulla turpis, pretium vel sem vel, placerat ultricies diam. Donec felis nibh, rhoncus id ornare at, lobortis vel massa. Nullam eu luctus ipsum. Mauris consequat dictum aliquam. Suspendisse ullamcorper aliquam mauris, viverra tincidunt augue suscipit at. Sed luctus scelerisque justo ut semper. Phasellus massa mauris, molestie at nulla eget, dictum ultricies nisi. Aliquam rhoncus nisl sed urna egestas, vel luctus lectus euismod. Mauris sagittis dapibus leo. Donec vitae dignissim risus, ac ultricies augue. Ut vulputate tortor sed dui consectetur placerat. Nullam viverra non diam vel dignissim. - -Suspendisse potenti. Integer facilisis neque vitae euismod adipiscing. Aenean dictum leo commodo dui sodales, sit amet volutpat mauris gravida. Proin tempus convallis eros at consectetur. Proin nisl purus, dictum vel euismod ut, imperdiet quis est. Etiam sollicitudin lobortis neque eget pulvinar. Etiam venenatis vel sem ut posuere. Aliquam consectetur rhoncus facilisis. Morbi a viverra orci. - -Praesent ut vehicula orci, vel convallis risus. Suspendisse consectetur varius interdum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin vel sodales enim. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur suscipit tristique justo vel dapibus. Sed tincidunt mattis massa a auctor. Donec in tincidunt elit. Curabitur interdum neque at ante fringilla tempus. In hac habitasse platea dictumst. Vivamus luctus ligula ut nisl fermentum egestas. Praesent pulvinar accumsan neque. Sed nec leo sit amet arcu vehicula vehicula non ac diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; - -Morbi vel arcu quis neque dictum ullamcorper. Pellentesque condimentum consequat lacinia. Vestibulum eleifend placerat erat, eu hendrerit dui pulvinar eget. Vestibulum accumsan, lectus id vehicula fringilla, erat eros dictum massa, ut tristique libero mi eu ante. In at ante nunc. Praesent sodales ullamcorper porta. Curabitur egestas odio elit. Praesent et libero malesuada, venenatis lorem vitae, eleifend nisl. Donec dapibus euismod turpis, nec porttitor turpis dapibus ac. Fusce iaculis lacus eget dictum aliquam. Sed dictum eu enim ac posuere. Ut accumsan, ipsum et laoreet consectetur, ipsum quam dapibus diam, ac molestie ligula dui id massa. Nulla aliquam mauris congue nibh vestibulum imperdiet. - -Aenean ultricies, orci vel consectetur suscipit, ante nisi eleifend est, vitae suscipit risus erat dictum dui. Ut nisi diam, tristique sed nisl nec, aliquam gravida orci. Vestibulum in molestie sem. Ut eu molestie mi. Suspendisse potenti. Nunc sagittis lorem ut est vehicula, vitae imperdiet leo rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec pharetra urna pretium, faucibus orci cursus, sodales purus. Duis iaculis dignissim augue, non aliquet ligula elementum quis. Sed tempus a nisi et faucibus. Donec mattis ligula ac sem aliquam, sed sodales est ullamcorper. Cras vel orci est. Integer eget ultricies nisi. - -Quisque rhoncus, nisl vel auctor consectetur, sapien augue iaculis urna, quis suscipit arcu dolor ac est. Fusce sit amet quam lacinia, malesuada lorem vitae, dapibus justo. Mauris eget massa nec dolor volutpat tempor ut eu ante. Donec risus mi, aliquam at euismod vulputate, ullamcorper non leo. Etiam ac elit velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus sollicitudin enim sed felis pharetra, a convallis mi venenatis. Donec convallis non velit venenatis suscipit. Sed massa risus, dictum quis aliquam ut, placerat quis arcu. Donec at nisi neque. Nullam porta et mi vel ultricies. Donec vel aliquam sem. Mauris varius, ipsum interdum mattis rhoncus, nunc nisl vehicula tortor, quis condimentum nibh nisl vel ante. Sed vel pretium dui. Lorem ipsum dolor sit amet, consectetur adipis --- a/tests/data/certificates/Readme.md +++ /dev/null @@ -1,35 +0,0 @@ - - -# Recreating certificates - -Regenerate a new certificate key together with the good (Nextcloud Security) and bad (default Org name) certificates - -## Good Certificate -``` -openssl req \ - -newkey rsa:2048 \ - -nodes \ - -keyout security.nextcloud.com.key \ - -x509 \ - -days 3650 \ - -out goodCertificate.crt -``` -- Country Name: `DE` -- State or Province Name:`Berlin` -- Organization Name:`Nextcloud Security` -- Common Name: `security.nextcloud.com` - -## Bad Certificate -``` -openssl req \ - -key security.nextcloud.com.key \ - -new \ - -x509 \ - -days 3650 \ - -out badCertificate.crt -``` -- Country Name: `DE` -- State or Province Name:`Berlin` --- a/tests/data/certificates/badCertificate.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDYzCCAkugAwIBAgIURRAxXDBFFR1X0WuUOj7oKFDCooMwDQYJKoZIhvcNAQEL -BQAwQTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEhMB8GA1UECgwYSW50 -ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI1MDgyNTEwMDAxNVoXDTM1MDgyMzEw -MDAxNVowQTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEhMB8GA1UECgwY -SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAvPH9evINYYNhBg5YaZh3OUVl4SUCUbAc+xmZmiuAxoXN10avmfa/ -rpoeHua2fbPa48BOIyDPxXoofEwfqB+Q8P4CsSjEaRegsj3Djdkoyz+edPb9vTWE -pkSCfj/K3zS2kkY2nPw6qVRVen3siP97kNv3NviU5wbvv5RW5Qjz8W6UMuX2Th4j -7mQ5b1YdLHMFd2H6Vs3vAfiX8hY1DUc6U4Uf4XBO372k8sUyQtsSGlHxBKwPN7Gh -KZJqkTUCXWC4JzOkShXTe179SlxFLQtOqU2azrJqlb6/0cxGtsZLcv/CI+x+Fpca -hAQ4Qzii7iRRBlk8aDfAtXJafqhKJo1N/wIDAQABo1MwUTAdBgNVHQ4EFgQU8LRp -H0E1ASvIgy5FCsVdX2nyYSkwHwYDVR0jBBgwFoAU8LRpH0E1ASvIgy5FCsVdX2ny -YSkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAZuFOU5L22cPI -E1bMaIc6UjQn5l2QB9qhTVJuGmdo8ARLNVv3FyEHDaSCNhV1V0MEmnOwwKGKXggo -Uxe8Ki29fOt3ftkWG2F22Fh3PsU4xIAqwQv5s+urR/9E6ylfIbqPdj6XiY/D9jID -wnPDsNG+7ar90YFxsxisemQIVAOLzWGS2a3fglRz0hJfaoAxJdXbz///MflWzdUL -7t9ngBsbf9M/dicltLrCcYYG+BMC+fAoZO3aRSfS/+hxBjc3hNYQ5zLzWqzLzF0y -N+rxo/2G0fvqipowkMTgYcCIBA9DZY1YPCwgTfHGyrH3XI7yImdoHYs5Ld39Sq0J -+fXdrZSPlQ== ------END CERTIFICATE----- --- a/tests/data/certificates/expiredCertificate.crt +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICATCCAWoCCQCjCIB6tCZ2sDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTE0MDgyNzA5MTI0M1oXDTE0MDgyODA5MTI0M1owRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvrMe -x5D45HVMV2U4kqTU0mzHAihHT6r+OtO6g7S9yIlJZGGVcEet6An78Ow7aYM141eI -Jfbvqql7OIblHXSw7mvkw4bOQ1ee5lmJYOYCgaMNJ6mBLJfpK9xwidb0ZvhWOA8P -DLIiBKA3T5ChXCzilD5GF2+H/BXBE9lL9tuDjM0CAwEAATANBgkqhkiG9w0BAQUF -AAOBgQBuNClmOj3wudlX86nygcZgQT2+ZS8f1iJgM9lbrrkenT6tgcT1/YjcrN9C -BZR29Wz7htflpqverLUGZXh72K+gYercyR16Zu7zjt/NWuZldZmzJ3bUGq2HSoCX -2sDykAEuaDxUlzdJrztlOH4vPlRaGbxUogpC2hB1BQfxA90CIA== ------END CERTIFICATE----- \ No newline at end of file --- a/tests/data/certificates/goodCertificate.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIUGyAVECfPaXxvfEOrxo58ANU0MYEwDQYJKoZIhvcNAQEL -BQAwbTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy -bGluMRswGQYDVQQKDBJOZXh0Y2xvdWQgU2VjdXJpdHkxHzAdBgNVBAMMFnNlY3Vy -aXR5Lm5leHRjbG91ZC5jb20wHhcNMjUwODI1MDk1MzE0WhcNMzUwODIzMDk1MzE0 -WjBtMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs -aW4xGzAZBgNVBAoMEk5leHRjbG91ZCBTZWN1cml0eTEfMB0GA1UEAwwWc2VjdXJp -dHkubmV4dGNsb3VkLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ALzx/XryDWGDYQYOWGmYdzlFZeElAlGwHPsZmZorgMaFzddGr5n2v66aHh7mtn2z -2uPATiMgz8V6KHxMH6gfkPD+ArEoxGkXoLI9w43ZKMs/nnT2/b01hKZEgn4/yt80 -tpJGNpz8OqlUVXp97Ij/e5Db9zb4lOcG77+UVuUI8/FulDLl9k4eI+5kOW9WHSxz -BXdh+lbN7wH4l/IWNQ1HOlOFH+FwTt+9pPLFMkLbEhpR8QSsDzexoSmSapE1Al1g -uCczpEoV03te/UpcRS0LTqlNms6yapW+v9HMRrbGS3L/wiPsfhaXGoQEOEM4ou4k -UQZZPGg3wLVyWn6oSiaNTf8CAwEAAaNTMFEwHQYDVR0OBBYEFPC0aR9BNQEryIMu -RQrFXV9p8mEpMB8GA1UdIwQYMBaAFPC0aR9BNQEryIMuRQrFXV9p8mEpMA8GA1Ud -EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKuoLKvlt7W63t9xDIYbYI3e -3BeSt769lA2aBaIktSg7hVIFvX929sMMROD0sNSx81IMl7Cxm9u6C3BuKzUZDOwv -hy7gj+CaA5e5moUmpfrFtTR4aAoacZpjDXd3H/OqMELFkx5QHhw5z2nnubrNaera -pMQj/GOe/7+d1vjma+kNcKT6JBd5BPQqfk9+AVv5TombaLf3xc7dFR6bUQZKkZ2L -9zCZamUai/qf66KQzdELLpUsX1O2ByCl95wVL5c0CcDObxLuVDn8AzsAnsa5EhM8 -68s38Xb/gUYDIny2F43+rEk8qQjd5OITFfd2K6ur5EcvIQrFHE7IUbKlIu9RqPs= ------END CERTIFICATE----- --- a/tests/data/certificates/openSslTrustedCertificate.crt +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN TRUSTED CERTIFICATE----- -MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB -qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV -BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw -NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j -LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG -A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs -W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta -3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk -6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 -Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J -NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP -r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU -DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz -YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX -xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 -/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ -LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 -jVaMaDAMMAoGCCsGAQUFBwMB ------END TRUSTED CERTIFICATE----- --- a/tests/data/desktopapp.svg +++ /dev/null @@ -1 +0,0 @@ -Desktop appWindows, OS X, Linux \ No newline at end of file --- a/tests/data/emails/new-account-email-custom-text-alternative.txt +++ /dev/null @@ -1,13 +0,0 @@ -Welcome aboard - text - -Welcome to your Nextcloud account, you can add, protect, and share your data. - text - -Your username is: abc - - -Set your password - text: https://example.org/resetPassword/123 -Install Client - text: https://nextcloud.com/install/#install-clients - - --- -TestCloud - A safe home for your data --- a/tests/data/emails/new-account-email-custom.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - -
-
- - - -
- - - - - - -
- - - -
- -
- - -
-
-
- - - - - - -
 
- - - - - -
-

Welcome aboard

-
- - - - - - -
 
- - - -
- - - - - - -
- - - - - -
- - - - - -
-

Welcome to your Nextcloud account, you can add, protect, and share your data.

-
-
- - - - - -
- - - - - -
-

Your username is: abc

-
-
- - - - - -
 
- - - - - - -
- - - - - -
-
- - - - - -
- - - - -
- Set your password -
-
- - - - - -
- - - - -
- Install Client -
-
- -
-
-
-
-
- - - - - -
 
- - - - -
-
- -
                                                           
- - --- a/tests/data/emails/new-account-email-custom.txt +++ /dev/null @@ -1,13 +0,0 @@ -Welcome aboard - -Welcome to your Nextcloud account, you can add, protect, and share your data. - -Your username is: abc - - -Set your password: https://example.org/resetPassword/123 -Install Client: https://nextcloud.com/install/#install-clients - - --- -TestCloud - A safe home for your data --- a/tests/data/emails/new-account-email-single-button.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - -
-
- - - -
- - - - - - -
- - - -
- -
- - -
-
-
- - - - - - -
 
- - - - - -
-

Welcome aboard

-
- - - - - - -
 
- - - -
- - - - - - -
- - - - - -
- - - - - -
-

Welcome to your Nextcloud account, you can add, protect, and share your data.

-
-
- - - - - -
- - - - - -
-

Your username is: abc

-
-
- - - - - -
 
- - - - - - -
- - - - - -
-
- - - - -
- - - - -
- Set your password -
-
-
-
-
-
-
- - - - - -
 
- - - - -
-
- -
                                                           
- - --- a/tests/data/emails/new-account-email-single-button.txt +++ /dev/null @@ -1,10 +0,0 @@ -Welcome aboard - -Welcome to your Nextcloud account, you can add, protect, and share your data. - -Your username is: abc - -https://example.org/resetPassword/123 - --- -TestCloud - A safe home for your data --- a/tests/data/emails/new-account-email.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - -
-
- - - -
- - - - - - -
- - - -
- -
- - -
-
-
- - - - - - -
 
- - - - - -
-

Welcome aboard

-
- - - - - - -
 
- - - -
- - - - - - -
- - - - - -
- - - - - -
-

Welcome to your Nextcloud account, you can add, protect, and share your data.

-
-
- - - - - -
- - - - - -
-

Your username is: abc

-
-
- - - - - -
 
- - - - - - -
- - - - - -
-
- - - - - -
- - - - -
- Set your password -
-
- - - - - -
- - - - -
- Install Client -
-
- -
-
-
-
-
- - - - - -
 
- - - - -
-
- -
                                                           
- - --- a/tests/data/emails/new-account-email.txt +++ /dev/null @@ -1,14 +0,0 @@ -Welcome aboard - -Welcome to your Nextcloud account, you can add, protect, and share your data. - -Your username is: abc - - -Set your password: https://example.org/resetPassword/123 -Install Client: https://nextcloud.com/install/#install-clients - - --- -TestCloud - A safe home for your data -This is an automatically sent email, please do not reply. \ No newline at end of file --- a/tests/data/guest_avatar_einstein_32.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - E - --- a/tests/data/ics/event-builder-complete.ics +++ /dev/null @@ -1,17 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -BEGIN:VEVENT -UID:event-uid-123 -DTSTAMP:20250105T000000Z -SUMMARY:My event -DTSTART:20250105T170958Z -DTEND:20250105T171958Z -STATUS:CONFIRMED -DESCRIPTION:Foo bar baz -ORGANIZER:mailto:organizer@domain.tld -ATTENDEE:mailto:attendee1@domain.tld -ATTENDEE:mailto:attendee2@domain.tld -END:VEVENT -END:VCALENDAR --- a/tests/data/ics/event-builder-complete.ics.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/ics/event-builder-without-attendees.ics +++ /dev/null @@ -1,14 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -BEGIN:VEVENT -UID:event-uid-123 -DTSTAMP:20250105T000000Z -SUMMARY:My event -DTSTART:20250105T170958Z -DTEND:20250105T171958Z -STATUS:CONFIRMED -DESCRIPTION:Foo bar baz -END:VEVENT -END:VCALENDAR --- a/tests/data/ics/event-builder-without-attendees.ics.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/ics/free-busy-request.ics +++ /dev/null @@ -1,14 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:20250116T060000Z -DTEND:20250117T060000Z -ORGANIZER:mailto:admin@imap.localhost -ATTENDEE:mailto:admin@imap.localhost -ATTENDEE:mailto:user@imap.localhost -ATTENDEE:mailto:empty@imap.localhost -END:VFREEBUSY -END:VCALENDAR --- a/tests/data/ics/free-busy-request.ics.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/data/integritycheck/SomeApp.crt +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEwTCCAqmgAwIBAgIUWv0iujufs5lUr0svCf/qTQvoyKAwDQYJKoZIhvcNAQEF -BQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw -MzIyNDk1M1oXDTE2MTEwMzIyNDk1M1owEjEQMA4GA1UEAwwHU29tZUFwcDCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK8q0x62agGSRBqeWsaeEwFfepMk -F8cAobMMi50qHCv9IrOn/ZH9l52xBrbIkErVmRjmly0d4JhD8Ymhidsh9ONKYl/j -+ishsZDM8eNNdp3Ew+fEYVvY1W7mR1qU24NWj0bzVsClI7hvPVIuw7AjfBDq1C5+ -A+ZSLSXYvOK2cEWjdxQfuNZwEZSjmA63DUllBIrm35IaTvfuyhU6BW9yHZxmb8+M -w0xDv30D5UkE/2N7Pa/HQJLxCR+3zKibRK3nUyRDLSXxMkU9PnFNaPNX59VPgyj4 -GB1CFSToldJVPF4pzh7p36uGXZVxs8m3LFD4Ol8mhi7jkxDZjqFN46gzR0r23Py6 -dol9vfawGIoUwp9LvL0S7MvdRY0oazLXwClLP4OQ17zpSMAiCj7fgNT661JamPGj -t5O7Zn2wA7I4ddDS/HDTWCu98Zwc9fHIpsJPgCZ9awoqxi4Mnf7Pk9g5nnXhszGC -cxxIASQKM+GhdzoRxKknax2RzUCwCzcPRtCj8AQT/x/mqN3PfRmlnFBNACUw9bpZ -SOoNq2pCF9igftDWpSIXQ38pVpKLWowjjg3DVRmVKBgivHnUnVLyzYBahHPj0vaz -tFtUFRaqXDnt+4qyUGyrT5h5pjZaTcHIcSB4PiarYwdVvgslgwnQzOUcGAzRWBD4 -6jV2brP5vFY3g6iPAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACTY3CCHC+Z28gCf -FWGKQ3wAKs+k4+0yoti0qm2EKX7rSGQ0PHSas6uW79WstC4Rj+DYkDtIhGMSg8FS -HVGZHGBCc0HwdX+BOAt3zi4p7Sf3oQef70/4imPoKxbAVCpd/cveVcFyDC19j1yB -Bapwu87oh+muoeaZxOlqQI4UxjBlR/uRSMhOn2UGauIr3dWJgAF4pGt7TtIzt+1v -0uA6FtN1Y4R5O8AaJPh1bIG0CVvFBE58esGzjEYLhOydgKFnEP94kVPgJD5ds9C3 -pPhEpo1dRpiXaF7WGIV1X6DI/ipWvfrF7CEy6I/kP1InY/vMDjQjeDnJ/VrXIWXO -yZvHXVaN/m+1RlETsH7YO/QmxRue9ZHN3gvvWtmpCeA95sfpepOk7UcHxHZYyQbF -49/au8j+5tsr4A83xzsT1JbcKRxkAaQ7WDJpOnE5O1+H0fB+BaLakTg6XX9d4Fo7 -7Gin7hVWX7pL+JIyxMzME3LhfI61+CRcqZQIrpyaafUziPQbWIPfEs7h8tCOWyvW -UO8ZLervYCB3j44ivkrxPlcBklDCqqKKBzDP9dYOtS/P4RB1NkHA9+NTvmBpTonS -SFXdg9fFMD7VfjDE3Vnk+8DWkVH5wBYowTAD7w9Wuzr7DumiAULexnP/Y7xwxLv7 -4B+pXTAcRK0zECDEaX3npS8xWzrB ------END CERTIFICATE----- \ No newline at end of file --- a/tests/data/integritycheck/SomeApp.crt.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/integritycheck/SomeApp.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEAryrTHrZqAZJEGp5axp4TAV96kyQXxwChswyLnSocK/0is6f9 -kf2XnbEGtsiQStWZGOaXLR3gmEPxiaGJ2yH040piX+P6KyGxkMzx4012ncTD58Rh -W9jVbuZHWpTbg1aPRvNWwKUjuG89Ui7DsCN8EOrULn4D5lItJdi84rZwRaN3FB+4 -1nARlKOYDrcNSWUEiubfkhpO9+7KFToFb3IdnGZvz4zDTEO/fQPlSQT/Y3s9r8dA -kvEJH7fMqJtEredTJEMtJfEyRT0+cU1o81fn1U+DKPgYHUIVJOiV0lU8XinOHunf -q4ZdlXGzybcsUPg6XyaGLuOTENmOoU3jqDNHSvbc/Lp2iX299rAYihTCn0u8vRLs -y91FjShrMtfAKUs/g5DXvOlIwCIKPt+A1PrrUlqY8aO3k7tmfbADsjh10NL8cNNY -K73xnBz18cimwk+AJn1rCirGLgyd/s+T2DmedeGzMYJzHEgBJAoz4aF3OhHEqSdr -HZHNQLALNw9G0KPwBBP/H+ao3c99GaWcUE0AJTD1ullI6g2rakIX2KB+0NalIhdD -fylWkotajCOODcNVGZUoGCK8edSdUvLNgFqEc+PS9rO0W1QVFqpcOe37irJQbKtP -mHmmNlpNwchxIHg+JqtjB1W+CyWDCdDM5RwYDNFYEPjqNXZus/m8VjeDqI8CAwEA -AQKCAgAGEpX/GpPSOh/iTFsZR6GhCo5VS4sHex4f9u9gI3WWkNADKm+//+qhrOFu -tMVL0tvb4SKcjcyber+E5fTBhAvZVVrTuDOUCzb8rh40oxrZnVitUEGPzZSYo6MV -oNN7WiTdcNIxG4iBfFnD35spIBHNBFcWxYedFHw8M6dYtLpvr5sRN4hQ5tG1NXaw -C+iKAtaFejuF9SOHtN+MnNZTZsFgCq0VpOugWTjqPJhWT7YK3NrmnSG/9ls6nkSa -E8ftv3dCapHGHvZ/MABaLTTWOtXurzL82Jz9Zq0U+ns3L31IRmq+55y5dY8I/0gc -Vh1TMUfUxKEiPwF6NBCdxvV9f0mZYQPEeHATnkew/SW35EDPgPSS/H3jJLORhAUJ -7ZzDkvwdfWjnHxyzJtbHHZ8r3gqwq0rqfyd4Rz04TusUukfikdy5ooL1dMLiy3od -V40zQxVpI44zrrLBjaJ+OItdD5NFRu82K0OF653a/1fP7x4fw3j/ATVQhW8g0EE3 -oEEdGaciqXDWNxlSwiZUmteq3chN+JRXrBWlOodV5LyJwiy5/5NMV91a5AP0wOsT -BXzhkjnLsGJMSQuzDs1CFUVjqs1PudhyW+pgoy8HaTSfamKiAHbJlBSuSX0acR7S -iwiGhzkChWtUv1vMVM56z19g/umwmZujwhr2jkN1Y0zZH//xAQKCAQEA3GV74emO -IMjsb8VbJyCHycYEfcu7fZ90mLrK7yyjNxobROniqjWYT92fjse1qrTv54luxFN7 -EUF5ec9QRYo1mnyltjqTCx3jfqMlCr94LaIEdMQJ/f3vW6pEj7xY4RSY9hZX8v1Z -mrhKcFkqGSFdWLliVD471Mxi2/7T8mKjmVhKy3P+Cm1aHardGqvT/mUTOB0sO0OT -CJSRUWh2Tx2d3B1M6nDXZL8TTVdndEKUycj204Pf6fm6bMJcVuro/kPcwByhG+Ar -dAKN4HdEa+XmrfqDKzl9yhLUpwXa7rI8g5if/oVj2kF/hny4mboL4U4rC+YWQZ1f -TBklcYPCx2NgqwKCAQEAy3bioCaqa+iAsSWLjRp1Dh+93vI7WXvuPdLkAQ/Iz6yQ -sCqjQj/4jIxm7qmhbh8X3B8VkaX2jnuaR9nUuZyjIvHg1WMCMeMYWiok5GCppkyH -yLoEIT1mTyTe3Wjk37Zi+1UQRHPS/8noVtVj0ng95SChnmguDF3otvl4lLH+wgCz -z1xMO9VyLTEGU3jbPgXqNeGKdqk/kEsU4C+NuwPiRMAuDuKR+s5ldB293GBPnFKu -OqgzVoJJChLAPUvzSE+hJmWin2hoCZMktLUdSCng9rm9AhXWYpLwUrANZuvI8RfO -9wX1NR17U+QbX2PvDlEl6B9Yt3G4R9qNxUPLrjP/rQKCAQEAyDO3kMOjw8xAWleg -Ma6vKm6h7dN/gOGz/HjRlumpaYhhdPwwVgVRUlszcXOgZmzt8Bk7cUOT61zah/f2 -JvUhNDA+J4aVw+dmm8Z/A4BiHrGp8peRrBNbtpy4owiog+099Wzef2/8UTtPAzc7 -spBIRyw/Ud8mYms28jhNN0S677TwXFgFUFt9HK31IyEq9U/DYZm+cCc2DPlH9/c4 -YS26FBTZpazTPEUFt5/J7iX9Gj9fV0vXvqaG3fy//IRvGWlzwV9ASh4b2snnLxuo -H4s7PJbvR/h1d3YbjY0YDvQBXFjsHTv2NHCC8xugZKRH3mYvXCOp2/ikdG/zP2Y9 -LPns+QKCAQB1M2Mx63PpusE+yajMO/xHiYM+xHvpfNjsZemOrv/2mKmzwKvQQrcy -hsHYIoBpxaFh28n53wbaZlqlntXJoW/bdkcTw/eEsxLZBUPBBelTcOwadQRh/VNM -ralvErgcIZx8uDApripRy4V5V2wr1bWZoaVXcR1tZD7j/2o1BR8Bs5PgE4OaR8aA -P6gsNwbbgF68cNHorm9997HrvZi/rGoPPkCJtHtwZKnOLD+sjRHuszXHdhI0d9II -6mowJOrbsXrbelolxud+9HKFYXqfkfgTR0SXyep3V7r1dpIRwio6roM6igUIdpYO -6evWk+MldRsHzd61tNz5DuzxP685Bpz1AoIBAGnP0wLx3B9ri2Q5g3ENPdZhAop+ -/CMZUWR297yc0gSIoCnTBtNG3y735P887zOQlIZaFnnMzPP85mPMMMUT5Os5kFrE -MzHaNkgpbxD2xcAB3sA1Hk/yn/c/nuTTZpTTYGv0OVE+/rQ4l2al1ngWTTz8/Bf1 -m3IDKluBTNkoD8DnS9+9CT3YWpnEr3sw8Vqzuat5hSgneJP1dtv2dCDXQ4MOUZbL -riXuafvnlbC6tfmOGdeZ90rdTQWRIckP8zDZI0kZ1XADEwD1iTnWgfI1hoKI4MN8 -3h4JbmLC0nvaRDivwOuy2BDhpYW6wRtphSYvfshlmAptt306Na35FUvLL80= ------END RSA PRIVATE KEY----- \ No newline at end of file --- a/tests/data/integritycheck/SomeApp.key.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/integritycheck/app/AnotherFile.txt +++ /dev/null @@ -1 +0,0 @@ -Another file with some Content. --- a/tests/data/integritycheck/app/subfolder/file.txt +++ /dev/null @@ -1 +0,0 @@ -A file with some content. --- a/tests/data/integritycheck/appWithInvalidData/AnotherFile.txt +++ /dev/null @@ -1 +0,0 @@ -TAMPERED FILE. \ No newline at end of file --- a/tests/data/integritycheck/core.crt +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEvjCCAqagAwIBAgIUc/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF -BQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw -MzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s -iOf4RwPXR6SE9bWZEm/b72SfWk//J6AbrD8WiOzBuT/ODy6k5T1arEdHO+Pux0W1 -MxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr/xolP3oD+eLbShPcblhdS -VtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0 -klnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5/2riAzIssMFSCarWCx0AKYb54+d -xLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77 -H87KFhYW8tKFFvF1V3AHl/sFQ9tDHaxM9Y0pZ2jPp/ccdiqnmdkBxBDqsiRvHvVB -Cn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO/wAtd2vUW8UFiq -s2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ/zrM0 -i8nfCFwTxWRxp3H9KoECzO/zS5R5KIS7s3/wq/w9T2Ie4rcecgXwDizwnn0C/aKc -bDIjujpL1s9HO05pcD/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ -Q238lC+A/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2 -AvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji -oNCXUbExC/0iCPUqdHZIVb+Lc/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd -9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb -H+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th/55 -cf3Fovj6JJgbb9XFxrdnsOsDOu/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX -uVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO//+TJtXRbyNgsf -oMRZGi8DLGU2SGEAHcRH/QZHq/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1 -0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI/XUxEWOa2F -K2EqhErgMK/N07U1JJJay5tYZRtvkGq46oP/5kQG8hYST0MDK6VihJoPpvCmAm4E -pEYKQ96x6A4EH9Y9mZlYozH/eqmxPbTK8n89/p7Ydun4rI+B2iiLnY8REWWy6+UQ -V204fGUkJqW5CrKy3P3XvY9X ------END CERTIFICATE----- \ No newline at end of file --- a/tests/data/integritycheck/core.crt.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors -SPDX-FileCopyrightText: 2016 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/integritycheck/core.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKQIBAAKCAgEAtvoSAemQCpls7m9E7xdKHsbtcZYfDmyI5/hHA9dHpIT1tZkS -b9vvZJ9aT/8noBusPxaI7MG5P84PLqTlPVqsR0c74+7HRbUzFgkkjiQfvgoqAykL -RIHRG37xasypXWveGEnjp1/pev/GiU/egP54ttKE9xuWF1JW2RkSSh6/xKHovbAM -J4cPI/HO+PXDZ1N0ZU70rO2fTEiXJ8QFqSq8Fa1zHKLcufSSWfGUzHe08NQTiSq+ -mJr2o2IHUA3Y9X3d42vn/auIDMiywwVIJqtYLHQAphvnj53EulxgXKo8nTJ0EKQX -vwMPjlEJl63o81iR3OBuqVRYQYaiaS4OglsGDjDODM7QPvsfzsoWFhby0oUW8XVX -cAeX+wVD20MdrEz1jSlnaM+n9xx2KqeZ2QHEEOqyJG8e9UEKfqqlvi9YYULu8c4F -9iymYQvXMuUpdm/d7MxkRnDs71m9Q/dU7/AC13a9RbxQWKqza/VCc0s3qM2HnWht -zCasG9aEnL29Ah20iNBsPGpZMeHXNRKvD3CuWSUIFn/OszSLyd8IXBPFZHGncf0q -gQLM7/NLlHkohLuzf/Cr/D1PYh7itx5yBfAOLPCefQL9opxsMiO6OkvWz0c7Tmlw -P9XfApw9nWLPKYGSYzIhsvnaJFU3kVNUd5l1c+kW6r4JNAlDbfyUL4D8o5UCAwEA -AQKCAgBOkCKpNYqGMogF/DqB2eMWQd1zdryQ6eMCjqSXLpjxN7F0LmwvISSxdIZH -cMunwBn94IQb+7W5gpUcNurCpCryU9CQNlbTRFDR9kz+xt3mL+EICFhxKrgI8UFg -1M0ncogir58Sn2jVSfsJvARSKHDWNp+mpe6UxuLJRi2HK5q1J7uRroQZeLD0gv+V -/5fNxpRkZzlBAqnyC/zyswSnNNUbDaUuN3NEWJF6EvMLs546BST6MSMyzN53GkD/ -i2KLTWa3Hf62+S5qJsYyXBM1nz41n/0jVTngfSIZzk4Fm4Z5DE+vUXVsqzjDp2HS -AXbS/UVrq+V3yOI4CEG1nXPXXpPDS/werQcSvANGHd/LLiQ3qfcs1S/SBihDjSFQ -CBgH3y06qDdnKxdPjpRYZpOBnkdQLHF4OwlhPXBd083Ep7jiF1WIgzwBP9o9wEWi -dVT0Vr5vsB61MQ+4p26Us1yWm51g6AxpTu5y2RPmbuDh1IvNbheeITQMSmbtGf1R -JZ4yqrnYpd3akja9hwko1xoWuHT15rr2pTs8g/PtHH7sNkZdThMtJscEt4YIIxoN -CQ+VM4lGYogtySbYEiUkRNF3t06AsPhBehcH7oldUqb6UKKoi6NCZOiC5RsdpOY3 -JJX2nkCMk52264sI+kWl6kEVBpMzeLW+BM0Xi1AQRyHPIY+VZQKCAQEA4sjNh35x -ezjiOWsq84UOUHdvei9HAm+MQMM2pEgdHWjjawhoH122gi2G/tpgNGONl/XNmZkk -ni00jFtNRA9xDF6mv1CynxiWhKGLdEH3MELQqGyeNOE9GBQVMo2W2J2mvOj+GzC+ -cRrEBjR1MDGx+XLO8FbYyKiwVg8/OIT9hIYSlBIsu0bPwYb6X3KlfZfmdh/MZvCs -HDthzRYnJlkVerB/2ZnfTVYflQh7XoKFipVXFMs+oG0mKCUT4HpqXWTek1Jqt8bQ -og9235C0jEcFWjSHtp2Jhena8yMD4YKQGI7tFVFm9UkHkKPcdfIW+hoVC4vLI0fs -LMwhzOvFof4pawKCAQEAzoyGHKUA2KG8JVV49C5LKLmJv0nBj7aT5EXcg1J9OZn7 -zP/o/BHJQpeLNI2UD5c0Ron33iRLVqNvU4sTdo597Qoc2jWsZWRmdTz2Q4VvnxHu -VBvao0vUG4xqIbMtv4VRkuNg9EmlF4luT5+x30E6DWDMK0RhSmM9yWG7My6JG2IN -LZ457tWvk7F1HTKNt3uFJTAOx0VqjJsbaw7Gsq7hTmObTUa+q8Ss0oK+iRkP5Obv -9F9zUWv2UjKs/G4JYADfFhS1Ovha8pu5p+NszlGBGvG99EErRpiUPcxCTjSiUvDl -ALn3YTDc9oSC+6b/sI7/4uQVSri5ybXLGzbtMWKm/wKCAQEAvO12M6uF1Ja1+Ams -lYTCQQzO5OZf7MqK+CTo74FYJ/kKhE9TltXWRqqw7L12Kg7Jlc/jgVNQaynTvh4N -x2Zp0llD5tvOgrXUJxgBek++Iwl2lOkv/3OpFtccNao5AaqMjpI3puU7sjQPG/A1 -tHmh/+LCPPzMypWlmXxIOcio/u9GqO5fL4E1cM8G4985uOCD0OJ6wUM8zqQ1vMn4 -wXyzZSuGxvvmSKI320teo4Ruxd3V1u/e830arZT98yNoWve+aNLfLszFYE0rxeHi -V36PGe/rI5ooSFRi3+zKveKsMplXL0xKTouRbtDjx6pvs9losN6701+GhGdmvTWp -xmNbkwKCAQEAmY94FcvG+UglbUxChKf2UOzAMGtRcNs40Lnv2+J0H2MQBbUtLlq6 -2rt4TzYDIiQ0RU1F7u3k5SDVH7OCYN5HWPfvw3usFCW01uzf2gtWlVjra7TZtBYo -N+MI9M0V8hHYN/C8oGIwT3Npg+EiiO0hj9ircm+ANaHayeHTH5Y1cRpQ2d2NDLfp -tVB11aNEIWm/74nvMs+1C5w1oj52E1pZP8JmL+ms0F+EbW2u4pazbmcTdweP4LT3 -iN0MJxBX//wl33C93H3QgBauzNcUib+m0LVxmCrrVa0SaW92zFXtaOSYHRYliSie -3thd2WKrLkTikXkpK0hzODfkLPOFHPZPWQKCAQAG0Yz7eQblxIHII60ReeYIovum -Gmn+ot0jeuPg5gYpopQygL87ygc00ER+SHgZLBjIx3uCYDbC6Q4SzEPLa/aUS3/e -94vYBVoWYvTYUazuwgJBA1Xm7BnlqG7cQziJOQxBIJBXaX96xUptcmlKrIIYD9Jz -qeUbbbqN4bBYjXJdNdMqU1f6t2IK7hcjFXJMpS2wJdv1AlYCUWDquQ0BUePCJAPf -N0rKm12ffhi564NqN/6PtT7iEkSPKT2CEyqrvXwx0Lajz0ZokFzRm2iYUTxik2fI -Lcq5zXyM4gs1hDnrasn1g0JyfeUgnPNNuWeFG0cMb8o7FeYQImhqheIgMJLP ------END RSA PRIVATE KEY----- \ No newline at end of file --- a/tests/data/integritycheck/core.key.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/integritycheck/htaccessUnmodified/.htaccess +++ /dev/null @@ -1 +0,0 @@ -# Start of valid file \ No newline at end of file --- a/tests/data/integritycheck/htaccessUnmodified/subfolder/.htaccess +++ /dev/null @@ -1,4 +0,0 @@ -# Start of valid file -#### DO NOT CHANGE ANYTHING ABOVE THIS LINE #### - -# Content that should change the hash in the root folder \ No newline at end of file --- a/tests/data/integritycheck/htaccessWithInvalidModifiedContent/.htaccess +++ /dev/null @@ -1,5 +0,0 @@ -# Start of valid file -#### DO NOT CHANGE ANYTHING ABOVE THIS LINE #### -# Content that should not modify the hash -#### DO NOT CHANGE ANYTHING ABOVE THIS LINE #### -# but it does because it is twice \ No newline at end of file --- a/tests/data/integritycheck/htaccessWithValidModifiedContent/.htaccess +++ /dev/null @@ -1,11 +0,0 @@ -# Start of valid file - - php_value default_charset 'UTF-8' - php_value output_buffering 0 - - SetEnv htaccessWorking true - - -#### DO NOT CHANGE ANYTHING ABOVE THIS LINE #### - -# Content that should change the hash in the root folder --- a/tests/data/integritycheck/htaccessWithValidModifiedContent/subfolder/.htaccess +++ /dev/null @@ -1,4 +0,0 @@ -# Start of valid file -#### DO NOT CHANGE ANYTHING ABOVE THIS LINE #### - -# Content that should change the hash in the root folder \ No newline at end of file --- a/tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js +++ /dev/null @@ -1,300 +0,0 @@ -/** -* This file is automatically generated -* DO NOT EDIT MANUALLY! -* -* You can update the list of MimeType Aliases in config/mimetypealiases.json -* The list of files is fetched from core/img/filetypes -* To regenerate this file run ./occ maintenance:mimetype:update-js -*/ -OC.MimeTypeList={ - aliases: { - "application/coreldraw": "image", - "application/epub+zip": "text", - "application/font-sfnt": "font", - "application/font-woff": "font", - "application/gpx+xml": "location", - "application/gzip": "package/x-generic", - "application/illustrator": "image", - "application/internet-shortcut": "link", - "application/javascript": "text/code", - "application/json": "text/code", - "application/km": "mindmap", - "application/msaccess": "file", - "application/msexcel": "x-office/spreadsheet", - "application/msonenote": "x-office/document", - "application/mspowerpoint": "x-office/presentation", - "application/msword": "x-office/document", - "application/octet-stream": "file", - "application/postscript": "image", - "application/rss+xml": "application/xml", - "application/toml": "text/code", - "application/vnd.android.package-archive": "package/x-generic", - "application/vnd.excalidraw+json": "whiteboard", - "application/vnd.garmin.tcx+xml": "location", - "application/vnd.google-earth.kml+xml": "location", - "application/vnd.google-earth.kmz": "location", - "application/vnd.lotus-wordpro": "x-office/document", - "application/vnd.ms-excel.addin.macroEnabled.12": "x-office/spreadsheet", - "application/vnd.ms-excel.sheet.binary.macroEnabled.12": "x-office/spreadsheet", - "application/vnd.ms-excel.sheet.macroEnabled.12": "x-office/spreadsheet", - "application/vnd.ms-excel.template.macroEnabled.12": "x-office/spreadsheet", - "application/vnd.ms-excel": "x-office/spreadsheet", - "application/vnd.ms-fontobject": "font", - "application/vnd.ms-powerpoint.addin.macroEnabled.12": "x-office/presentation", - "application/vnd.ms-powerpoint.presentation.macroEnabled.12": "x-office/presentation", - "application/vnd.ms-powerpoint.slideshow.macroEnabled.12": "x-office/presentation", - "application/vnd.ms-powerpoint.template.macroEnabled.12": "x-office/presentation", - "application/vnd.ms-powerpoint": "x-office/presentation", - "application/vnd.ms-visio.drawing.macroEnabled.12": "application/vnd.visio", - "application/vnd.ms-visio.drawing": "application/vnd.visio", - "application/vnd.ms-visio.stencil.macroEnabled.12": "application/vnd.visio", - "application/vnd.ms-visio.stencil": "application/vnd.visio", - "application/vnd.ms-visio.template.macroEnabled.12": "application/vnd.visio", - "application/vnd.ms-visio.template": "application/vnd.visio", - "application/vnd.ms-word.document.macroEnabled.12": "x-office/document", - "application/vnd.ms-word.template.macroEnabled.12": "x-office/document", - "application/vnd.oasis.opendocument.graphics-flat-xml": "x-office/drawing", - "application/vnd.oasis.opendocument.graphics-template": "x-office/drawing", - "application/vnd.oasis.opendocument.graphics": "x-office/drawing", - "application/vnd.oasis.opendocument.presentation-flat-xml": "x-office/presentation", - "application/vnd.oasis.opendocument.presentation-template": "x-office/presentation", - "application/vnd.oasis.opendocument.presentation": "x-office/presentation", - "application/vnd.oasis.opendocument.spreadsheet-flat-xml": "x-office/spreadsheet", - "application/vnd.oasis.opendocument.spreadsheet-template": "x-office/spreadsheet", - "application/vnd.oasis.opendocument.spreadsheet": "x-office/spreadsheet", - "application/vnd.oasis.opendocument.text-flat-xml": "x-office/document", - "application/vnd.oasis.opendocument.text-master": "x-office/document", - "application/vnd.oasis.opendocument.text-template": "x-office/document", - "application/vnd.oasis.opendocument.text-web": "x-office/document", - "application/vnd.oasis.opendocument.text": "x-office/document", - "application/vnd.openxmlformats-officedocument.presentationml.presentation": "x-office/presentation", - "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "x-office/presentation", - "application/vnd.openxmlformats-officedocument.presentationml.template": "x-office/presentation", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "x-office/spreadsheet", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template": "x-office/spreadsheet", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf": "x-office/form-template", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform": "x-office/form", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "x-office/document", - "application/vnd.openxmlformats-officedocument.wordprocessingml.template": "x-office/document", - "application/vnd.visio": "x-office/document", - "application/vnd.wordperfect": "x-office/document", - "application/vnd.xmind.workbook": "mindmap", - "application/x-7z-compressed": "package/x-generic", - "application/x-bzip2": "package/x-generic", - "application/x-cbr": "text", - "application/x-dcraw": "image", - "application/x-deb": "package/x-generic", - "application/x-fictionbook+xml": "text", - "application/x-font": "font", - "application/x-freemind": "mindmap", - "application/x-gimp": "image", - "application/x-gzip": "application/gzip", - "application/x-iwork-keynote-sffkey": "x-office/presentation", - "application/x-iwork-numbers-sffnumbers": "x-office/spreadsheet", - "application/x-iwork-pages-sffpages": "x-office/document", - "application/x-mobipocket-ebook": "text", - "application/x-openvpn-profile": "text", - "application/x-perl": "text/code", - "application/x-photoshop": "image", - "application/x-php": "text/code", - "application/x-rar-compressed": "package/x-generic", - "application/x-tar": "package/x-generic", - "application/x-tex": "text", - "application/xml": "text/html", - "application/yaml": "text/code", - "application/zip": "package/x-generic", - "application/zstd": "package/x-generic", - "audio/aac": "audio", - "audio/midi": "audio", - "audio/mpeg": "audio", - "audio/ogg": "audio", - "audio/opus": "audio", - "audio/wav": "audio", - "audio/webm": "audio", - "audio/x-midi": "audio", - "database": "file", - "httpd/unix-directory": "dir", - "image/targa": "image/tga", - "image/x-emf": "image/emf", - "text/css": "text/code", - "text/csv": "x-office/spreadsheet", - "text/html": "text/code", - "text/tab-separated-values": "x-office/spreadsheet", - "text/x-c": "text/code", - "text/x-c++src": "text/code", - "text/x-h": "text/code", - "text/x-java-source": "text/code", - "text/x-ldif": "text/code", - "text/x-nfo": "text/code", - "text/x-python": "text/code", - "text/x-rst": "text", - "text/x-shellscript": "text/code", - "video/mp4": "video", - "video/mpeg": "video", - "video/ogg": "video", - "video/webm": "video", - "web": "text/code", - "my-custom/mimetype": "custom" -}, - files: [ - "application", - "application-pdf", - "audio", - "file", - "folder", - "folder-drag-accept", - "folder-encrypted", - "folder-external", - "folder-public", - "folder-shared", - "folder-starred", - "font", - "image", - "link", - "location", - "mindmap", - "package-x-generic", - "text", - "text-calendar", - "text-code", - "text-vcard", - "video", - "whiteboard", - "x-office-document", - "x-office-drawing", - "x-office-form", - "x-office-form-template", - "x-office-presentation", - "x-office-spreadsheet" -], - themes: [], - names: {'application/cmd': t('core', "Windows Command Script"), -'application/epub+zip': t('core', "Electronic book document"), -'application/font-sfnt': t('core', "TrueType Font Collection"), -'application/font-woff': t('core', "Web Open Font Format"), -'application/gpx+xml': t('core', "GPX geographic data"), -'application/gzip': t('core', "Gzip archive"), -'application/illustrator': t('core', "Adobe Illustrator document"), -'application/java': t('core', "Java source code"), -'application/javascript': t('core', "JavaScript source code"), -'application/json': t('core', "JSON document"), -'application/msaccess': t('core', "Microsoft Access database"), -'application/msonenote': t('core', "Microsoft OneNote document"), -'application/msword': t('core', "Microsoft Word document"), -'application/octet-stream': t('core', "Unknown"), -'application/pdf': t('core', "PDF document"), -'application/postscript': t('core', "PostScript document"), -'application/rss+xml': t('core', "RSS summary"), -'application/toml': t('core', "TOML configuration file"), -'application/vnd.android.package-archive': t('core', "Android package"), -'application/vnd.google-earth.kml+xml': t('core', "KML geographic data"), -'application/vnd.google-earth.kmz': t('core', "KML geographic compressed data"), -'application/vnd.lotus-wordpro': t('core', "Lotus Word Pro document"), -'application/vnd.ms-excel': t('core', "Excel spreadsheet"), -'application/vnd.ms-excel.addin.macroEnabled.12': t('core', "Excel add-in"), -'application/vnd.ms-excel.sheet.binary.macroEnabled.12': t('core', "Excel 2007 binary spreadsheet"), -'application/vnd.ms-excel.sheet.macroEnabled.12': t('core', "Excel spreadsheet"), -'application/vnd.ms-excel.template.macroEnabled.12': t('core', "Excel spreadsheet template"), -'application/vnd.ms-outlook': t('core', "Outlook Message"), -'application/vnd.ms-powerpoint': t('core', "PowerPoint presentation"), -'application/vnd.ms-powerpoint.addin.macroEnabled.12': t('core', "PowerPoint add-in"), -'application/vnd.ms-powerpoint.presentation.macroEnabled.12': t('core', "PowerPoint presentation"), -'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': t('core', "PowerPoint presentation"), -'application/vnd.ms-powerpoint.template.macroEnabled.12': t('core', "PowerPoint presentation template"), -'application/vnd.ms-word.document.macroEnabled.12': t('core', "Word document"), -'application/vnd.oasis.opendocument.formula': t('core', "ODF formula"), -'application/vnd.oasis.opendocument.graphics': t('core', "ODG drawing"), -'application/vnd.oasis.opendocument.graphics-flat-xml': t('core', "ODG drawing (Flat XML)"), -'application/vnd.oasis.opendocument.graphics-template': t('core', "ODG template"), -'application/vnd.oasis.opendocument.presentation': t('core', "ODP presentation"), -'application/vnd.oasis.opendocument.presentation-flat-xml': t('core', "ODP presentation (Flat XML)"), -'application/vnd.oasis.opendocument.presentation-template': t('core', "ODP template"), -'application/vnd.oasis.opendocument.spreadsheet': t('core', "ODS spreadsheet"), -'application/vnd.oasis.opendocument.spreadsheet-flat-xml': t('core', "ODS spreadsheet (Flat XML)"), -'application/vnd.oasis.opendocument.spreadsheet-template': t('core', "ODS template"), -'application/vnd.oasis.opendocument.text': t('core', "ODT document"), -'application/vnd.oasis.opendocument.text-flat-xml': t('core', "ODT document (Flat XML)"), -'application/vnd.oasis.opendocument.text-template': t('core', "ODT template"), -'application/vnd.openxmlformats-officedocument.presentationml.presentation': t('core', "PowerPoint 2007 presentation"), -'application/vnd.openxmlformats-officedocument.presentationml.slideshow': t('core', "PowerPoint 2007 show"), -'application/vnd.openxmlformats-officedocument.presentationml.template': t('core', "PowerPoint 2007 presentation template"), -'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': t('core', "Excel 2007 spreadsheet"), -'application/vnd.openxmlformats-officedocument.spreadsheetml.template': t('core', "Excel 2007 spreadsheet template"), -'application/vnd.openxmlformats-officedocument.wordprocessingml.document': t('core', "Word 2007 document"), -'application/vnd.openxmlformats-officedocument.wordprocessingml.template': t('core', "Word 2007 document template"), -'application/vnd.visio': t('core', "Microsoft Visio document"), -'application/vnd.wordperfect': t('core', "WordPerfect document"), -'application/x-7z-compressed': t('core', "7-zip archive"), -'application/x-blender': t('core', "Blender scene"), -'application/x-bzip2': t('core', "Bzip2 archive"), -'application/x-deb': t('core', "Debian package"), -'application/x-fictionbook+xml': t('core', "FictionBook document"), -'application/x-font': t('core', "Unknown font"), -'application/x-krita': t('core', "Krita document"), -'application/x-mobipocket-ebook': t('core', "Mobipocket e-book"), -'application/x-msi': t('core', "Windows Installer package"), -'application/x-openvpn-profile': t('core', "OpenVPN connection profile"), -'application/x-perl': t('core', "Perl script"), -'application/x-php': t('core', "PHP script"), -'application/x-tar': t('core', "Tar archive"), -'application/xml': t('core', "XML document"), -'application/yaml': t('core', "YAML document"), -'application/zip': t('core', "Zip archive"), -'application/zstd': t('core', "Zstandard archive"), -'audio/aac': t('core', "AAC audio"), -'audio/flac': t('core', "FLAC audio"), -'audio/mp4': t('core', "MPEG-4 audio"), -'audio/mpeg': t('core', "MP3 audio"), -'audio/ogg': t('core', "Ogg audio"), -'audio/wav': t('core', "RIFF\/WAVe standard Audio"), -'audio/webm': t('core', "WebM audio"), -'audio/x-scpls': t('core', "MP3 ShoutCast playlist"), -'image/bmp': t('core', "Windows BMP image"), -'image/bpg': t('core', "Better Portable Graphics image"), -'image/emf': t('core', "EMF image"), -'image/gif': t('core', "GIF image"), -'image/heic': t('core', "HEIC image"), -'image/heif': t('core', "HEIF image"), -'image/jp2': t('core', "JPEG-2000 JP2 image"), -'image/jpeg': t('core', "JPEG image"), -'image/png': t('core', "PNG image"), -'image/svg+xml': t('core', "SVG image"), -'image/tga': t('core', "Truevision Targa image"), -'image/tiff': t('core', "TIFF image"), -'image/webp': t('core', "WebP image"), -'image/x-dcraw': t('core', "Digital raw image"), -'image/x-icon': t('core', "Windows Icon"), -'message/rfc822': t('core', "Email message"), -'text/calendar': t('core', "VCS\/ICS calendar"), -'text/css': t('core', "CSS stylesheet"), -'text/csv': t('core', "CSV document"), -'text/html': t('core', "HTML document"), -'text/markdown': t('core', "Markdown document"), -'text/org': t('core', "Org-mode file"), -'text/plain': t('core', "Plain text document"), -'text/rtf': t('core', "Rich Text document"), -'text/tab-separated-values': t('core', "TSV document"), -'text/vcard': t('core', "Electronic business card"), -'text/x-c++src': t('core', "C++ source code"), -'text/x-java-source': t('core', "Java source code"), -'text/x-ldif': t('core', "LDIF address book"), -'text/x-nfo': t('core', "NFO document"), -'text/x-php': t('core', "PHP source"), -'text/x-python': t('core', "Python script"), -'text/x-rst': t('core', "ReStructuredText document"), -'video/3gpp': t('core', "3GPP multimedia file"), -'video/MP2T': t('core', "MPEG video"), -'video/dv': t('core', "DV video"), -'video/mp2t': t('core', "MPEG-2 transport stream"), -'video/mp4': t('core', "MPEG-4 video"), -'video/mpeg': t('core', "MPEG video"), -'video/ogg': t('core', "Ogg video"), -'video/quicktime': t('core', "QuickTime video"), -'video/webm': t('core', "WebM video"), -'video/x-flv': t('core', "Flash video"), -'video/x-matroska': t('core', "Matroska video"), -'video/x-ms-wmv': t('core', "Windows Media video"), -'video/x-msvideo': t('core', "AVI video"), -'my-custom/mimetype': t('core', "Custom Document"), -}, -}; --- a/tests/data/integritycheck/mimetypeListModified/core/signature.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "hashes": { - "core\/js\/mimetypelist.js": "25635feead9858d82354005d59b36ae965323bc651b012910c0178bef8e90c9809abb2a6aa2425268d0f08a545567d2f2a10f00818435dc04ded44b7d9e61fad" - }, - "signature": "lrvTud+92i2m7XmQXbpT2LFSuFUk351DPjCRJ8hvPngKLB1yoSLQqUNOFTRN97fUKlkR2k2O8x\/HWAOG9pmB9H2m5bYrJ8\/mUbhbYSoVV5yC1rWtWH7A1uCvdj\/wDjuTR9NRKQXbIQa+v0DuwxyeRO6GiY9YRl+K5UYmP2jZscolECHCWKnHfLBoesrkt6VsJIqJTRk\/6l8\/MzMxmzcq8mWH5AdEvwieyEIXqmJevRi\/lVQG8h4l3yZukjtDHdFnbKdyOjezRQFK\/joJlxV3pNUVIsBl6ClGghS75c\/kO9AD+70BX6u6aRr9TPbkh84EmCyzceFaU+FzCd\/NC+fwPjDrJQlVh6EfwOEzkpNQP5KA5r8FVQZw51U9o1kJ3mdK04w0X2Eap0ZOz3RP2W2Vo2KJH7OitXA9TCNh9VoVY5jBJRYNeFi3NQewC6qZ2IXhXIEi8nyLjrsh71WfimvBl37s8LkDHODzKIQTCO14a7mDiiPZHry2BRHwF8G7znaEf6CCbygUZi4PK3VZT5Sv5OBzLfgkpTxRXIN6NxKljBRF8B2bz027J4L7A3s6ttpmBOXtj98MQekp27eWC4mb+ihUaxvpe6M3dsK7hCfUDl4VvPlVYo+vnCP8WNa1I2IcB38HN5Wb6E5JDhaXgCz6i3CcNXwghcrs1j7wXiDw8Rc=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -} \ No newline at end of file --- a/tests/data/integritycheck/root.crt +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE1DCCArygAwIBAgIUFgEnT7tUWNgEKfbMiRTOm3Z8figwDQYJKoZIhvcNAQEF -BQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMCAXDTE1MTEw -MzIxMDMzMloYDzk5OTkxMjMxMjM1OTU5WjAjMSEwHwYDVQQKDBhvd25DbG91ZCBD -b2RlIFNpZ25pbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCt -rYEwiwpuhzRNx1L2SntkL/zalA7OkwEcsBn4Ysw47nEdvp648AVohT7d+U7+DWix -O06xvlAJUhqYTXX7EG9n+mBnG+TIMq2zlei0Jj3uq1pEZE9elfGZael2uc8gRXMZ -YFmSlTvzexqvfK4B3DwoZaMaWJecEO9iyuUyzHMBpE8bHtGDOUGy/oTO9WASbtl9 -Rfk38VLLV6csCPwKjii6Q3YZ+AYU0YqLg22BwZlqlTexUjWAmVIqaoxmSEuKa41X -nuAIHfP65oiob86s4IvJXVr3r7NjdF2IJ/ZrwmjGKaWgSZKcoZBPLArZJeJiIQ4Y -KzLZATwba5dWswV0mbDj8eP2BG+02NVKxylOBbeoBESnahZeZ5nJ5XKZr+ErJAUW -b417fEnYaQNBlNnijjkqXaDipmTktUfnv4lm30sUAgho6I3Ga7gQrFPzKg9V4j2S -+LOTc1HmJOnR6Kfttx+yAHYLKtvV5yIMMpz+rZ2X5g/N2GdgleZj5VU9nmKzTPeG -x6V+dBaIkqNe8/AXaVnxt9KSb03Q/+CFjKTNDtEN5fNJuXS0+h+oop6nhpktM2i7 -gCpxeLNEaQaeoxR5093VN00oOJOYBvQoVGEDftEwdG6dWbTZsIykBF7aK+p8DMy9 -tCdc2GnGMEuFlUNA9ucv2Rv7IcuPdspnK31CZoMNKwIDAQABMA0GCSqGSIb3DQEB -BQUAA4ICAQBROWec0HOsnLPN40gkBQ62mNBUJgcAr5K2eMIEMSRFRD2ldEVOvmCO -u1Q62umy9tiSRiFQTcG1J9k0zlOjy/hfpBl2G1Zce0OoEeuNkH7c0W/idHSloWRZ -YlK3tVJD6DzY6s9VbO6e/ncecNsXkirkWp/cvMYquH2d4OmSl0/hW0VMdxOCLxkA -xbW+3Dh05u7tgKVRD67/GRvLtg+xHaOJqiOh3MpMaHy+6xT5Fd5K2QC0pcGtZuqF -EnnfdeUI/Dy76yQE8pBfjaUFf3TS1n1E6kun1Nkf0X4pvwi8W1goLsPu5sWDNNga -1RGYj0o5OdIo27qebfmu76WX0fNNd47VabtzNV+W7Msj0yeZg+hxAtAvs0ZEyJh+ -4biWsv+ALSlqz4sSdoOVGUEBdnkUrWN19lou62ix9vTmuCrVEA3TuZA3PR0+hqqQ -/A/DcmWwxWYKyaBgxwHc/nGo1qWrDh22P5Rp7++Zw3kOCY6QmeJkAiHFs6Crw4ub -HKVMw3fV5H9oiUFjadPZoCU51uXKX4YRqKxWJ5djlp4r1GCEQHyxngTsmH3komnw -kh4LsEQRqdhuT0A4sZN7CenMJfQiFqupL7RVSycJFQpgzwVFmOzjCVT4PT/W5ARv -9YtqEkvyoRTwErwuN/FIVvhWnIP/C69Z1/T/nXyj86P2G7PMgnchIQ== ------END CERTIFICATE----- --- a/tests/data/integritycheck/root.crt.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/integritycheck/root.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKgIBAAKCAgEAra2BMIsKboc0TcdS9kp7ZC/82pQOzpMBHLAZ+GLMOO5xHb6e -uPAFaIU+3flO/g1osTtOsb5QCVIamE11+xBvZ/pgZxvkyDKts5XotCY97qtaRGRP -XpXxmWnpdrnPIEVzGWBZkpU783sar3yuAdw8KGWjGliXnBDvYsrlMsxzAaRPGx7R -gzlBsv6EzvVgEm7ZfUX5N/FSy1enLAj8Co4oukN2GfgGFNGKi4NtgcGZapU3sVI1 -gJlSKmqMZkhLimuNV57gCB3z+uaIqG/OrOCLyV1a96+zY3RdiCf2a8JoximloEmS -nKGQTywK2SXiYiEOGCsy2QE8G2uXVrMFdJmw4/Hj9gRvtNjVSscpTgW3qAREp2oW -XmeZyeVyma/hKyQFFm+Ne3xJ2GkDQZTZ4o45Kl2g4qZk5LVH57+JZt9LFAIIaOiN -xmu4EKxT8yoPVeI9kvizk3NR5iTp0ein7bcfsgB2Cyrb1eciDDKc/q2dl+YPzdhn -YJXmY+VVPZ5is0z3hselfnQWiJKjXvPwF2lZ8bfSkm9N0P/ghYykzQ7RDeXzSbl0 -tPofqKKep4aZLTNou4AqcXizRGkGnqMUedPd1TdNKDiTmAb0KFRhA37RMHRunVm0 -2bCMpARe2ivqfAzMvbQnXNhpxjBLhZVDQPbnL9kb+yHLj3bKZyt9QmaDDSsCAwEA -AQKCAgEAnR6/JkRTPqTQW6D8W9YMBRoovTF+p8F0GxjxlbUDnmmQKeGeRB7YNbN9 -qWD25n0I/nVx/vj1/UiqyKgjGOvIbZ+kAQPKGJdIb5Qp/nguRTH9qqu45g/ujuSz -EfaM/Fv4AbgZsLOTlfUDskiwPvyX68/vG1GUbtsfRhfZ+/fb/1s/OYDK99Ufq6f4 -TCbOMD7aQSvBh6upRE5a7Up/gakUDVYkjN/F2KWsmgRfWCjl+vddd+ywfFO4cqkL -tSioNmSQbPlNIeq/I3fVn9PufJVzwMrVFgh82HeYein1E43ALa3VqcmFem/rVsS4 -V7SfNjlDP/gsuwcT8paGRigUwmScEkyXYJ7oSNEN8Xe4kWWakfwGpa1HmaYPdHx+ -O+G8coHp2kcc/tUZma+Ffo3tNRMfGcpowG+PetbCh4uSNPo5U5U5W53+vxgyZHFi -lY7gc5HVi5JBSxmWwTa3RDcz1dByWS83NdObrxAntp3W9g8tVj2N9gfUnJS+wp3d -m8HvO3bzIUuhQcWAtcnXqRAGsl+uc/xgdF2membV/yOHdn2Z1zXKEnjC1T6cEV06 -qocwgp0/EAMhzL4FP8xA9MvztZR7bJOyUihTIadG7Zb18XBea7yA73KvfYGrzjsg -lmqV4CbGuo0If8SJD03xXthMqi5cBXu4V13sYAbNO8pPDjhfPIkCggEBAOUjScLN -RoaoOBO3JUWoYauZrf9sa72/zzTOg+c8unQquExMjnsyEVJPD4+TpKGN5/sewwb5 -zksI5c1njOrdFdSKoKlgB/6owv2v70Uqe+HeVA+m5Qy6JSgMAAXZgJ5PtaCCu+Xq -MOqTd/xInfP1oCecJF9UvqeQJBJN16fMXBib9J5+sLCRJnFNywfmlfAiIOa4MIoY -Tz7LW+r4Zot0x2o6KcCT8AB3LJIz2seHmTf9Jjk17kw7pFJWtZf6eA1lgDlzadKJ -iHgrdSbchDZv410B0df/ZV+gZ7PMgK/cbo3H3JOXPV1dxmTONTIIvxYj1LmOdMP4 -p5oeM367LUWQN20CggEBAMIJykbsm7IVthkMeCm/VhG/wd44iB0g5+us8q8pUO1+ -KfTzytHPjMack5a/XaEApRRiJpCAJNcBpUNbJqaOZQ1QqnX6ozlSR2aUi27noVz6 -/heG+3qZElejYfLMpJAF1crjdFJIqKucizc+E6AZ+nOixpJcDABYWby9QQw4O1Pr -Ii3E7xyFvqizYzE7P/HG6P0gMxta9aKHKGhGHX51Uc+BUfF0T0ymXVvWLlfMg+HI -SBPqMphW+YR0xwK21A2LvqYBM2MyaThY2wyVSZs9akjiAukRwX8l7lA5A/try0Q0 -1POyN6oo1H7iSHuraqXfDjcSpK5M/QOpXMuoPyTI//cCggEBAKCn00mwH6i+PUMl -gA6M9p4YTDTwUcJiv+cofLcejyRv53QnoSajfh2VrTVfsWhMVMBvWxKDB674eBdC -aT0q8elpoSfgWvqkXML+HecC2IUPGyU2QRZhVTf04fc3/sQA4zm9L/0N7GosJ05N -o+Gu8DGVerMUefCGUaQ7y96snE3s2uBdt4i03J1Ii/foJmyNoT/jGLVaQgWnE7V1 -oIBayo6iZS/PCdFpvWhszxJi8nydE7W9KG1uy9GnVf9O7+mEpxig4StqnrKS1br/ -lfuNC37kjbrCKNOZZdxcoEWtah4iaXdZ7P6Ph1CafBWuqDvft4C6bwgQSYL3ded/ -WUiSyykCggEAePeThF3TvtUsPjd43kXriYsreLdzm/08uL+MWEkAq96gl5Y5Fk43 -LEbG+A77dvko8Skzc5h/3w6mkfRMhz90njVw37ZOddjmrHvk5VJAVfAf4lkDhG3T -cpFn6e9MlIEexKrChN3JUZt5awonQAOSEO8krm/2B20NHM47tDuGOQ34s+H3U6fJ -sfCL4VBX0Ao6jDu7wM0XH6j1NvSnRIQtaZjslgP1wApjX3KKV7Anc+XhkZDK1BA8 -5CfNPdLvJja9t04+VBREZp12ikSzq7VBAojsWZL5N6RVCuxQoDiWc0IglIDBlTJ5 -L1Uw7PBzv07s1MappgRXJCY8tLaCDxPEBwKCAQEA1Rxj0YpQ1D+8oFQ3ck0b4ODy -DCSCNe6Xw+Wzv5BqGVsQCmW14uVBT+S/qij5dTrGIPXudLSHtdjs9tmoDqnkxY/o -Nj5rx6J2brnNLTD7yo/j74kgaRwSuHafpGX1C0zge0rgIgVu8DhWHan7F/38K0cO -T1jYJbYcTAvEcO1XXXItnaHR1ETY4p0G5FvUTLWaNQnQTU3r5ZaCkjXN9UBA2k6U -6j6A9/JXIlNPFNoB103iAD5jvHa96AlivHSyp4UTlsiwAxec316CY2zdWrVWQCF4 -J8DspH3ygeLtvKOveEYsiaiNuJLKREC2GIRUm6O4C/RdP0s0QAODpk+yGCfukg== ------END RSA PRIVATE KEY----- --- a/tests/data/integritycheck/root.key.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2015 ownCloud, Inc. -SPDX-License-Identifier: AGPL-3.0-only --- a/tests/data/l10n/cs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "translations" : { - "_%n window_::_%n windows_" : ["%n okno", "%n okna", "%n oken"] - }, - "pluralForm" : "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" -} --- a/tests/data/l10n/de.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "translations" : { - "_%n file_::_%n files_": ["%n Datei", "%n Dateien"], - "Ordered placeholders one %s two %s": "Placeholder one %s two %s", - "Reordered placeholders one %s two %s": "Placeholder two %2$s one %1$s", - "Reordered placeholders one %1$s two %2$s": "Placeholder two %2$s one %1$s" - }, - "pluralForm" : "nplurals=2; plural=(n != 1);" -} --- a/tests/data/l10n/ru.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "translations" : { - "_%n file_::_%n files_" : ["%n файл", "%n файла", "%n файлов"] - }, - "pluralForm" : "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" -} --- a/tests/data/lorem-big.txt +++ /dev/null @@ -1,29 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse id hendrerit felis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed dictum dolor nec lobortis sagittis. Aliquam blandit lobortis ullamcorper. Donec malesuada ante ante, vel cursus purus suscipit ut. Sed a eros vitae lorem tristique hendrerit a a odio. Cras interdum mi lectus, sed molestie mauris tempus sit amet. Praesent auctor neque venenatis diam tincidunt sodales. Maecenas tincidunt ligula et ipsum dignissim, et molestie dui hendrerit. Nunc lobortis mauris vel tempor tristique. Sed consectetur bibendum nunc. Pellentesque augue velit, interdum ac posuere eu, eleifend et massa. Vestibulum ac mi eu mi viverra iaculis. Sed consequat leo adipiscing dui varius, sagittis vestibulum massa vehicula. - -Praesent imperdiet erat vitae dui placerat, sit amet vulputate tortor viverra. In laoreet elit lorem, at luctus est tempor ac. Nam elementum scelerisque nisl vitae accumsan. Curabitur cursus rutrum est, ut adipiscing ante elementum ut. Ut lectus sem, luctus ut massa non, dignissim vestibulum ipsum. Suspendisse non convallis enim, aliquam adipiscing velit. Donec pharetra non turpis hendrerit porttitor. Aliquam erat volutpat. - -Cras eget arcu eu dolor faucibus accumsan non eget orci. Integer quis quam tincidunt, faucibus neque in, imperdiet lacus. Aenean dui turpis, lacinia quis ligula non, semper adipiscing tellus. Curabitur vitae dui quis nisl malesuada commodo. Nunc eleifend metus enim, eu aliquet dui semper sit amet. Sed a odio sapien. Suspendisse vitae ante id sapien semper accumsan. Sed vestibulum erat quis laoreet pellentesque. Nullam placerat ligula eu odio faucibus, eget dictum orci tristique. Quisque sit amet pulvinar velit, lacinia dictum sem. - -Phasellus id sagittis lacus. Vivamus facilisis convallis metus, sit amet ultricies purus gravida quis. Vivamus eget egestas arcu, a euismod risus. Fusce metus arcu, molestie ut lacinia at, commodo eu nibh. In leo tortor, feugiat aliquet semper in, malesuada ac nulla. Fusce tempor ultricies blandit. Pellentesque et lorem quam. Suspendisse eros eros, mattis ut porttitor vitae, fermentum eget augue. Aliquam vitae justo sed est dictum lobortis. Cras lacinia commodo ligula sed ornare. Donec ut eros semper, sodales tellus quis, cursus quam. Quisque elementum ullamcorper pellentesque. Integer in rutrum ante, vel condimentum metus. Aliquam erat volutpat. - -Quisque convallis tempus eros, sit amet mattis ipsum eleifend in. Mauris volutpat, urna ut commodo tempus, est quam scelerisque erat, eget consequat nulla arcu id eros. Nam elementum nibh quis tincidunt adipiscing. Fusce tristique pretium mollis. Nulla quis ornare felis, vel lacinia diam. In et dui et mauris vestibulum commodo. In hac habitasse platea dictumst. Vestibulum viverra pellentesque tortor, tempor vehicula orci. Praesent rutrum turpis ipsum, ac commodo nisi vehicula ac. - -Nulla gravida ultrices mauris. Ut congue purus nec dolor euismod, vitae auctor elit condimentum. Nullam elementum velit lectus, fringilla pharetra sem hendrerit sit amet. Cras quis lobortis nisi, id interdum massa. Proin eget porttitor sapien. Vivamus quam odio, consectetur at rhoncus vel, posuere in nisi. Donec hendrerit urna ac massa tristique, pellentesque consequat orci dapibus. Mauris mi erat, aliquet id adipiscing ultricies, ultrices nec lacus. Proin imperdiet elit magna, ut suscipit metus faucibus eget. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at leo lorem. Donec mi nibh, blandit ut mi quis, dictum adipiscing ante. Curabitur sodales blandit odio id viverra. Fusce sagittis rutrum nibh. Mauris risus lacus, placerat at dictum in, varius volutpat quam. - -Ut accumsan tincidunt dui, non semper lorem sodales nec. Curabitur pellentesque velit nec erat ultrices, in convallis diam suscipit. Curabitur pellentesque convallis mi. Phasellus iaculis orci quis nisi semper auctor. Vestibulum eleifend tortor quis odio feugiat, pulvinar congue turpis blandit. Sed viverra diam risus, vel mattis orci dignissim ut. Cras pellentesque dapibus dolor. Morbi tempor eleifend magna. Ut sodales ut sapien sed elementum. In hac habitasse platea dictumst. - -Nullam eget sagittis tellus. Donec a tellus id ligula viverra pellentesque. Pellentesque nulla turpis, pretium vel sem vel, placerat ultricies diam. Donec felis nibh, rhoncus id ornare at, lobortis vel massa. Nullam eu luctus ipsum. Mauris consequat dictum aliquam. Suspendisse ullamcorper aliquam mauris, viverra tincidunt augue suscipit at. Sed luctus scelerisque justo ut semper. Phasellus massa mauris, molestie at nulla eget, dictum ultricies nisi. Aliquam rhoncus nisl sed urna egestas, vel luctus lectus euismod. Mauris sagittis dapibus leo. Donec vitae dignissim risus, ac ultricies augue. Ut vulputate tortor sed dui consectetur placerat. Nullam viverra non diam vel dignissim. - -Suspendisse potenti. Integer facilisis neque vitae euismod adipiscing. Aenean dictum leo commodo dui sodales, sit amet volutpat mauris gravida. Proin tempus convallis eros at consectetur. Proin nisl purus, dictum vel euismod ut, imperdiet quis est. Etiam sollicitudin lobortis neque eget pulvinar. Etiam venenatis vel sem ut posuere. Aliquam consectetur rhoncus facilisis. Morbi a viverra orci. - -Praesent ut vehicula orci, vel convallis risus. Suspendisse consectetur varius interdum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin vel sodales enim. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur suscipit tristique justo vel dapibus. Sed tincidunt mattis massa a auctor. Donec in tincidunt elit. Curabitur interdum neque at ante fringilla tempus. In hac habitasse platea dictumst. Vivamus luctus ligula ut nisl fermentum egestas. Praesent pulvinar accumsan neque. Sed nec leo sit amet arcu vehicula vehicula non ac diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; - -Morbi vel arcu quis neque dictum ullamcorper. Pellentesque condimentum consequat lacinia. Vestibulum eleifend placerat erat, eu hendrerit dui pulvinar eget. Vestibulum accumsan, lectus id vehicula fringilla, erat eros dictum massa, ut tristique libero mi eu ante. In at ante nunc. Praesent sodales ullamcorper porta. Curabitur egestas odio elit. Praesent et libero malesuada, venenatis lorem vitae, eleifend nisl. Donec dapibus euismod turpis, nec porttitor turpis dapibus ac. Fusce iaculis lacus eget dictum aliquam. Sed dictum eu enim ac posuere. Ut accumsan, ipsum et laoreet consectetur, ipsum quam dapibus diam, ac molestie ligula dui id massa. Nulla aliquam mauris congue nibh vestibulum imperdiet. - -Aenean ultricies, orci vel consectetur suscipit, ante nisi eleifend est, vitae suscipit risus erat dictum dui. Ut nisi diam, tristique sed nisl nec, aliquam gravida orci. Vestibulum in molestie sem. Ut eu molestie mi. Suspendisse potenti. Nunc sagittis lorem ut est vehicula, vitae imperdiet leo rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec pharetra urna pretium, faucibus orci cursus, sodales purus. Duis iaculis dignissim augue, non aliquet ligula elementum quis. Sed tempus a nisi et faucibus. Donec mattis ligula ac sem aliquam, sed sodales est ullamcorper. Cras vel orci est. Integer eget ultricies nisi. - -Quisque rhoncus, nisl vel auctor consectetur, sapien augue iaculis urna, quis suscipit arcu dolor ac est. Fusce sit amet quam lacinia, malesuada lorem vitae, dapibus justo. Mauris eget massa nec dolor volutpat tempor ut eu ante. Donec risus mi, aliquam at euismod vulputate, ullamcorper non leo. Etiam ac elit velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus sollicitudin enim sed felis pharetra, a convallis mi venenatis. Donec convallis non velit venenatis suscipit. Sed massa risus, dictum quis aliquam ut, placerat quis arcu. Donec at nisi neque. Nullam porta et mi vel ultricies. Donec vel aliquam sem. Mauris varius, ipsum interdum mattis rhoncus, nunc nisl vehicula tortor, quis condimentum nibh nisl vel ante. Sed vel pretium dui. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce pulvinar molestie neque, quis luctus ante. - -Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec ac ipsum egestas, luctus urna aliquet, fringilla tortor. Cras eleifend sed tellus tempor congue. Integer venenatis magna dignissim pharetra cursus. Nulla volutpat, sem sit amet malesuada laoreet, odio libero consectetur orci, sit amet pellentesque neque ligula quis libero. Morbi nec interdum metus, in imperdiet justo. Fusce euismod egestas nulla et elementum. Etiam aliquam lectus quis eros semper, sed dapibus velit ultrices. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -Proin sed elit vel massa placerat mattis et vel mauris. Aliquam accumsan ante ligula, at accumsan enim pellentesque sit amet. Sed eget quam vel nunc molestie iaculis vitae nec massa. Vivamus sed accumsan libero. Duis pretium quam at blandit sollicitudin. Nullam sodales sit amet elit ac adipiscing. Ut ullamcorper dignissim sapien. Nulla facilisi. Fusce sed consequat lectus, et tempus ipsum. Aliquam vestibulum lectus vitae quam mollis, et adipiscing nisl lobortis --- a/tests/data/lorem.txt +++ /dev/null @@ -1,4 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file --- a/tests/data/scss/styles-error.scss +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -body { error } --- a/tests/data/scss/styles-success.scss +++ /dev/null @@ -1,5 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -body { background-color: $color-primary; } --- a/tests/data/setUploadLimit/htaccess +++ /dev/null @@ -1,58 +0,0 @@ -# Version: 8.2.0 - - - - SetEnvIfNoCase ^Authorization$ "(.+)" XAUTHORIZATION=$1 - RequestHeader set XAuthorization %{XAUTHORIZATION}e env=XAUTHORIZATION - - - - - # Add security and privacy related headers - Header set X-Content-Type-Options "nosniff" - Header set X-Robots-Tag "noindex, nofollow" - Header set X-Frame-Options "SAMEORIGIN" - SetEnv modHeadersAvailable true - - - # Add cache control for CSS and JS files - - Header set Cache-Control "max-age=7200, public" - - - -php_value upload_max_filesize 513M -php_value post_max_size 513M -php_value memory_limit 512M -php_value default_charset 'UTF-8' -php_value output_buffering off - - SetEnv htaccessWorking true - - - -RewriteEngine on -RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}] -RewriteRule ^\.well-known/host-meta /public.php?service=host-meta [QSA,L] -RewriteRule ^\.well-known/host-meta\.json /public.php?service=host-meta-json [QSA,L] -RewriteRule ^\.well-known/webfinger /public.php?service=webfinger [QSA,L] -RewriteRule ^\.well-known/carddav /remote.php/carddav/ [R=301,L] -RewriteRule ^\.well-known/caldav /remote.php/caldav/ [R=301,L] -RewriteRule ^apps/calendar/caldav\.php remote.php/caldav/ [QSA,L] -RewriteRule ^apps/contacts/carddav\.php remote.php/carddav/ [QSA,L] -RewriteRule ^remote/(.*) remote.php [QSA,L] -RewriteRule ^(build|tests|config|lib|3rdparty|templates)/.* - [R=404,L] -RewriteRule ^(\.|autotest|occ|issue|indie|db_|console).* - [R=404,L] - - -AddType image/svg+xml svg svgz -AddEncoding gzip svgz - - -DirectoryIndex index.php index.html - -AddDefaultCharset utf-8 -Options -Indexes - - ModPagespeed Off - --- a/tests/data/setUploadLimit/user.ini +++ /dev/null @@ -1,5 +0,0 @@ -upload_max_filesize=513M -post_max_size=513M -memory_limit=512M -default_charset='UTF-8' -output_buffering=off --- a/tests/data/svg/files-app-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/svg/mixed-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/svg/mixed-source.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/svg/rect-black.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/svg/rect-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/svg/settings-admin-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - --- a/tests/data/testimage.eps +++ /dev/null @@ -1,175 +0,0 @@ -%!PS-Adobe-3.0 EPSF-3.0 -%%Creator: cairo 1.14.1 (http://cairographics.org) -%%CreationDate: Fri May 15 16:27:38 2015 -%%Pages: 1 -%%DocumentData: Clean7Bit -%%LanguageLevel: 2 -%%BoundingBox: 0 -1 2400 1706 -%%EndComments -%%BeginProlog -save -50 dict begin -/q { gsave } bind def -/Q { grestore } bind def -/cm { 6 array astore concat } bind def -/w { setlinewidth } bind def -/J { setlinecap } bind def -/j { setlinejoin } bind def -/M { setmiterlimit } bind def -/d { setdash } bind def -/m { moveto } bind def -/l { lineto } bind def -/c { curveto } bind def -/h { closepath } bind def -/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto - 0 exch rlineto 0 rlineto closepath } bind def -/S { stroke } bind def -/f { fill } bind def -/f* { eofill } bind def -/n { newpath } bind def -/W { clip } bind def -/W* { eoclip } bind def -/BT { } bind def -/ET { } bind def -/pdfmark where { pop globaldict /?pdfmark /exec load put } - { globaldict begin /?pdfmark /pop load def /pdfmark - /cleartomark load def end } ifelse -/BDC { mark 3 1 roll /BDC pdfmark } bind def -/EMC { mark /EMC pdfmark } bind def -/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def -/Tj { show currentpoint cairo_store_point } bind def -/TJ { - { - dup - type /stringtype eq - { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse - } forall - currentpoint cairo_store_point -} bind def -/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore - cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def -/Tf { pop /cairo_font exch def /cairo_font_matrix where - { pop cairo_selectfont } if } bind def -/Td { matrix translate cairo_font_matrix matrix concatmatrix dup - /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point - /cairo_font where { pop cairo_selectfont } if } bind def -/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def - cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def -/g { setgray } bind def -/rg { setrgbcolor } bind def -/d1 { setcachedevice } bind def -%%EndProlog -%%BeginSetup -%%EndSetup -%%Page: 1 1 -%%BeginPageSetup -%%PageBoundingBox: 0 -1 2400 1706 -%%EndPageSetup -q 0 -1 2400 1707 rectclip q -0.113725 0.176471 0.266667 rg -0 1705.263 2400 -1705.262 re f -1 g -1348.539 1275.525 m 1269.973 1275.525 1206.5 1212.052 1206.5 1133.486 c - 1206.5 1101.099 1217.285 1071.294 1235.465 1047.427 c 1274.906 1093.076 - 1333.117 1122.064 1398.113 1122.064 c 1429.914 1122.064 1460.066 1114.986 - 1487.238 1102.572 c 1489.438 1112.517 1490.578 1122.865 1490.578 1133.486 - c 1490.578 1212.052 1427.105 1275.525 1348.539 1275.525 c h -1163.055 1209.794 m 1122.137 1209.794 1089.246 1176.626 1089.246 1135.712 - c 1089.246 1122.466 1092.664 1109.951 1098.719 1099.228 c 1123.406 1113.158 - 1151.949 1121.232 1182.27 1121.232 c 1185.195 1121.232 1188.016 1121.111 - 1190.902 1120.951 c 1190.578 1125.099 1190.348 1129.255 1190.348 1133.486 - c 1190.348 1156.275 1195.297 1177.955 1203.992 1197.541 c 1192.285 1205.396 - 1178.254 1209.794 1163.055 1209.794 c h -1513.418 1158.83 m 1510.395 1158.83 1507.48 1158.458 1504.504 1158.271 -c 1505.793 1150.154 1506.734 1141.958 1506.734 1133.486 c 1506.734 1120.294 - 1505.055 1107.568 1501.996 1095.33 c 1537.844 1075.494 1567.609 1045.81 - 1587.223 1009.826 c 1607.563 1020.416 1630.258 1027.15 1654.344 1028.767 - c 1648.137 1101.56 1587.828 1158.83 1513.418 1158.83 c h -1398.113 1105.912 m 1288.176 1105.912 1199.258 1017.001 1199.258 907.056 - c 1199.258 797.123 1288.172 708.201 1398.113 708.201 c 1508.055 708.201 - 1596.969 797.126 1596.969 907.056 c 1596.969 1017.001 1508.051 1105.912 - 1398.113 1105.912 c h -1182.27 1105.076 m 1096.98 1105.076 1027.977 1036.072 1027.977 950.783 -c 1027.977 900.576 1051.898 856.126 1088.969 827.962 c 1104.598 858.103 -1135.988 878.65 1172.242 878.65 c 1176.625 878.65 1180.832 878.119 1185.055 - 877.537 c 1183.73 887.181 1183.105 897.048 1183.105 907.056 c 1183.105 -954.908 1198.68 999.154 1225.16 1034.892 c 1209.309 1054.728 1197.965 1078.556 - 1193.133 1104.521 c 1189.547 1104.767 1185.918 1105.076 1182.27 1105.076 - c h -1665.762 1013.169 m 1639.863 1013.169 1615.602 1006.556 1594.184 995.345 - c 1606.348 968.404 1613.121 938.513 1613.121 907.056 c 1613.121 848.158 - 1589.441 794.677 1551.016 755.826 c 1579.246 724.486 1620.203 704.861 1665.762 - 704.861 c 1751.051 704.861 1820.055 773.865 1820.055 859.154 c 1820.055 - 944.443 1751.051 1013.169 1665.762 1013.169 c h -1012.938 992.837 m 934.367 992.837 870.617 929.646 870.617 851.076 c 870.617 - 772.509 934.367 708.759 1012.938 708.759 c 1042.84 708.759 1070.543 718.076 - 1093.426 733.826 c 1083.969 748.517 1078.387 766.083 1078.387 784.791 c - 1078.387 794.501 1079.82 803.83 1082.563 812.642 c 1039.723 843.603 1011.824 - 893.986 1011.824 950.783 c 1011.824 965.212 1013.723 979.169 1017.113 992.56 - c 1015.707 992.599 1014.355 992.837 1012.938 992.837 c h -1848.184 870.849 m 1843.992 870.849 1839.938 870.373 1835.93 869.736 c -1836.152 866.193 1836.207 862.751 1836.207 859.154 c 1836.207 813.853 1818.332 - 772.736 1789.418 742.181 c 1803.645 725.638 1824.539 715.166 1848.184 715.166 - c 1891.254 715.166 1926.168 749.798 1926.168 792.869 c 1926.168 835.939 - 1891.254 870.849 1848.184 870.849 c h -1172.242 862.498 m 1129.176 862.498 1094.539 827.861 1094.539 784.791 c - 1094.539 741.724 1129.176 706.81 1172.242 706.81 c 1205.258 706.81 1233.277 - 727.408 1244.656 756.384 c 1216.891 784.662 1196.938 820.74 1188.117 860.826 - c 1182.961 861.892 1177.723 862.498 1172.242 862.498 c h -2026.43 797.044 m 2026.43 643.029 l 1944.828 643.029 l 1891.484 643.029 - 1848.184 599.455 1848.184 546.111 c 1848.184 492.775 1891.484 449.466 1944.828 - 449.466 c 1976.578 449.466 l 1992.227 449.466 2007.852 457.068 2020.305 - 468.685 c 2032.754 480.302 2042.254 496.392 2042.582 514.361 c 2043.145 - 545.197 2042.582 634.955 2042.582 634.955 c 2042.582 797.044 l h -1280.305 727.419 m 1280.305 514.361 l 1280.305 478.552 1309.668 449.466 - 1345.477 449.466 c 1345.477 465.623 l 1318.383 465.623 1296.457 487.263 - 1296.457 514.361 c 1296.457 717.392 l 1290.836 720.416 1285.617 723.923 - 1280.305 727.419 c h -1118.77 707.646 m 1090.16 680.345 1072.258 641.908 1072.258 599.306 c 1072.258 - 516.642 1139.438 449.466 1222.098 449.466 c 1222.098 465.623 l 1148.152 - 465.623 1088.414 525.357 1088.414 599.306 c 1088.414 639.076 1105.773 674.822 - 1133.254 699.291 c 1128.117 701.654 1123.359 704.427 1118.77 707.646 c -h -928.547 643.587 m 875.203 643.587 831.906 600.291 831.906 546.947 c 831.906 - 457.544 l 848.059 457.544 l 848.059 546.947 l 848.059 591.572 883.922 627.712 - 928.547 627.712 c 973.184 627.712 1009.316 591.572 1009.316 546.947 c 1009.316 - 457.544 l 1025.469 457.544 l 1025.469 546.947 l 1025.469 600.291 981.898 - 643.587 928.547 643.587 c h -1470.805 643.029 m 1417.457 643.029 1374.164 599.455 1374.164 546.111 c - 1374.164 492.763 1417.457 449.189 1470.805 449.189 c 1524.152 449.189 1567.727 - 492.763 1567.727 546.111 c 1567.727 599.455 1524.152 643.029 1470.805 643.029 - c h -392.418 642.751 m 339.078 642.751 295.496 599.451 295.496 546.111 c 295.496 - 492.767 339.078 449.466 392.418 449.466 c 445.762 449.466 489.063 492.767 - 489.063 546.111 c 489.063 599.451 445.762 642.751 392.418 642.751 c h -537.523 635.513 m 537.523 514.361 l 537.523 478.552 566.613 449.466 602.414 - 449.466 c 626.816 449.466 648.344 462.958 659.508 482.888 c 670.59 462.958 - 691.93 449.466 716.324 449.466 c 752.133 449.466 781.496 478.552 781.496 - 514.361 c 781.496 635.513 l 765.344 635.513 l 765.344 514.361 l 765.344 - 487.267 743.418 465.623 716.324 465.623 c 689.23 465.623 667.586 487.267 - 667.586 514.361 c 667.586 635.513 l 651.434 635.513 l 651.434 514.361 l - 651.434 487.267 629.52 465.623 602.414 465.623 c 575.328 465.623 553.676 - 487.267 553.676 514.361 c 553.676 635.513 l h -1610.059 635.513 m 1610.059 546.111 l 1610.059 492.775 1653.359 449.466 - 1706.703 449.466 c 1760.047 449.466 1803.621 492.775 1803.621 546.111 c - 1803.621 635.513 l 1787.469 635.513 l 1787.469 546.111 l 1787.469 501.494 - 1751.328 465.623 1706.703 465.623 c 1662.074 465.623 1626.215 501.49 1626.215 - 546.111 c 1626.215 635.513 l h -1470.805 626.876 m 1515.438 626.876 1551.574 590.74 1551.574 546.111 c -1551.574 501.478 1515.438 465.341 1470.805 465.341 c 1426.176 465.341 1390.316 - 501.478 1390.316 546.111 c 1390.316 590.74 1426.176 626.876 1470.805 626.876 - c h -1944.828 626.876 m 2026.43 626.876 l 2026.492 616.205 2026.938 542.685 -2026.43 514.638 c 2026.203 502.158 2019.355 489.634 2009.441 480.38 c 1999.527 - 471.13 1986.695 465.623 1976.578 465.623 c 1944.828 465.623 l 1900.199 -465.623 1864.34 501.49 1864.34 546.111 c 1864.34 590.74 1900.199 626.876 - 1944.828 626.876 c h -392.418 626.599 m 437.047 626.599 472.906 590.736 472.906 546.111 c 472.906 - 501.482 437.047 465.623 392.418 465.623 c 347.793 465.623 311.652 501.482 - 311.652 546.111 c 311.652 590.736 347.793 626.599 392.418 626.599 c h -392.418 626.599 m f -Q Q -showpage -%%Trailer -end restore -%%EOF --- a/tests/data/testimagelarge.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file --- a/tests/databases-all-config.php +++ /dev/null @@ -1,16 +0,0 @@ - [ - 'sqlite', - 'mysql', - 'pgsql', - 'oci', - ], -]; --- a/tests/docker/mariadb/oc.cnf +++ /dev/null @@ -1,5 +0,0 @@ - -[mysqld] - -innodb_buffer_pool_size = 512M -innodb_flush_log_at_trx_commit = 2 --- a/tests/docker/mysqlmb4.config.php +++ /dev/null @@ -1,11 +0,0 @@ - true, -]; --- a/tests/docker/mysqlmb4/mb4.cnf +++ /dev/null @@ -1,8 +0,0 @@ - -[mysqld] -innodb_large_prefix=true -innodb_file_format=barracuda -innodb_file_per_table=true - -innodb_buffer_pool_size = 512M -innodb_flush_log_at_trx_commit = 2 --- a/tests/enable_all.php +++ /dev/null @@ -1,31 +0,0 @@ -installApp($app); - $appManager->enableApp($app); - echo "Enabled application {$app}\n"; -} - -foreach (new \DirectoryIterator(__DIR__ . '/../apps/') as $file) { - if ($file->isDot()) { - continue; - } - if (!file_exists($file->getPathname() . '/.git')) { - enableApp($file->getFilename()); - } -} --- a/tests/http-analyzer.php +++ /dev/null @@ -1,80 +0,0 @@ - $requests */ -$requests = []; -foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { - $request = json_decode($line, true); - if (is_array($request)) { - $requests[] = $request; - } -} - -if ($requests === []) { - echo "No outgoing HTTP requests were recorded.\n"; - exit(0); -} - -$totalDuration = array_sum(array_column($requests, 'duration')); -printf("%d requests, %.1fs total\n", count($requests), $totalDuration); - -/** @param callable(array): string $key */ -function group(array $requests, callable $key): array { - $groups = []; - foreach ($requests as $request) { - $name = $key($request); - $groups[$name] ??= ['duration' => 0.0, 'requests' => 0]; - $groups[$name]['duration'] += $request['duration']; - $groups[$name]['requests']++; - } - uasort($groups, static fn (array $a, array $b): int => $b['duration'] <=> $a['duration']); - return $groups; -} - -foreach ([ - 'host' => static fn (array $r): string => parse_url($r['uri'], PHP_URL_HOST) ?: '(unparsed)', - 'test' => static fn (array $r): string => $r['test'], -] as $label => $key) { - $groups = group($requests, $key); - printf("\nRequests by %s\n", $label); - printf(" %9s %9s %s\n", 'sum', 'requests', $label); - foreach (array_slice($groups, 0, $topCount, true) as $name => $stats) { - printf(" %8.2fs %9d %s\n", $stats['duration'], $stats['requests'], $name); - } -} - -usort($requests, static fn (array $a, array $b): int => $b['duration'] <=> $a['duration']); -printf("\nTop %d slowest requests\n", $topCount); -foreach (array_slice($requests, 0, $topCount) as $request) { - printf( - " %8.2fs %-6s %-4s %s\n %s\n", - $request['duration'], - $request['method'], - $request['outcome'], - $request['uri'], - $request['test'], - ); -} --- a/tests/junit-analyzer.php +++ /dev/null @@ -1,139 +0,0 @@ -open($file)) { - fwrite(STDERR, "cannot open $file\n"); - exit(1); -} - -/** @var list $tests in execution order */ -$tests = []; -while ($reader->read()) { - if ($reader->nodeType !== XMLReader::ELEMENT || $reader->name !== 'testcase') { - continue; - } - $tests[] = [ - 'class' => $reader->getAttribute('class') ?: '(none)', - 'name' => (string)$reader->getAttribute('name'), - 'duration' => (float)$reader->getAttribute('time'), - ]; -} -$reader->close(); - -$testCount = count($tests); -if ($testCount === 0) { - $error = libxml_get_errors()[0] ?? null; - fwrite(STDERR, "no testcase elements found in $file" - . ($error !== null ? ': ' . trim($error->message) : '') . "\n"); - exit(1); -} - -$totalDuration = array_sum(array_column($tests, 'duration')); -printf("%d tests, %.1fs total (%.1f min)\n\n", $testCount, $totalDuration, $totalDuration / 60); - -if ($totalDuration <= 0) { - fwrite(STDERR, "no timing data to rank\n"); - exit(0); -} - -$chunks = array_chunk($tests, (int)ceil($testCount / BUCKETS)); -$buckets = count($chunks); - -printf("Execution order, %d buckets (are later tests slower?)\n", $buckets); -echo " bucket tests sum(s) mean(ms) median(ms) max(s) cum%\n"; - -$durationSoFar = 0.0; -foreach ($chunks as $bucket => $chunk) { - $durations = array_column($chunk, 'duration'); - sort($durations); - $inBucket = count($durations); - $bucketDuration = array_sum($durations); - $durationSoFar += $bucketDuration; - - printf( - " %3d-%3d%% %7d %9.1f %10.2f %12.2f %9.2f %5.1f%%\n", - $bucket * 100 / $buckets, - ($bucket + 1) * 100 / $buckets, - $inBucket, - $bucketDuration, - $bucketDuration / $inBucket * 1000, - $durations[intdiv($inBucket, 2)] * 1000, - max($durations), - $durationSoFar / $totalDuration * 100, - ); -} - -$slowestFirst = $tests; -usort($slowestFirst, static fn (array $a, array $b): int => $b['duration'] <=> $a['duration']); - -printf("\nTop %d slowest tests\n", $topCount); -foreach (array_slice($slowestFirst, 0, $topCount) as $test) { - printf(" %8.2fs %s::%s\n", $test['duration'], $test['class'], $test['name']); -} - -/** @var array $classes */ -$classes = []; -foreach ($tests as $test) { - $classes[$test['class']] ??= ['duration' => 0.0, 'tests' => 0]; - $classes[$test['class']]['duration'] += $test['duration']; - $classes[$test['class']]['tests']++; -} -uasort($classes, static fn (array $a, array $b): int => $b['duration'] <=> $a['duration']); - -printf("\nTop %d slowest classes (sum of its tests)\n", $topCount); -printf(" %9s %7s %10s %s\n", 'sum', 'tests', 'mean(ms)', 'class'); -foreach (array_slice($classes, 0, $topCount, true) as $class => $stats) { - printf( - " %8.2fs %7d %10.2f %s\n", - $stats['duration'], - $stats['tests'], - $stats['duration'] / $stats['tests'] * 1000, - $class, - ); -} - -// How top-heavy is the run? A handful of tests dominating reads very differently -// from the cost being spread evenly. -$durationSoFar = 0.0; -$testsInHalfTheRuntime = 0; -foreach ($slowestFirst as $test) { - $durationSoFar += $test['duration']; - $testsInHalfTheRuntime++; - if ($durationSoFar >= $totalDuration / 2) { - break; - } -} -printf( - "\nThe slowest %d tests (%.1f%% of tests) account for 50%% of the runtime.\n", - $testsInHalfTheRuntime, - $testsInHalfTheRuntime / $testCount * 100, -); --- a/tests/kvstore-cluster.config.php +++ /dev/null @@ -1,23 +0,0 @@ - '\\OC\\Memcache\\KeyValueCache', - 'memcache.distributed' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.locking' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.kvstore' => [ - 'seeds' => [ - ['host' => '127.0.0.1', 'port' => 7000], - ['host' => '127.0.0.1', 'port' => 7001], - ['host' => '127.0.0.1', 'port' => 7002], - ['host' => '127.0.0.1', 'port' => 7003], - ['host' => '127.0.0.1', 'port' => 7004], - ['host' => '127.0.0.1', 'port' => 7005], - ], - ], -]; --- a/tests/kvstore-sentinel.config.php +++ /dev/null @@ -1,21 +0,0 @@ - '\\OC\\Memcache\\KeyValueCache', - 'memcache.distributed' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.locking' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.kvstore' => [ - 'sentinel' => [ - 'service' => 'mymaster', - 'seeds' => [ - ['host' => '127.0.0.1', 'port' => 26379], - ], - ], - ], -]; --- a/tests/kvstore-single.config.php +++ /dev/null @@ -1,20 +0,0 @@ - '\\OC\\Memcache\\KeyValueCache', - 'memcache.distributed' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.locking' => '\\OC\\Memcache\\KeyValueCache', - 'memcache.kvstore' => [ - 'server' => [ - 'host' => 'localhost', - 'port' => 6379, - 'protocol' => 'tcp', - ], - ], -]; --- a/tests/lib/Accounts/AccountManagerTest.php +++ /dev/null @@ -1,1065 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->phoneNumberUtil = new PhoneNumberUtil(); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->config = $this->createMock(IConfig::class); - $this->jobList = $this->createMock(IJobList::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->verificationToken = $this->createMock(IVerificationToken::class); - $this->mailer = $this->createMock(IMailer::class); - $this->defaults = $this->createMock(Defaults::class); - $this->l10nFactory = $this->createMock(IFactory::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->clientService = $this->createMock(IClientService::class); - - $this->accountManager = new AccountManager( - $this->connection, - $this->config, - $this->eventDispatcher, - $this->jobList, - $this->logger, - $this->verificationToken, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->crypto, - $this->phoneNumberUtil, - $this->clientService, - ); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - $query = $this->connection->getQueryBuilder(); - $query->delete($this->table)->executeStatement(); - } - - protected function makeUser(string $uid, string $name, ?string $email = null): IUser { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUid') - ->willReturn($uid); - $user->expects($this->any()) - ->method('getDisplayName') - ->willReturn($name); - if ($email !== null) { - $user->expects($this->any()) - ->method('getEMailAddress') - ->willReturn($email); - } - - return $user; - } - - protected function populateOrUpdate(): void { - $users = [ - [ - 'user' => $this->makeUser('j.doe', 'Jane Doe', 'jane.doe@acme.com'), - 'data' => [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'Jane Doe', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'jane.doe@acme.com', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '@sometwitter', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '@someMastodon@mastodon.social', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '+491601231212', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => 'some street', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://acme.com', - 'scope' => IAccountManager::SCOPE_PRIVATE - ], - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => 'Some organisation', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => 'Human', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => 'Hi', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => 'Biography', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - ], - ], - [ - 'user' => $this->makeUser('a.allison', 'Alice Allison', 'a.allison@example.org'), - 'data' => [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'Alice Allison', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'a.allison@example.org', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '@a_alice', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '@a_alice@cool.social', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '+491602312121', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => 'Dundee Road 45', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://example.org', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => 'Another organisation', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => 'Alien', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => 'Hello', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => 'Different biography', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - ], - ], - [ - 'user' => $this->makeUser('b32c5a5b-1084-4380-8856-e5223b16de9f', 'Armel Oliseh', 'oliseh@example.com'), - 'data' => [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'Armel Oliseh', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'oliseh@example.com', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '+491603121212', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => 'Sunflower Blvd. 77', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://example.com', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => 'Yet another organisation', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => 'Being', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => 'This is a headline', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => 'Some long biography', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - ], - ], - [ - 'user' => $this->makeUser('31b5316a-9b57-4b17-970a-315a4cbe73eb', 'K. Cheng', 'cheng@emca.com'), - 'data' => [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'K. Cheng', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'cheng@emca.com', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '', ' - scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '', ' - scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '+71601212123', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => 'Pinapple Street 22', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://emca.com', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => 'Organisation A', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => 'Animal', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => 'My headline', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => 'Short biography', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::COLLECTION_EMAIL, - 'value' => 'k.cheng@emca.com', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::COLLECTION_EMAIL, - 'value' => 'kai.cheng@emca.com', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - ], - ], - [ - 'user' => $this->makeUser('goodpal@elpmaxe.org', 'Goodpal, Kim', 'goodpal@elpmaxe.org'), - 'data' => [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'Goodpal, Kim', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'goodpal@elpmaxe.org', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL - ], - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '+71602121231', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => 'Octopus Ave 17', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://elpmaxe.org', - 'scope' => IAccountManager::SCOPE_PUBLISHED - ], - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => 'Organisation B', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => 'Organism', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => 'Best headline', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => 'Autobiography', - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - ], - ], - ]; - $this->config->expects($this->exactly(count($users)))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]); - foreach ($users as $userInfo) { - $this->invokePrivate($this->accountManager, 'updateUser', [$userInfo['user'], $userInfo['data'], null, false]); - } - } - - /** - * get a instance of the accountManager - * - * @return MockObject | AccountManager - */ - public function getInstance(?array $mockedMethods = null) { - return $this->getMockBuilder(AccountManager::class) - ->setConstructorArgs([ - $this->connection, - $this->config, - $this->eventDispatcher, - $this->jobList, - $this->logger, - $this->verificationToken, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->crypto, - $this->phoneNumberUtil, - $this->clientService, - ]) - ->onlyMethods($mockedMethods) - ->getMock(); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')] - public function testUpdateUser(array $newData, array $oldData, bool $insertNew, bool $updateExisting): void { - $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']); - /** @var IUser $user */ - $user = $this->createMock(IUser::class); - - if ($updateExisting) { - $accountManager->expects($this->once())->method('updateExistingUser') - ->with($user, $newData); - $accountManager->expects($this->never())->method('insertNewUser'); - } - if ($insertNew) { - $accountManager->expects($this->once())->method('insertNewUser') - ->with($user, $newData); - $accountManager->expects($this->never())->method('updateExistingUser'); - } - - if (!$insertNew && !$updateExisting) { - $accountManager->expects($this->never())->method('updateExistingUser'); - $accountManager->expects($this->never())->method('insertNewUser'); - $this->eventDispatcher->expects($this->never())->method('dispatchTyped'); - } else { - $this->eventDispatcher->expects($this->once())->method('dispatchTyped') - ->willReturnCallback( - function ($event) use ($user, $newData): void { - $this->assertInstanceOf(UserUpdatedEvent::class, $event); - $this->assertSame($user, $event->getUser()); - $this->assertSame($newData, $event->getData()); - } - ); - } - - $this->invokePrivate($accountManager, 'updateUser', [$user, $newData, $oldData]); - } - - public static function dataTrueFalse(): array { - return [ - #$newData | $oldData | $insertNew | $updateExisting - [['myProperty' => ['value' => 'newData']], ['myProperty' => ['value' => 'oldData']], false, true], - [['myProperty' => ['value' => 'oldData']], ['myProperty' => ['value' => 'oldData']], false, false] - ]; - } - - public function testAddMissingDefaults(): void { - $user = $this->createMock(IUser::class); - - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('settings', 'profile_enabled_by_default', '1') - ->willReturn('1'); - - $input = [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'bob', - 'verified' => IAccountManager::NOT_VERIFIED, - ], - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'bob@bob.bob', - ], - ]; - - $expected = [ - [ - 'name' => IAccountManager::PROPERTY_DISPLAYNAME, - 'value' => 'bob', - 'scope' => IAccountManager::SCOPE_FEDERATED, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_EMAIL, - 'value' => 'bob@bob.bob', - 'scope' => IAccountManager::SCOPE_FEDERATED, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_ADDRESS, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_AVATAR, - 'scope' => IAccountManager::SCOPE_FEDERATED - ], - - [ - 'name' => IAccountManager::PROPERTY_PHONE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_TWITTER, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_BLUESKY, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - ], - - [ - 'name' => IAccountManager::PROPERTY_ORGANISATION, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - ], - - [ - 'name' => IAccountManager::PROPERTY_ROLE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - ], - - [ - 'name' => IAccountManager::PROPERTY_HEADLINE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - ], - - [ - 'name' => IAccountManager::PROPERTY_BIOGRAPHY, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - ], - - [ - 'name' => IAccountManager::PROPERTY_BIRTHDATE, - 'value' => '', - 'scope' => IAccountManager::SCOPE_LOCAL, - ], - - [ - 'name' => IAccountManager::PROPERTY_PROFILE_ENABLED, - 'value' => '1', - ], - - [ - 'name' => IAccountManager::PROPERTY_PRONOUNS, - 'value' => '', - 'scope' => IAccountManager::SCOPE_FEDERATED, - ], - ]; - $this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]); - - $defaultUserRecord = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]); - $result = $this->invokePrivate($this->accountManager, 'addMissingDefaultValues', [$input, $defaultUserRecord]); - - $this->assertSame($expected, $result); - } - - public function testGetAccount(): void { - $accountManager = $this->getInstance(['getUser']); - /** @var IUser $user */ - $user = $this->createMock(IUser::class); - - $data = [ - [ - 'value' => '@twitterhandle', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - 'name' => IAccountManager::PROPERTY_TWITTER, - ], - [ - 'value' => '@mastohandle@mastodon.social', - 'scope' => IAccountManager::SCOPE_LOCAL, - 'verified' => IAccountManager::NOT_VERIFIED, - 'name' => IAccountManager::PROPERTY_FEDIVERSE, - ], - [ - 'value' => 'test@example.com', - 'scope' => IAccountManager::SCOPE_PUBLISHED, - 'verified' => IAccountManager::VERIFICATION_IN_PROGRESS, - 'name' => IAccountManager::PROPERTY_EMAIL, - ], - [ - 'value' => 'https://example.com', - 'scope' => IAccountManager::SCOPE_FEDERATED, - 'verified' => IAccountManager::VERIFIED, - 'name' => IAccountManager::PROPERTY_WEBSITE, - ], - ]; - $expected = new Account($user); - $expected->setProperty(IAccountManager::PROPERTY_TWITTER, '@twitterhandle', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - $expected->setProperty(IAccountManager::PROPERTY_FEDIVERSE, '@mastohandle@mastodon.social', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - $expected->setProperty(IAccountManager::PROPERTY_EMAIL, 'test@example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS); - $expected->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED); - - $accountManager->expects($this->once()) - ->method('getUser') - ->willReturn($data); - $this->assertEquals($expected, $accountManager->getAccount($user)); - } - - public static function dataParsePhoneNumber(): array { - return [ - ['0711 / 25 24 28-90', 'DE', '+4971125242890'], - ['0711 / 25 24 28-90', '', null], - ['+49 711 / 25 24 28-90', '', '+4971125242890'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataParsePhoneNumber')] - public function testSanitizePhoneNumberOnUpdateAccount(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void { - $this->config->method('getSystemValueString') - ->willReturn($defaultRegion); - - $user = $this->createMock(IUser::class); - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_PHONE, $phoneInput, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - $manager = $this->getInstance(['getUser', 'updateUser']); - $manager->method('getUser') - ->with($user, false) - ->willReturn([]); - $manager->expects($phoneNumber === null ? self::never() : self::once()) - ->method('updateUser'); - - if ($phoneNumber === null) { - $this->expectException(\InvalidArgumentException::class); - } - - $manager->updateAccount($account); - - if ($phoneNumber !== null) { - self::assertEquals($phoneNumber, $account->getProperty(IAccountManager::PROPERTY_PHONE)->getValue()); - } - } - - public static function dataSanitizeOnUpdate(): array { - return [ - [IAccountManager::PROPERTY_WEBSITE, 'https://nextcloud.com', 'https://nextcloud.com'], - [IAccountManager::PROPERTY_WEBSITE, 'http://nextcloud.com', 'http://nextcloud.com'], - [IAccountManager::PROPERTY_WEBSITE, 'ftp://nextcloud.com', null], - [IAccountManager::PROPERTY_WEBSITE, '//nextcloud.com/', null], - [IAccountManager::PROPERTY_WEBSITE, 'https:///?query', null], - - [IAccountManager::PROPERTY_TWITTER, '@nextcloud', 'nextcloud'], - [IAccountManager::PROPERTY_TWITTER, '_nextcloud', '_nextcloud'], - [IAccountManager::PROPERTY_TWITTER, 'FooB4r', 'FooB4r'], - [IAccountManager::PROPERTY_TWITTER, 'X', null], - [IAccountManager::PROPERTY_TWITTER, 'next.cloud', null], - [IAccountManager::PROPERTY_TWITTER, 'ab/cd.zip', null], - [IAccountManager::PROPERTY_TWITTER, 'tooLongForTwitterAndX', null], - - [IAccountManager::PROPERTY_FEDIVERSE, 'nextcloud@mastodon.social', 'nextcloud@mastodon.social'], - [IAccountManager::PROPERTY_FEDIVERSE, '@nextcloud@mastodon.xyz', 'nextcloud@mastodon.xyz'], - [IAccountManager::PROPERTY_FEDIVERSE, 'l33t.h4x0r@sub.localhost.local', 'l33t.h4x0r@sub.localhost.local'], - [IAccountManager::PROPERTY_FEDIVERSE, 'invalid/name@mastodon.social', null], - [IAccountManager::PROPERTY_FEDIVERSE, 'name@evil.host/malware.exe', null], - [IAccountManager::PROPERTY_FEDIVERSE, '@is-it-a-host-or-name', null], - [IAccountManager::PROPERTY_FEDIVERSE, 'only-a-name', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeOnUpdate')] - public function testSanitizingOnUpdateAccount(string $property, string $input, ?string $output): void { - - if ($property === IAccountManager::PROPERTY_FEDIVERSE) { - // We do not test the server response here we do this in the `testSanitizingFediverseServer` - $this->config - ->method('getSystemValueBool') - ->with('has_internet_connection', true) - ->willReturn(false); - } - - $user = $this->createMock(IUser::class); - - $account = new Account($user); - $account->setProperty($property, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - - $manager = $this->getInstance(['getUser', 'updateUser']); - $manager->method('getUser') - ->with($user, false) - ->willReturn([]); - $manager->expects($output === null ? self::never() : self::once()) - ->method('updateUser'); - - if ($output === null) { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage($property); - } - - $manager->updateAccount($account); - - if ($output !== null) { - self::assertEquals($output, $account->getProperty($property)->getValue()); - } - } - - public static function dataSanitizeFediverseServer(): array { - return [ - 'no internet' => [ - '@foo@example.com', - 'foo@example.com', - false, - null, - ], - 'no internet - no at' => [ - 'foo@example.com', - 'foo@example.com', - false, - null, - ], - 'valid response' => [ - '@foo@example.com', - 'foo@example.com', - true, - json_encode([ - 'subject' => 'acct:foo@example.com', - 'links' => [ - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => 'https://example.com/users/foo', - ], - ], - ]), - ], - 'valid response - no at' => [ - 'foo@example.com', - 'foo@example.com', - true, - json_encode([ - 'subject' => 'acct:foo@example.com', - 'links' => [ - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => 'https://example.com/users/foo', - ], - ], - ]), - ], - // failures - 'invalid response' => [ - '@foo@example.com', - null, - true, - json_encode([ - 'subject' => 'acct:foo@example.com', - 'links' => [], - ]), - ], - 'no response' => [ - '@foo@example.com', - null, - true, - null, - ], - 'wrong user' => [ - '@foo@example.com', - null, - true, - json_encode([ - 'links' => [], - ]), - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeFediverseServer')] - public function testSanitizingFediverseServer(string $input, ?string $output, bool $hasInternet, ?string $serverResponse): void { - $this->config->expects(self::once()) - ->method('getSystemValueBool') - ->with('has_internet_connection', true) - ->willReturn($hasInternet); - - if ($hasInternet) { - $client = $this->createMock(IClient::class); - if ($serverResponse !== null) { - $response = $this->createMock(IResponse::class); - $response->method('getBody') - ->willReturn($serverResponse); - $client->expects(self::once()) - ->method('get') - ->with('https://example.com/.well-known/webfinger?resource=acct:foo@example.com') - ->willReturn($response); - } else { - $client->expects(self::once()) - ->method('get') - ->with('https://example.com/.well-known/webfinger?resource=acct:foo@example.com') - ->willThrowException(new \Exception('404')); - } - - $this->clientService - ->expects(self::once()) - ->method('newClient') - ->willReturn($client); - } else { - $this->clientService - ->expects(self::never()) - ->method('newClient'); - } - - $user = $this->createMock(IUser::class); - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_FEDIVERSE, $input, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - - $manager = $this->getInstance(['getUser', 'updateUser']); - $manager->method('getUser') - ->with($user, false) - ->willReturn([]); - $manager->expects($output === null ? self::never() : self::once()) - ->method('updateUser'); - - if ($output === null) { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage(IAccountManager::PROPERTY_FEDIVERSE); - } - - $manager->updateAccount($account); - - if ($output !== null) { - self::assertEquals($output, $account->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue()); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')] - public function testSearchUsers(string $property, array $values, array $expected): void { - $this->populateOrUpdate(); - - $matchedUsers = $this->accountManager->searchUsers($property, $values); - foreach ($expected as $expectedEntry) { - $this->assertContains($expectedEntry, $matchedUsers); - } - if (empty($expected)) { - $this->assertEmpty($matchedUsers); - } - } - - public static function searchDataProvider(): array { - return [ - [ #0 Search for an existing name - IAccountManager::PROPERTY_DISPLAYNAME, - ['Jane Doe'], - ['Jane Doe' => 'j.doe'] - ], - [ #1 Search for part of a name (no result) - IAccountManager::PROPERTY_DISPLAYNAME, - ['Jane'], - [] - ], - [ #2 Search for part of a name (no result, test wildcard) - IAccountManager::PROPERTY_DISPLAYNAME, - ['Jane%'], - [] - ], - [ #3 Search for phone - IAccountManager::PROPERTY_PHONE, - ['+491603121212'], - ['+491603121212' => 'b32c5a5b-1084-4380-8856-e5223b16de9f'], - ], - [ #4 Search for twitter handles - IAccountManager::PROPERTY_TWITTER, - ['@sometwitter', '@a_alice', '@unseen'], - ['@sometwitter' => 'j.doe', '@a_alice' => 'a.allison'], - ], - [ #5 Search for email - IAccountManager::PROPERTY_EMAIL, - ['cheng@emca.com'], - ['cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'], - ], - [ #6 Search for email by additional email - IAccountManager::PROPERTY_EMAIL, - ['kai.cheng@emca.com'], - ['kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'], - ], - [ #7 Search for additional email - IAccountManager::COLLECTION_EMAIL, - ['kai.cheng@emca.com', 'cheng@emca.com'], - ['kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb'], - ], - [ #8 Search for email by additional email (two valid search values, but the same user) - IAccountManager::PROPERTY_EMAIL, - ['kai.cheng@emca.com', 'cheng@emca.com'], - [ - 'kai.cheng@emca.com' => '31b5316a-9b57-4b17-970a-315a4cbe73eb', - ], - ], - ]; - } - - public static function dataCheckEmailVerification(): array { - return [ - [['steve', 'Steve Smith', 'steve@steve.steve'], null], - [['emma', 'Emma Morales', 'emma@emma.com'], 'emma@morales.com'], - [['sarah@web.org', 'Sarah Foster', 'sarah@web.org'], null], - [['cole@web.org', 'Cole Harrison', 'cole@web.org'], 'cole@example.com'], - [['8d29e358-cf69-4849-bbf9-28076c0b908b', 'Alice McPherson', 'alice@example.com'], 'alice@mcpherson.com'], - [['11da2744-3f4d-4c17-8c13-4c057a379237', 'James Loranger', 'james@example.com'], ''], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckEmailVerification')] - public function testCheckEmailVerification(array $userData, ?string $newEmail): void { - $user = $this->makeUser(...$userData); - // Once because of getAccount, once because of getUser - $this->config->expects($this->exactly(2))->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]); - $account = $this->accountManager->getAccount($user); - $emailUpdated = false; - - if (!empty($newEmail)) { - $account->getProperty(IAccountManager::PROPERTY_EMAIL)->setValue($newEmail); - $emailUpdated = true; - } - - if ($emailUpdated) { - $this->jobList->expects($this->once()) - ->method('add') - ->with(VerifyUserData::class); - } else { - $this->jobList->expects($this->never()) - ->method('add') - ->with(VerifyUserData::class); - } - - /** @var array $oldData */ - $oldData = $this->invokePrivate($this->accountManager, 'getUser', [$user, false]); - $this->invokePrivate($this->accountManager, 'checkEmailVerification', [$account, $oldData]); - } - - public static function dataSetDefaultPropertyScopes(): array { - return [ - [ - [], - [ - IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED, - IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED, - IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_LOCAL, - ] - ], - [ - [ - IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE, - ], [ - IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE, - ] - ], - [ - [ - IAccountManager::PROPERTY_ADDRESS => 'invalid scope', - 'invalid property' => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE, - ], - [ - IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED, - IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE, - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultPropertyScopes')] - public function testSetDefaultPropertyScopes(array $propertyScopes, array $expectedResultScopes): void { - $user = $this->makeUser('steve', 'Steve Smith', 'steve@steve.steve'); - $this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn($propertyScopes); - - $result = $this->invokePrivate($this->accountManager, 'buildDefaultUserRecord', [$user]); - $resultProperties = array_column($result, 'name'); - - $this->assertEmpty(array_diff($resultProperties, IAccountManager::ALLOWED_PROPERTIES), 'Building default user record returned non-allowed properties'); - foreach ($expectedResultScopes as $expectedResultScopeKey => $expectedResultScopeValue) { - $resultScope = $result[array_search($expectedResultScopeKey, $resultProperties)]['scope']; - $this->assertEquals($expectedResultScopeValue, $resultScope, "The result scope doesn't follow the value set into the config or defaults correctly."); - } - } -} --- a/tests/lib/Accounts/AccountPropertyCollectionTest.php +++ /dev/null @@ -1,189 +0,0 @@ -collection = new AccountPropertyCollection(self::COLLECTION_NAME); - } - - protected function makePropertyMock(string $propertyName): IAccountProperty&MockObject { - $mock = $this->createMock(IAccountProperty::class); - $mock->expects($this->any()) - ->method('getName') - ->willReturn($propertyName); - - return $mock; - } - - public function testSetAndGetProperties(): void { - $propsBefore = $this->collection->getProperties(); - $this->assertIsArray($propsBefore); - $this->assertEmpty($propsBefore); - - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - - $this->collection->setProperties($props); - $propsAfter = $this->collection->getProperties(); - $this->assertIsArray($propsAfter); - $this->assertCount(count($props), $propsAfter); - } - - public function testSetPropertiesMixedInvalid(): void { - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock('sneaky_property'), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - - $this->expectException(InvalidArgumentException::class); - $this->collection->setProperties($props); - } - - public function testAddProperty(): void { - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - $this->collection->setProperties($props); - - $additionalProperty = $this->makePropertyMock(self::COLLECTION_NAME); - $this->collection->addProperty($additionalProperty); - - $propsAfter = $this->collection->getProperties(); - $this->assertCount(count($props) + 1, $propsAfter); - $this->assertNotFalse(array_search($additionalProperty, $propsAfter, true)); - } - - public function testAddPropertyInvalid(): void { - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - $this->collection->setProperties($props); - - $additionalProperty = $this->makePropertyMock('sneaky_property'); - $exceptionThrown = false; - try { - $this->collection->addProperty($additionalProperty); - } catch (\InvalidArgumentException $e) { - $exceptionThrown = true; - } finally { - $propsAfter = $this->collection->getProperties(); - $this->assertCount(count($props), $propsAfter); - $this->assertFalse(array_search($additionalProperty, $propsAfter, true)); - $this->assertTrue($exceptionThrown); - } - } - - public function testRemoveProperty(): void { - $additionalProperty = $this->makePropertyMock(self::COLLECTION_NAME); - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $additionalProperty, - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - $this->collection->setProperties($props); - - $propsBefore = $this->collection->getProperties(); - $this->collection->removeProperty($additionalProperty); - $propsAfter = $this->collection->getProperties(); - - $this->assertTrue(count($propsBefore) > count($propsAfter)); - $this->assertCount(count($propsBefore) - 1, $propsAfter); - $this->assertFalse(array_search($additionalProperty, $propsAfter, true)); - } - - public function testRemovePropertyNotFound(): void { - $additionalProperty = $this->makePropertyMock(self::COLLECTION_NAME); - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - $this->collection->setProperties($props); - - $propsBefore = $this->collection->getProperties(); - $this->collection->removeProperty($additionalProperty); - $propsAfter = $this->collection->getProperties(); - - // no errors, gently - $this->assertCount(count($propsBefore), $propsAfter); - } - - public function testRemovePropertyByValue(): void { - $additionalProperty = $this->makePropertyMock(self::COLLECTION_NAME); - $additionalProperty->expects($this->any()) - ->method('getValue') - ->willReturn('Lorem ipsum'); - - $additionalPropertyTwo = clone $additionalProperty; - - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $additionalProperty, - $this->makePropertyMock(self::COLLECTION_NAME), - $additionalPropertyTwo - ]; - $this->collection->setProperties($props); - - $propsBefore = $this->collection->getProperties(); - $this->collection->removePropertyByValue('Lorem ipsum'); - $propsAfter = $this->collection->getProperties(); - - $this->assertTrue(count($propsBefore) > count($propsAfter)); - $this->assertCount(count($propsBefore) - 2, $propsAfter); - $this->assertFalse(array_search($additionalProperty, $propsAfter, true)); - $this->assertFalse(array_search($additionalPropertyTwo, $propsAfter, true)); - } - - public function testRemovePropertyByValueNotFound(): void { - $additionalProperty = $this->makePropertyMock(self::COLLECTION_NAME); - $additionalProperty->expects($this->any()) - ->method('getValue') - ->willReturn('Lorem ipsum'); - - $props = [ - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - $this->makePropertyMock(self::COLLECTION_NAME), - ]; - $this->collection->setProperties($props); - - $propsBefore = $this->collection->getProperties(); - $this->collection->removePropertyByValue('Lorem ipsum'); - $propsAfter = $this->collection->getProperties(); - - // no errors, gently - $this->assertCount(count($propsBefore), $propsAfter); - } -} --- a/tests/lib/Accounts/AccountPropertyTest.php +++ /dev/null @@ -1,133 +0,0 @@ -assertEquals(IAccountManager::PROPERTY_WEBSITE, $accountProperty->getName()); - $this->assertEquals('https://example.com', $accountProperty->getValue()); - $this->assertEquals(IAccountManager::SCOPE_PUBLISHED, $accountProperty->getScope()); - $this->assertEquals(IAccountManager::VERIFIED, $accountProperty->getVerified()); - } - - public function testSetValue(): void { - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - IAccountManager::SCOPE_PUBLISHED, - IAccountManager::VERIFIED, - '' - ); - $actualReturn = $accountProperty->setValue('https://example.org'); - $this->assertEquals('https://example.org', $accountProperty->getValue()); - $this->assertEquals('https://example.org', $actualReturn->getValue()); - } - - public function testSetScope(): void { - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - IAccountManager::SCOPE_PUBLISHED, - IAccountManager::VERIFIED, - '' - ); - $actualReturn = $accountProperty->setScope(IAccountManager::SCOPE_LOCAL); - $this->assertEquals(IAccountManager::SCOPE_LOCAL, $accountProperty->getScope()); - $this->assertEquals(IAccountManager::SCOPE_LOCAL, $actualReturn->getScope()); - } - - public static function scopesProvider(): array { - return [ - // current values - [IAccountManager::SCOPE_PRIVATE, IAccountManager::SCOPE_PRIVATE], - [IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_LOCAL], - [IAccountManager::SCOPE_FEDERATED, IAccountManager::SCOPE_FEDERATED], - [IAccountManager::SCOPE_PUBLISHED, IAccountManager::SCOPE_PUBLISHED], - // invalid values - ['unknown', null], - ['v2-unknown', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('scopesProvider')] - public function testSetScopeMapping(string $storedScope, ?string $returnedScope): void { - if ($returnedScope === null) { - $this->expectException(\InvalidArgumentException::class); - } - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - $storedScope, - IAccountManager::VERIFIED, - '' - ); - $this->assertEquals($returnedScope, $accountProperty->getScope()); - } - - public function testSetVerified(): void { - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - IAccountManager::SCOPE_PUBLISHED, - IAccountManager::VERIFIED, - '' - ); - $actualReturn = $accountProperty->setVerified(IAccountManager::NOT_VERIFIED); - $this->assertEquals(IAccountManager::NOT_VERIFIED, $accountProperty->getVerified()); - $this->assertEquals(IAccountManager::NOT_VERIFIED, $actualReturn->getVerified()); - } - - public function testSetVerificationData(): void { - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - IAccountManager::SCOPE_PUBLISHED, - IAccountManager::VERIFIED, - '' - ); - $token = uniqid(); - $actualReturn = $accountProperty->setVerificationData($token); - $this->assertEquals($token, $accountProperty->getVerificationData()); - $this->assertEquals($token, $actualReturn->getVerificationData()); - } - - public function testJsonSerialize(): void { - $accountProperty = new AccountProperty( - IAccountManager::PROPERTY_WEBSITE, - 'https://example.com', - IAccountManager::SCOPE_PUBLISHED, - IAccountManager::VERIFIED, - '60a7a633b74af', - ); - $this->assertEquals([ - 'name' => IAccountManager::PROPERTY_WEBSITE, - 'value' => 'https://example.com', - 'scope' => IAccountManager::SCOPE_PUBLISHED, - 'verified' => IAccountManager::VERIFIED, - 'verificationData' => '60a7a633b74af' - ], $accountProperty->jsonSerialize()); - } -} --- a/tests/lib/Accounts/AccountTest.php +++ /dev/null @@ -1,156 +0,0 @@ -createMock(IUser::class); - $account = new Account($user); - $this->assertEquals($user, $account->getUser()); - } - - public function testSetProperty(): void { - $user = $this->createMock(IUser::class); - $property = new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''); - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED); - $this->assertEquals($property, $account->getProperty(IAccountManager::PROPERTY_WEBSITE)); - } - - public function testGetAndGetAllProperties(): void { - $user = $this->createMock(IUser::class); - $properties = [ - IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, '') - ]; - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED); - $account->setProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED); - - $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL); - $additionalProperty = new AccountProperty($col->getName(), 'second@example.org', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''); - $col->addProperty($additionalProperty); - $account->setPropertyCollection($col); - - $this->assertEquals($properties, $account->getProperties()); - $properties[] = $additionalProperty; - $this->assertEquals(array_values($properties), \iterator_to_array($account->getAllProperties())); - } - - public function testSetAllPropertiesFromJson(): void { - $user = $this->createMock(IUser::class); - $properties = [ - IAccountManager::PROPERTY_DISPLAYNAME => new AccountProperty(IAccountManager::PROPERTY_DISPLAYNAME, 'Steve', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_ADDRESS => new AccountProperty(IAccountManager::PROPERTY_ADDRESS, '123 Acorn Avenue', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://www.example.org', IAccountManager::SCOPE_FEDERATED, IAccountManager::VERIFIED, ''), - IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, 'steve@earth.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''), - IAccountManager::PROPERTY_AVATAR => new AccountProperty(IAccountManager::PROPERTY_AVATAR, '', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '+358407991028', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_TWITTER => new AccountProperty(IAccountManager::PROPERTY_TWITTER, 'therealsteve', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_BLUESKY => new AccountProperty(IAccountManager::PROPERTY_BLUESKY, 'therealsteve.bsky.social', IAccountManager::SCOPE_PRIVATE, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_ORGANISATION => new AccountProperty(IAccountManager::PROPERTY_ORGANISATION, 'Steve Incorporated', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_ROLE => new AccountProperty(IAccountManager::PROPERTY_ROLE, 'Founder', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_HEADLINE => new AccountProperty(IAccountManager::PROPERTY_HEADLINE, 'I am Steve', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_BIOGRAPHY => new AccountProperty(IAccountManager::PROPERTY_BIOGRAPHY, 'Steve is the best', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_PROFILE_ENABLED => new AccountProperty(IAccountManager::PROPERTY_PROFILE_ENABLED, '1', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::COLLECTION_EMAIL => [ - new AccountProperty(IAccountManager::COLLECTION_EMAIL, 'steve@mars.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - new AccountProperty(IAccountManager::COLLECTION_EMAIL, 'steve@neptune.com', IAccountManager::SCOPE_FEDERATED, IAccountManager::NOT_VERIFIED, ''), - ], - ]; - $account = new Account($user); - $account->setAllPropertiesFromJson(json_decode(json_encode($properties), true)); - $this->assertEquals($properties, $account->jsonSerialize()); - } - - public function testGetFilteredProperties(): void { - $user = $this->createMock(IUser::class); - $properties = [ - IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''), - IAccountManager::PROPERTY_PHONE => new AccountProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''), - ]; - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED); - $account->setProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED); - $account->setProperty(IAccountManager::PROPERTY_PHONE, '123456', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED); - - $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL); - $additionalProperty1 = new AccountProperty($col->getName(), 'second@example.org', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''); - $additionalProperty2 = new AccountProperty($col->getName(), 'third@example.org', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''); - $col->addProperty($additionalProperty1); - $col->addProperty($additionalProperty2); - $account->setPropertyCollection($col); - - $this->assertEquals( - [ - IAccountManager::PROPERTY_WEBSITE => $properties[IAccountManager::PROPERTY_WEBSITE], - IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE], - IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty1, - IAccountManager::COLLECTION_EMAIL . '#1' => $additionalProperty2, - ], - $account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED) - ); - $this->assertEquals( - [ - IAccountManager::PROPERTY_EMAIL => $properties[IAccountManager::PROPERTY_EMAIL], - IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE], - IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2, - ], - $account->getFilteredProperties(null, IAccountManager::VERIFIED) - ); - $this->assertEquals( - [ - IAccountManager::PROPERTY_PHONE => $properties[IAccountManager::PROPERTY_PHONE], - IAccountManager::COLLECTION_EMAIL . '#0' => $additionalProperty2, - ], - $account->getFilteredProperties(IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED), - ); - } - - public function testJsonSerialize(): void { - $user = $this->createMock(IUser::class); - $properties = [ - IAccountManager::PROPERTY_WEBSITE => new AccountProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED, ''), - IAccountManager::PROPERTY_EMAIL => new AccountProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED, ''), - IAccountManager::COLLECTION_EMAIL => [ - new AccountProperty(IAccountManager::COLLECTION_EMAIL, 'apple@orange.com', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''), - new AccountProperty(IAccountManager::COLLECTION_EMAIL, 'banana@orange.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''), - new AccountProperty(IAccountManager::COLLECTION_EMAIL, 'kiwi@watermelon.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''), - ], - ]; - - $account = new Account($user); - $account->setProperty(IAccountManager::PROPERTY_WEBSITE, 'https://example.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::NOT_VERIFIED); - $account->setProperty(IAccountManager::PROPERTY_EMAIL, 'user@example.com', IAccountManager::SCOPE_LOCAL, IAccountManager::VERIFIED); - - $col = new AccountPropertyCollection(IAccountManager::COLLECTION_EMAIL); - $col->setProperties([ - new AccountProperty($col->getName(), 'apple@orange.com', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED, ''), - new AccountProperty($col->getName(), 'banana@orange.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFICATION_IN_PROGRESS, ''), - new AccountProperty($col->getName(), 'kiwi@watermelon.com', IAccountManager::SCOPE_PUBLISHED, IAccountManager::VERIFIED, ''), - ]); - $account->setPropertyCollection($col); - - $this->assertEquals($properties, $account->jsonSerialize()); - } -} --- a/tests/lib/Accounts/HooksTest.php +++ /dev/null @@ -1,139 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->accountManager = $this->getMockBuilder(AccountManager::class) - ->disableOriginalConstructor()->getMock(); - - $this->hooks = new Hooks($this->logger, $this->accountManager); - } - - /** - * - * @param $params - * @param $data - * @param $setEmail - * @param $setDisplayName - * @param $error - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestChangeUserHook')] - public function testChangeUserHook($params, $data, $setEmail, $setDisplayName, $error): void { - if ($error) { - $this->accountManager->expects($this->never())->method('updateAccount'); - } else { - $account = $this->createMock(IAccount::class); - $this->accountManager->expects($this->atLeastOnce())->method('getAccount')->willReturn($account); - if ($setEmail) { - $property = $this->createMock(IAccountProperty::class); - $property->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn($data[IAccountManager::PROPERTY_EMAIL]['value']); - $property->expects($this->atLeastOnce()) - ->method('setValue') - ->with($params['value']); - - $account->expects($this->atLeastOnce()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_EMAIL) - ->willReturn($property); - - $this->accountManager->expects($this->once()) - ->method('updateAccount') - ->with($account); - } elseif ($setDisplayName) { - $property = $this->createMock(IAccountProperty::class); - $property->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']); - $property->expects($this->atLeastOnce()) - ->method('setValue') - ->with($params['value']); - - $account->expects($this->atLeastOnce()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_DISPLAYNAME) - ->willReturn($property); - - $this->accountManager->expects($this->once()) - ->method('updateAccount') - ->with($account); - } else { - $this->accountManager->expects($this->never())->method('updateAccount'); - } - } - - $params['user'] = $this->createMock(IUser::class); - $this->hooks->changeUserHook($params['user'], $params['feature'], $params['value']); - } - - public static function dataTestChangeUserHook(): array { - return [ - [ - ['feature' => '', 'value' => ''], - [ - IAccountManager::PROPERTY_EMAIL => ['value' => ''], - IAccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] - ], - false, false, true - ], - [ - ['feature' => 'foo', 'value' => 'bar'], - [ - IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] - ], - false, false, false - ], - [ - ['feature' => 'eMailAddress', 'value' => 'newMail@example.com'], - [ - IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] - ], - true, false, false - ], - [ - ['feature' => 'displayName', 'value' => 'newDisplayName'], - [ - IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] - ], - false, true, false - ], - ]; - } -} --- a/tests/lib/Activity/ManagerTest.php +++ /dev/null @@ -1,296 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->session = $this->createMock(IUserSession::class); - $this->config = $this->createMock(IConfig::class); - $this->validator = $this->createMock(IValidator::class); - $this->richTextFormatter = $this->createMock(IRichTextFormatter::class); - $this->time = $this->createMock(ITimeFactory::class); - - $this->activityManager = new Manager( - $this->request, - $this->session, - $this->config, - $this->validator, - $this->richTextFormatter, - $this->createMock(IL10N::class), - $this->time, - ); - - $this->assertSame([], self::invokePrivate($this->activityManager, 'getConsumers')); - - $this->activityManager->registerConsumer(function () { - return new NoOpConsumer(); - }); - - $this->assertNotEmpty(self::invokePrivate($this->activityManager, 'getConsumers')); - $this->assertNotEmpty(self::invokePrivate($this->activityManager, 'getConsumers')); - } - - public function testGetConsumers(): void { - $consumers = self::invokePrivate($this->activityManager, 'getConsumers'); - - $this->assertNotEmpty($consumers); - } - - public function testGetConsumersInvalidConsumer(): void { - $this->expectException(\InvalidArgumentException::class); - - $this->activityManager->registerConsumer(function () { - return new \stdClass(); - }); - - self::invokePrivate($this->activityManager, 'getConsumers'); - } - - public static function getUserFromTokenThrowInvalidTokenData(): array { - return [ - [null, []], - ['', []], - ['12345678901234567890123456789', []], - ['1234567890123456789012345678901', []], - ['123456789012345678901234567890', []], - ['123456789012345678901234567890', ['user1', 'user2']], - ]; - } - - /** - * - * @param string $token - * @param array $users - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getUserFromTokenThrowInvalidTokenData')] - public function testGetUserFromTokenThrowInvalidToken($token, $users): void { - $this->expectException(\UnexpectedValueException::class); - - $this->mockRSSToken($token, $token, $users); - self::invokePrivate($this->activityManager, 'getUserFromToken'); - } - - public static function getUserFromTokenData(): array { - return [ - [null, '123456789012345678901234567890', 'user1'], - ['user2', null, 'user2'], - ['user2', '123456789012345678901234567890', 'user2'], - ]; - } - - /** - * - * @param string $userLoggedIn - * @param string $token - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getUserFromTokenData')] - public function testGetUserFromToken($userLoggedIn, $token, $expected): void { - if ($userLoggedIn !== null) { - $this->mockUserSession($userLoggedIn); - } - $this->mockRSSToken($token, '123456789012345678901234567890', ['user1']); - - $this->assertEquals($expected, $this->activityManager->getCurrentUserId()); - } - - protected function mockRSSToken($requestToken, $userToken, $users) { - if ($requestToken !== null) { - $this->request->expects($this->any()) - ->method('getParam') - ->with('token', '') - ->willReturn($requestToken); - } - - $this->config->expects($this->any()) - ->method('getUsersForUserValue') - ->with('activity', 'rsstoken', $userToken) - ->willReturn($users); - } - - protected function mockUserSession($user) { - $mockUser = $this->getMockBuilder(IUser::class) - ->disableOriginalConstructor() - ->getMock(); - $mockUser->expects($this->any()) - ->method('getUID') - ->willReturn($user); - - $this->session->expects($this->any()) - ->method('isLoggedIn') - ->willReturn(true); - $this->session->expects($this->any()) - ->method('getUser') - ->willReturn($mockUser); - } - - public function testPublishExceptionNoApp(): void { - $this->expectException(IncompleteActivityException::class); - - $event = $this->activityManager->generateEvent(); - $this->activityManager->publish($event); - } - - public function testPublishExceptionNoType(): void { - $this->expectException(IncompleteActivityException::class); - - $event = $this->activityManager->generateEvent(); - $event->setApp('test'); - $this->activityManager->publish($event); - } - - public function testPublishExceptionNoAffectedUser(): void { - $this->expectException(IncompleteActivityException::class); - - $event = $this->activityManager->generateEvent(); - $event->setApp('test') - ->setType('test_type'); - $this->activityManager->publish($event); - } - - public function testPublishExceptionNoSubject(): void { - $this->expectException(IncompleteActivityException::class); - - $event = $this->activityManager->generateEvent(); - $event->setApp('test') - ->setType('test_type') - ->setAffectedUser('test_affected'); - $this->activityManager->publish($event); - } - - public static function dataPublish(): array { - return [ - [null, ''], - ['test_author', 'test_author'], - ]; - } - - /** - * @param string|null $author - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublish')] - public function testPublish($author, $expected): void { - if ($author !== null) { - $authorObject = $this->getMockBuilder(IUser::class) - ->disableOriginalConstructor() - ->getMock(); - $authorObject->expects($this->once()) - ->method('getUID') - ->willReturn($author); - $this->session->expects($this->atLeastOnce()) - ->method('getUser') - ->willReturn($authorObject); - } - - $time = time(); - $this->time - ->method('getTime') - ->willReturn($time); - - $event = $this->activityManager->generateEvent(); - $event->setApp('test') - ->setType('test_type') - ->setSubject('test_subject', []) - ->setAffectedUser('test_affected') - ->setObject('file', 123); - - $consumer = $this->getMockBuilder('OCP\Activity\IConsumer') - ->disableOriginalConstructor() - ->getMock(); - $consumer->expects($this->once()) - ->method('receive') - ->with($event) - ->willReturnCallback(function (IEvent $event) use ($expected, $time): void { - $this->assertEquals($time, $event->getTimestamp(), 'Timestamp not set correctly'); - $this->assertSame($expected, $event->getAuthor(), 'Author name not set correctly'); - }); - $this->activityManager->registerConsumer(function () use ($consumer) { - return $consumer; - }); - - $this->activityManager->publish($event); - } - - public function testPublishAllManually(): void { - $event = $this->activityManager->generateEvent(); - $event->setApp('test_app') - ->setType('test_type') - ->setAffectedUser('test_affected') - ->setAuthor('test_author') - ->setTimestamp(1337) - ->setSubject('test_subject', ['test_subject_param']) - ->setMessage('test_message', ['test_message_param']) - ->setObject('test_object_type', 42, 'test_object_name') - ->setLink('test_link') - ; - - $consumer = $this->getMockBuilder('OCP\Activity\IConsumer') - ->disableOriginalConstructor() - ->getMock(); - $consumer->expects($this->once()) - ->method('receive') - ->willReturnCallback(function (IEvent $event): void { - $this->assertSame('test_app', $event->getApp(), 'App not set correctly'); - $this->assertSame('test_type', $event->getType(), 'Type not set correctly'); - $this->assertSame('test_affected', $event->getAffectedUser(), 'Affected user not set correctly'); - $this->assertSame('test_author', $event->getAuthor(), 'Author not set correctly'); - $this->assertSame(1337, $event->getTimestamp(), 'Timestamp not set correctly'); - $this->assertSame('test_subject', $event->getSubject(), 'Subject not set correctly'); - $this->assertSame(['test_subject_param'], $event->getSubjectParameters(), 'Subject parameter not set correctly'); - $this->assertSame('test_message', $event->getMessage(), 'Message not set correctly'); - $this->assertSame(['test_message_param'], $event->getMessageParameters(), 'Message parameter not set correctly'); - $this->assertSame('test_object_type', $event->getObjectType(), 'Object type not set correctly'); - $this->assertSame(42, $event->getObjectId(), 'Object ID not set correctly'); - $this->assertSame('test_object_name', $event->getObjectName(), 'Object name not set correctly'); - $this->assertSame('test_link', $event->getLink(), 'Link not set correctly'); - }); - $this->activityManager->registerConsumer(function () use ($consumer) { - return $consumer; - }); - - $this->activityManager->publish($event); - } -} - -class NoOpConsumer implements IConsumer { - #[\Override] - public function receive(IEvent $event) { - } -} --- a/tests/lib/AllConfigTest.php +++ /dev/null @@ -1,464 +0,0 @@ -connection->getQueryBuilder(); - $qb->insert('preferences') - ->values([ - 'userid' => $qb->createNamedParameter($userid), - 'appid' => $qb->createNamedParameter($appid), - 'configkey' => $qb->createNamedParameter($configkey), - 'configvalue' => $qb->createNamedParameter($configvalue), - ]) - ->executeStatement(); - } - - /** Return all oc_preferences rows for the given user */ - private function getPreferenceRows(string $userid): array { - $qb = $this->connection->getQueryBuilder(); - return $qb->select('userid', 'appid', 'configkey', 'configvalue') - ->from('preferences') - ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userid))) - ->executeQuery() - ->fetchAllAssociative(); - } - - /** Return the total number of rows in oc_preferences */ - private function countPreferenceRows(): int { - $qb = $this->connection->getQueryBuilder(); - return (int)$qb->select($qb->func()->count('*')) - ->from('preferences') - ->executeQuery() - ->fetchOne(); - } - - /** Truncate oc_preferences */ - private function clearPreferences(): void { - $this->connection->getQueryBuilder()->delete('preferences')->executeStatement(); - } - - protected function getConfig($systemConfig = null, $connection = null) { - if ($this->connection === null) { - $this->connection = Server::get(IDBConnection::class); - } - if ($connection === null) { - $connection = $this->connection; - } - if ($systemConfig === null) { - $systemConfig = $this->getMockBuilder('\OC\SystemConfig') - ->disableOriginalConstructor() - ->getMock(); - } - return new AllConfig($systemConfig, $connection); - } - - public function testDeleteUserValue(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userDelete', 'appDelete', 'keyDelete', 'valueDelete'); - - $config->deleteUserValue('userDelete', 'appDelete', 'keyDelete'); - - $actualCount = count($this->getPreferenceRows('userDelete')); - - $this->assertEquals(0, $actualCount, 'There was one value in the database and after the tests there should be no entry left.'); - } - - public function testSetUserValue(): void { - $config = $this->getConfig(); - - $config->setUserValue('userSet', 'appSet', 'keySet', 'valueSet'); - - $result = $this->getPreferenceRows('userSet'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userSet', - 'appid' => 'appSet', - 'configkey' => 'keySet', - 'configvalue' => 'valueSet' - ], $result[0]); - - // test if the method overwrites existing database entries - $config->setUserValue('userSet', 'appSet', 'keySet', 'valueSet2'); - - $result = $this->getPreferenceRows('userSet'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userSet', - 'appid' => 'appSet', - 'configkey' => 'keySet', - 'configvalue' => 'valueSet2' - ], $result[0]); - - // cleanup - it therefore relies on the successful execution of the previous test - $config->deleteUserValue('userSet', 'appSet', 'keySet'); - } - - /** - * This test needs to stay! Emails are expected to be lowercase due to performance reasons. - * This way we can skip the expensive casing change on the database. - */ - public function testSetUserValueSettingsEmail(): void { - $config = $this->getConfig(); - - $config->setUserValue('userSet', 'settings', 'email', 'mixed.CASE@domain.COM'); - - $result = $this->getPreferenceRows('userSet'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userSet', - 'appid' => 'settings', - 'configkey' => 'email', - 'configvalue' => 'mixed.case@domain.com' - ], $result[0]); - } - - public function testSetUserValueWithPreCondition(): void { - $config = $this->getConfig(); - - $config->setUserValue('userPreCond', 'appPreCond', 'keyPreCond', 'valuePreCond'); - - $result = $this->getPreferenceRows('userPreCond'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userPreCond', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond' - ], $result[0]); - - // test if the method overwrites existing database entries with valid precond - $config->setUserValue('userPreCond', 'appPreCond', 'keyPreCond', 'valuePreCond2', 'valuePreCond'); - - $result = $this->getPreferenceRows('userPreCond'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userPreCond', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond2' - ], $result[0]); - - // cleanup - $config->deleteUserValue('userPreCond', 'appPreCond', 'keyPreCond'); - } - - public static function dataSetUserValueUnexpectedValue(): array { - return [ - [true], - [false], - [null], - [new \stdClass()], - ]; - } - - /** - * @param mixed $value - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetUserValueUnexpectedValue')] - public function testSetUserValueUnexpectedValue($value): void { - $this->expectException(\UnexpectedValueException::class); - - $config = $this->getConfig(); - $config->setUserValue('userSetBool', 'appSetBool', 'keySetBool', $value); - } - - public function testSetUserValueWithPreConditionFailure(): void { - $this->expectException(PreConditionNotMetException::class); - - $config = $this->getConfig(); - - $config->setUserValue('userPreCond1', 'appPreCond', 'keyPreCond', 'valuePreCond'); - - $result = $this->getPreferenceRows('userPreCond1'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userPreCond1', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond' - ], $result[0]); - - // test if the method overwrites existing database entries with valid precond - $config->setUserValue('userPreCond1', 'appPreCond', 'keyPreCond', 'valuePreCond2', 'valuePreCond3'); - - $result = $this->getPreferenceRows('userPreCond1'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userPreCond1', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond' - ], $result[0]); - - // cleanup - $config->deleteUserValue('userPreCond1', 'appPreCond', 'keyPreCond'); - } - - public function testSetUserValueWithPreConditionFailureWhenResultStillMatches(): void { - $this->expectException(PreConditionNotMetException::class); - - $config = $this->getConfig(); - - $config->setUserValue('userPreCond1', 'appPreCond', 'keyPreCond', 'valuePreCond'); - - $result = $this->getPreferenceRows('userPreCond1'); - - $this->assertCount(1, $result); - $this->assertEquals([ - 'userid' => 'userPreCond1', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond' - ], $result[0]); - - // test if the method throws with invalid precondition when the value is the same - $config->setUserValue('userPreCond1', 'appPreCond', 'keyPreCond', 'valuePreCond', 'valuePreCond3'); - - $result = $this->getPreferenceRows('userPreCond1'); - - $this->assertCount(1, $result); - $this->assertEquals([ - 'userid' => 'userPreCond1', - 'appid' => 'appPreCond', - 'configkey' => 'keyPreCond', - 'configvalue' => 'valuePreCond' - ], $result[0]); - - // cleanup - $config->deleteUserValue('userPreCond1', 'appPreCond', 'keyPreCond'); - } - - public function testSetUserValueUnchanged(): void { - // TODO - FIXME until the dependency injection is handled properly (in AllConfig) - $this->markTestSkipped('Skipped because this is just testable if database connection can be injected'); - - $resultMock = $this->getMockBuilder('\Doctrine\DBAL\Driver\Statement') - ->disableOriginalConstructor()->getMock(); - $resultMock->expects($this->once()) - ->method('fetchOne') - ->willReturn('valueSetUnchanged'); - - $connectionMock = $this->createMock(IDBConnection::class); - $connectionMock->expects($this->once()) - ->method('executeQuery') - ->with($this->equalTo('SELECT `configvalue` FROM `*PREFIX*preferences` ' - . 'WHERE `userid` = ? AND `appid` = ? AND `configkey` = ?'), - $this->equalTo(['userSetUnchanged', 'appSetUnchanged', 'keySetUnchanged'])) - ->willReturn($resultMock); - $connectionMock->expects($this->never()) - ->method('executeStatement'); - - $config = $this->getConfig(null, $connectionMock); - - $config->setUserValue('userSetUnchanged', 'appSetUnchanged', 'keySetUnchanged', 'valueSetUnchanged'); - } - - public function testGetUserValue(): void { - $config = $this->getConfig(); - - // setup - it therefore relies on the successful execution of the previous test - $config->setUserValue('userGet', 'appGet', 'keyGet', 'valueGet'); - $value = $config->getUserValue('userGet', 'appGet', 'keyGet'); - - $this->assertEquals('valueGet', $value); - - $result = $this->getPreferenceRows('userGet'); - - $this->assertEquals(1, count($result)); - $this->assertEquals([ - 'userid' => 'userGet', - 'appid' => 'appGet', - 'configkey' => 'keyGet', - 'configvalue' => 'valueGet' - ], $result[0]); - - // drop data from database - but the config option should be cached in the config object - $qb = $this->connection->getQueryBuilder(); - $qb->delete('preferences') - ->where($qb->expr()->eq('userid', $qb->createNamedParameter('userGet'))) - ->executeStatement(); - - // testing the caching mechanism - $value = $config->getUserValue('userGet', 'appGet', 'keyGet'); - - $this->assertEquals('valueGet', $value); - - $this->assertEquals(0, count($this->getPreferenceRows('userGet'))); - } - - public function testGetUserKeys(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userFetch', 'appFetch1', 'keyFetch1', 'value1'); - $this->insertPreferenceRow('userFetch', 'appFetch1', 'keyFetch2', 'value2'); - $this->insertPreferenceRow('userFetch', 'appFetch2', 'keyFetch3', 'value3'); - $this->insertPreferenceRow('userFetch', 'appFetch1', 'keyFetch4', 'value4'); - $this->insertPreferenceRow('userFetch', 'appFetch4', 'keyFetch1', 'value5'); - $this->insertPreferenceRow('userFetch', 'appFetch5', 'keyFetch1', 'value6'); - $this->insertPreferenceRow('userFetch2', 'appFetch', 'keyFetch1', 'value7'); - - $value = $config->getUserKeys('userFetch', 'appFetch1'); - $this->assertEquals(['keyFetch1', 'keyFetch2', 'keyFetch4'], $value); - - $value = $config->getUserKeys('userFetch2', 'appFetch'); - $this->assertEquals(['keyFetch1'], $value); - - // cleanup - $this->clearPreferences(); - } - - public function testGetUserKeysAllInts(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userFetch8', 'appFetch1', '123', 'value'); - $this->insertPreferenceRow('userFetch8', 'appFetch1', '456', 'value'); - - $value = $config->getUserKeys('userFetch8', 'appFetch1'); - $this->assertEquals(['123', '456'], $value); - $this->assertIsString($value[0]); - $this->assertIsString($value[1]); - - // cleanup - $this->clearPreferences(); - } - - public function testGetUserValueDefault(): void { - $config = $this->getConfig(); - - $this->assertEquals('', $config->getUserValue('userGetUnset', 'appGetUnset', 'keyGetUnset')); - $this->assertEquals(null, $config->getUserValue('userGetUnset', 'appGetUnset', 'keyGetUnset', null)); - $this->assertEquals('foobar', $config->getUserValue('userGetUnset', 'appGetUnset', 'keyGetUnset', 'foobar')); - } - - public function testGetUserValueForUsers(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userFetch1', 'appFetch2', 'keyFetch1', 'value1'); - $this->insertPreferenceRow('userFetch2', 'appFetch2', 'keyFetch1', 'value2'); - $this->insertPreferenceRow('userFetch3', 'appFetch2', 'keyFetch1', '3'); - $this->insertPreferenceRow('userFetch4', 'appFetch2', 'keyFetch1', 'value4'); - $this->insertPreferenceRow('userFetch5', 'appFetch2', 'keyFetch1', 'value5'); - $this->insertPreferenceRow('userFetch6', 'appFetch2', 'keyFetch1', 'value6'); - $this->insertPreferenceRow('userFetch7', 'appFetch2', 'keyFetch1', 'value7'); - - $value = $config->getUserValueForUsers('appFetch2', 'keyFetch1', - ['userFetch1', 'userFetch2', 'userFetch3', 'userFetch5']); - $this->assertEquals([ - 'userFetch1' => 'value1', - 'userFetch2' => 'value2', - 'userFetch3' => 3, - 'userFetch5' => 'value5' - ], $value); - - $value = $config->getUserValueForUsers('appFetch2', 'keyFetch1', - ['userFetch1', 'userFetch4', 'userFetch9']); - $this->assertEquals([ - 'userFetch1' => 'value1', - 'userFetch4' => 'value4' - ], $value, 'userFetch9 is an non-existent user and should not be shown.'); - - // cleanup - $this->clearPreferences(); - } - - public function testDeleteAllUserValues(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userFetch3', 'appFetch1', 'keyFetch1', 'value1'); - $this->insertPreferenceRow('userFetch3', 'appFetch1', 'keyFetch2', 'value2'); - $this->insertPreferenceRow('userFetch3', 'appFetch2', 'keyFetch3', 'value3'); - $this->insertPreferenceRow('userFetch3', 'appFetch1', 'keyFetch4', 'value4'); - $this->insertPreferenceRow('userFetch3', 'appFetch4', 'keyFetch1', 'value5'); - $this->insertPreferenceRow('userFetch3', 'appFetch5', 'keyFetch1', 'value6'); - $this->insertPreferenceRow('userFetch4', 'appFetch2', 'keyFetch1', 'value7'); - - $config->deleteAllUserValues('userFetch3'); - - $this->assertEquals(1, $this->countPreferenceRows(), 'After removing `userFetch3` there should be exactly 1 entry left.'); - - // cleanup - $this->clearPreferences(); - } - - public function testDeleteAppFromAllUsers(): void { - $config = $this->getConfig(); - - // preparation - add something to the database - $this->insertPreferenceRow('userFetch5', 'appFetch1', 'keyFetch1', 'value1'); - $this->insertPreferenceRow('userFetch5', 'appFetch1', 'keyFetch2', 'value2'); - $this->insertPreferenceRow('userFetch5', 'appFetch2', 'keyFetch3', 'value3'); - $this->insertPreferenceRow('userFetch5', 'appFetch1', 'keyFetch4', 'value4'); - $this->insertPreferenceRow('userFetch5', 'appFetch4', 'keyFetch1', 'value5'); - $this->insertPreferenceRow('userFetch5', 'appFetch5', 'keyFetch1', 'value6'); - $this->insertPreferenceRow('userFetch6', 'appFetch2', 'keyFetch1', 'value7'); - - $config->deleteAppFromAllUsers('appFetch1'); - - $this->assertEquals(4, $this->countPreferenceRows(), 'After removing `appFetch1` there should be exactly 4 entries left.'); - - $config->deleteAppFromAllUsers('appFetch2'); - - $this->assertEquals(2, $this->countPreferenceRows(), 'After removing `appFetch2` there should be exactly 2 entries left.'); - - // cleanup - $this->clearPreferences(); - } - - public function testGetUsersForUserValue(): void { - // mock the check for the database to run the correct SQL statements for each database type - $systemConfig = $this->getMockBuilder('\OC\SystemConfig') - ->disableOriginalConstructor() - ->getMock(); - $config = $this->getConfig($systemConfig); - - // preparation - add something to the database - $this->insertPreferenceRow('user1', 'appFetch9', 'keyFetch9', 'value9'); - $this->insertPreferenceRow('user2', 'appFetch9', 'keyFetch9', 'value9'); - $this->insertPreferenceRow('user3', 'appFetch9', 'keyFetch9', 'value8'); - $this->insertPreferenceRow('user4', 'appFetch9', 'keyFetch8', 'value9'); - $this->insertPreferenceRow('user5', 'appFetch8', 'keyFetch9', 'value9'); - $this->insertPreferenceRow('user6', 'appFetch9', 'keyFetch9', 'value9'); - - $value = $config->getUsersForUserValue('appFetch9', 'keyFetch9', 'value9'); - $this->assertEquals(['user1', 'user2', 'user6'], $value); - - // cleanup - $this->clearPreferences(); - } -} --- a/tests/lib/App/AppManagerTest.php +++ /dev/null @@ -1,934 +0,0 @@ -createMock(AppConfig::class); - - $config->expects($this->any()) - ->method('getValue') - ->willReturnCallback(function ($app, $key, $default) use (&$appConfig) { - return (isset($appConfig[$app]) && isset($appConfig[$app][$key])) - ? $appConfig[$app][$key] - : $default; - }); - $config->expects($this->any()) - ->method('setValue') - ->willReturnCallback(function ($app, $key, $value) use (&$appConfig): void { - if (!isset($appConfig[$app])) { - $appConfig[$app] = []; - } - $appConfig[$app][$key] = $value; - }); - $config->expects($this->any()) - ->method('getValues') - ->willReturnCallback(function ($app, $key) use (&$appConfig) { - if ($app) { - return $appConfig[$app]; - } else { - $values = []; - foreach ($appConfig as $appid => $appData) { - if (isset($appData[$key])) { - $values[$appid] = $appData[$key]; - } - } - return $values; - } - }); - $config->expects($this->any()) - ->method('searchValues') - ->willReturnCallback(function ($key, $lazy, $type) use (&$appConfig) { - $values = []; - foreach ($appConfig as $appid => $appData) { - if (isset($appData[$key])) { - $values[$appid] = $appData[$key]; - } - } - return $values; - }); - - return $config; - } - - protected IUserSession&MockObject $userSession; - private IConfig&MockObject $config; - protected IGroupManager&MockObject $groupManager; - protected AppConfig&MockObject $appConfig; - protected ICache&MockObject $cache; - protected ICacheFactory&MockObject $cacheFactory; - protected IEventDispatcher&MockObject $eventDispatcher; - protected LoggerInterface&MockObject $logger; - protected IURLGenerator&MockObject $urlGenerator; - protected ServerVersion&MockObject $serverVersion; - protected ConfigManager&MockObject $configManager; - - protected DependencyAnalyzer $dependencyAnalyzer; - - protected AppManager $manager; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->userSession = $this->createMock(IUserSession::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->config = $this->createMock(IConfig::class); - $this->appConfig = $this->getAppConfig(); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cache = $this->createMock(ICache::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->serverVersion = $this->createMock(ServerVersion::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->dependencyAnalyzer = new DependencyAnalyzer($this->createMock(Platform::class)); - - $this->overwriteService(AppConfig::class, $this->appConfig); - $this->overwriteService(IURLGenerator::class, $this->urlGenerator); - - $this->cacheFactory->expects($this->any()) - ->method('createDistributed') - ->with('settings') - ->willReturn($this->cache); - - $this->config - ->method('getSystemValueBool') - ->with('installed', false) - ->willReturn(true); - - $this->manager = new AppManager( - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ); - } - - #[DataProvider('dataGetAppIcon')] - public function testGetAppIcon($callback, ?bool $dark, ?string $expected): void { - $this->urlGenerator->expects($this->atLeastOnce()) - ->method('imagePath') - ->willReturnCallback($callback); - - if ($dark !== null) { - $this->assertEquals($expected, $this->manager->getAppIcon('test', $dark)); - } else { - $this->assertEquals($expected, $this->manager->getAppIcon('test')); - } - } - - public static function dataGetAppIcon(): array { - $nothing = function ($appId): void { - self::assertEquals('test', $appId); - throw new \RuntimeException(); - }; - - $createCallback = function ($workingIcons) { - return function ($appId, $icon) use ($workingIcons) { - self::assertEquals('test', $appId); - if (in_array($icon, $workingIcons)) { - return '/path/' . $icon; - } - throw new \RuntimeException(); - }; - }; - - return [ - 'does not find anything' => [ - $nothing, - false, - null, - ], - 'nothing if request dark but only bright available' => [ - $createCallback(['app.svg']), - true, - null, - ], - 'nothing if request bright but only dark available' => [ - $createCallback(['app-dark.svg']), - false, - null, - ], - 'bright and only app.svg' => [ - $createCallback(['app.svg']), - false, - '/path/app.svg', - ], - 'dark and only app-dark.svg' => [ - $createCallback(['app-dark.svg']), - true, - '/path/app-dark.svg', - ], - 'dark only appname -dark.svg' => [ - $createCallback(['test-dark.svg']), - true, - '/path/test-dark.svg', - ], - 'bright and only appname.svg' => [ - $createCallback(['test.svg']), - false, - '/path/test.svg', - ], - 'priotize custom over default' => [ - $createCallback(['app.svg', 'test.svg']), - false, - '/path/test.svg', - ], - 'defaults to bright' => [ - $createCallback(['test-dark.svg', 'test.svg']), - null, - '/path/test.svg', - ], - 'no dark icon on default' => [ - $createCallback(['test-dark.svg', 'test.svg', 'app-dark.svg', 'app.svg']), - false, - '/path/test.svg', - ], - 'no bright icon on dark' => [ - $createCallback(['test-dark.svg', 'test.svg', 'app-dark.svg', 'app.svg']), - true, - '/path/test-dark.svg', - ], - ]; - } - - public function testEnableApp(): void { - // making sure "files_trashbin" is disabled - if ($this->manager->isEnabledForUser('files_trashbin')) { - $this->manager->disableApp('files_trashbin'); - } - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('files_trashbin')); - - $this->manager->enableApp('files_trashbin'); - $this->assertEquals('yes', $this->appConfig->getValue('files_trashbin', 'enabled', 'no')); - } - - public function testDisableApp(): void { - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppDisableEvent('files_trashbin')); - - $this->manager->disableApp('files_trashbin'); - $this->assertEquals('no', $this->appConfig->getValue('files_trashbin', 'enabled', 'no')); - } - - public function testNotEnableIfNotInstalled(): void { - $this->expectException(AppPathNotFoundException::class); - $this->expectExceptionMessage('Could not find path for some_random_name_which_i_hope_is_not_an_app'); - $this->appConfig->expects(self::never()) - ->method('setValue'); - - $this->manager->enableApp('some_random_name_which_i_hope_is_not_an_app'); - } - - public function testEnableAppForGroups(): void { - $group1 = $this->createMock(IGroup::class); - $group1->method('getGID') - ->willReturn('group1'); - $group2 = $this->createMock(IGroup::class); - $group2->method('getGID') - ->willReturn('group2'); - - $groups = [$group1, $group2]; - - /** @var AppManager|MockObject $manager */ - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppPath', - ]) - ->getMock(); - - $manager->expects($this->exactly(2)) - ->method('getAppPath') - ->with('test') - ->willReturn('apps/test'); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new AppEnableEvent('test', ['group1', 'group2'])); - - $manager->enableAppForGroups('test', $groups); - $this->assertEquals('["group1","group2"]', $this->appConfig->getValue('test', 'enabled', 'no')); - } - - public static function dataEnableAppForGroupsAllowedTypes(): array { - return [ - [[]], - [[ - 'types' => [], - ]], - [[ - 'types' => ['nickvergessen'], - ]], - ]; - } - - /** - * @param array $appInfo - */ - #[DataProvider('dataEnableAppForGroupsAllowedTypes')] - public function testEnableAppForGroupsAllowedTypes(array $appInfo): void { - $group1 = $this->createMock(IGroup::class); - $group1->method('getGID') - ->willReturn('group1'); - $group2 = $this->createMock(IGroup::class); - $group2->method('getGID') - ->willReturn('group2'); - - $groups = [$group1, $group2]; - - /** @var AppManager|MockObject $manager */ - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppPath', - 'getAppInfo', - ]) - ->getMock(); - - $manager->expects($this->once()) - ->method('getAppPath') - ->with('test') - ->willReturn(''); - - $manager->expects($this->once()) - ->method('getAppInfo') - ->with('test') - ->willReturn($appInfo); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('test', ['group1', 'group2'])); - - $manager->enableAppForGroups('test', $groups); - $this->assertEquals('["group1","group2"]', $this->appConfig->getValue('test', 'enabled', 'no')); - } - - public static function dataEnableAppForGroupsForbiddenTypes(): array { - return [ - ['filesystem'], - ['prelogin'], - ['authentication'], - ['logging'], - ['prevent_group_restriction'], - ]; - } - - /** - * - * @param string $type - * - */ - #[DataProvider('dataEnableAppForGroupsForbiddenTypes')] - public function testEnableAppForGroupsForbiddenTypes($type): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('test can\'t be enabled for groups.'); - - $group1 = $this->createMock(IGroup::class); - $group1->method('getGID') - ->willReturn('group1'); - $group2 = $this->createMock(IGroup::class); - $group2->method('getGID') - ->willReturn('group2'); - - $groups = [$group1, $group2]; - - /** @var AppManager|MockObject $manager */ - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppPath', - 'getAppInfo', - ]) - ->getMock(); - - $manager->expects($this->once()) - ->method('getAppPath') - ->with('test') - ->willReturn(''); - - $manager->expects($this->once()) - ->method('getAppInfo') - ->with('test') - ->willReturn([ - 'types' => [$type], - ]); - - $this->eventDispatcher->expects($this->never())->method('dispatchTyped')->with(new AppEnableEvent('test', ['group1', 'group2'])); - - $manager->enableAppForGroups('test', $groups); - } - - public function testIsInstalledEnabled(): void { - $this->appConfig->setValue('test', 'enabled', 'yes'); - $this->assertTrue($this->manager->isEnabledForAnyone('test')); - } - - public function testIsInstalledDisabled(): void { - $this->appConfig->setValue('test', 'enabled', 'no'); - $this->assertFalse($this->manager->isEnabledForAnyone('test')); - } - - public function testIsInstalledEnabledForGroups(): void { - $this->appConfig->setValue('test', 'enabled', '["foo"]'); - $this->assertTrue($this->manager->isEnabledForAnyone('test')); - } - - private function newUser($uid) { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn($uid); - - return $user; - } - - public function testIsEnabledForUserEnabled(): void { - $this->appConfig->setValue('test', 'enabled', 'yes'); - $user = $this->newUser('user1'); - $this->assertTrue($this->manager->isEnabledForUser('test', $user)); - } - - public function testIsEnabledForUserDisabled(): void { - $this->appConfig->setValue('test', 'enabled', 'no'); - $user = $this->newUser('user1'); - $this->assertFalse($this->manager->isEnabledForUser('test', $user)); - } - - public function testGetAppPath(): void { - $this->assertEquals(\OC::$SERVERROOT . '/apps/files', $this->manager->getAppPath('files')); - } - - public function testGetAppPathSymlink(): void { - $fakeAppDirname = sha1(uniqid('test', true)); - $fakeAppPath = sys_get_temp_dir() . '/' . $fakeAppDirname; - $fakeAppLink = \OC::$SERVERROOT . '/' . $fakeAppDirname; - - mkdir($fakeAppPath); - if (symlink($fakeAppPath, $fakeAppLink) === false) { - $this->markTestSkipped('Failed to create symlink'); - } - - // Use the symlink as the app path - \OC::$APPSROOTS[] = [ - 'path' => $fakeAppLink, - 'url' => \OC::$WEBROOT . '/' . $fakeAppDirname, - 'writable' => false, - ]; - - $fakeTestAppPath = $fakeAppPath . '/' . 'test_test_app'; - mkdir($fakeTestAppPath); - - $generatedAppPath = $this->manager->getAppPath('test_test_app'); - - rmdir($fakeTestAppPath); - unlink($fakeAppLink); - rmdir($fakeAppPath); - - $this->assertEquals($fakeAppLink . '/test_test_app', $generatedAppPath); - } - - public function testGetAppPathFail(): void { - $this->expectException(AppPathNotFoundException::class); - $this->manager->getAppPath('testnotexisting'); - } - - public function testIsEnabledForUserEnabledForGroup(): void { - $user = $this->newUser('user1'); - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn(['foo', 'bar']); - - $this->appConfig->setValue('test', 'enabled', '["foo"]'); - $this->assertTrue($this->manager->isEnabledForUser('test', $user)); - } - - public function testIsEnabledForUserDisabledForGroup(): void { - $user = $this->newUser('user1'); - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn(['bar']); - - $this->appConfig->setValue('test', 'enabled', '["foo"]'); - $this->assertFalse($this->manager->isEnabledForUser('test', $user)); - } - - public function testIsEnabledForUserLoggedOut(): void { - $this->appConfig->setValue('test', 'enabled', '["foo"]'); - $this->assertFalse($this->manager->isEnabledForUser('test')); - } - - public function testIsEnabledForUserLoggedIn(): void { - $user = $this->newUser('user1'); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn(['foo', 'bar']); - - $this->appConfig->setValue('test', 'enabled', '["foo"]'); - $this->assertTrue($this->manager->isEnabledForUser('test')); - } - - public function testGetEnabledApps(): void { - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'enabled', 'no'); - $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - $apps = [ - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'files', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'test1', - 'test3', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ]; - $this->assertEquals($apps, $this->manager->getEnabledApps()); - } - - public function testGetAppsForUser(): void { - $user = $this->newUser('user1'); - $this->groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn(['foo', 'bar']); - - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'enabled', 'no'); - $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - $this->appConfig->setValue('test4', 'enabled', '["asd"]'); - $enabled = [ - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'files', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'test1', - 'test3', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ]; - $this->assertEquals($enabled, $this->manager->getEnabledAppsForUser($user)); - } - - public function testGetAppsNeedingUpgrade(): void { - /** @var AppManager&MockObject $manager */ - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods(['getAppInfo']) - ->getMock(); - - $appInfos = [ - 'appstore' => ['id' => 'appstore'], - 'cloud_federation_api' => ['id' => 'cloud_federation_api'], - 'dav' => ['id' => 'dav'], - 'files' => ['id' => 'files'], - 'federatedfilesharing' => ['id' => 'federatedfilesharing'], - 'profile' => ['id' => 'profile'], - 'provisioning_api' => ['id' => 'provisioning_api'], - 'lookup_server_connector' => ['id' => 'lookup_server_connector'], - 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '9.0.0'], - 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], - 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], - 'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'], - 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], - 'settings' => ['id' => 'settings'], - 'sharing' => ['id' => 'sharing'], - 'theming' => ['id' => 'theming'], - 'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'], - 'viewer' => ['id' => 'viewer'], - 'workflowengine' => ['id' => 'workflowengine'], - 'oauth2' => ['id' => 'oauth2'], - ]; - - $manager->expects($this->any()) - ->method('getAppInfo') - ->willReturnCallback( - function ($appId) use ($appInfos) { - return $appInfos[$appId]; - } - ); - - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test1', 'installed_version', '1.0.0'); - $this->appConfig->setValue('test2', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'installed_version', '1.0.0'); - $this->appConfig->setValue('test3', 'enabled', 'yes'); - $this->appConfig->setValue('test3', 'installed_version', '1.0.0'); - $this->appConfig->setValue('test4', 'enabled', 'yes'); - $this->appConfig->setValue('test4', 'installed_version', '2.4.0'); - - $apps = $manager->getAppsNeedingUpgrade('8.2.0'); - - $this->assertCount(2, $apps); - $this->assertEquals('test1', $apps[0]['id']); - $this->assertEquals('test4', $apps[1]['id']); - } - - public function testGetIncompatibleApps(): void { - /** @var AppManager|MockObject $manager */ - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods(['getAppInfo']) - ->getMock(); - - $appInfos = [ - 'appstore' => ['id' => 'appstore'], - 'cloud_federation_api' => ['id' => 'cloud_federation_api'], - 'dav' => ['id' => 'dav'], - 'files' => ['id' => 'files'], - 'federatedfilesharing' => ['id' => 'federatedfilesharing'], - 'profile' => ['id' => 'profile'], - 'provisioning_api' => ['id' => 'provisioning_api'], - 'lookup_server_connector' => ['id' => 'lookup_server_connector'], - 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '8.0.0'], - 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], - 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], - 'settings' => ['id' => 'settings'], - 'sharing' => ['id' => 'sharing'], - 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], - 'theming' => ['id' => 'theming'], - 'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'], - 'workflowengine' => ['id' => 'workflowengine'], - 'oauth2' => ['id' => 'oauth2'], - 'viewer' => ['id' => 'viewer'], - ]; - - $manager->expects($this->any()) - ->method('getAppInfo') - ->willReturnCallback( - function ($appId) use ($appInfos) { - return $appInfos[$appId]; - } - ); - - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'enabled', 'yes'); - $this->appConfig->setValue('test3', 'enabled', 'yes'); - - $apps = $manager->getIncompatibleApps('8.2.0'); - - $this->assertCount(2, $apps); - $this->assertEquals('test1', $apps[0]['id']); - $this->assertEquals('test3', $apps[1]['id']); - } - - public function testGetEnabledAppsForGroup(): void { - $group = $this->createMock(IGroup::class); - $group->expects($this->any()) - ->method('getGID') - ->willReturn('foo'); - - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'enabled', 'no'); - $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - $this->appConfig->setValue('test4', 'enabled', '["asd"]'); - $enabled = [ - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'files', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'test1', - 'test3', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ]; - $this->assertEquals($enabled, $this->manager->getEnabledAppsForGroup($group)); - } - - public function testGetAppRestriction(): void { - $this->appConfig->setValue('test1', 'enabled', 'yes'); - $this->appConfig->setValue('test2', 'enabled', 'no'); - $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - - $this->assertEquals([], $this->manager->getAppRestriction('test1')); - $this->assertEquals([], $this->manager->getAppRestriction('test2')); - $this->assertEquals(['foo'], $this->manager->getAppRestriction('test3')); - } - - public static function isBackendRequiredDataProvider(): array { - return [ - // backend available - [ - 'caldav', - ['app1' => ['caldav']], - true, - ], - [ - 'caldav', - ['app1' => [], 'app2' => ['foo'], 'app3' => ['caldav']], - true, - ], - // backend not available - [ - 'caldav', - ['app3' => [], 'app1' => ['foo'], 'app2' => ['bar', 'baz']], - false, - ], - // no app available - [ - 'caldav', - [], - false, - ], - ]; - } - - #[DataProvider('isBackendRequiredDataProvider')] - public function testIsBackendRequired( - string $backend, - array $appBackends, - bool $expected, - ): void { - $appInfoData = array_map( - static fn (array $backends) => ['dependencies' => ['backend' => $backends]], - $appBackends, - ); - - $reflection = new \ReflectionClass($this->manager); - $property = $reflection->getProperty('appInfos'); - $property->setValue($this->manager, $appInfoData); - - $this->assertEquals($expected, $this->manager->isBackendRequired($backend)); - } - - public function testGetAppVersion() { - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppInfo', - ]) - ->getMock(); - - $manager->expects(self::once()) - ->method('getAppInfo') - ->with('myapp') - ->willReturn(['version' => '99.99.99-rc.99']); - - $this->serverVersion - ->expects(self::never()) - ->method('getVersionString'); - - $this->assertEquals( - '99.99.99-rc.99', - $manager->getAppVersion('myapp'), - ); - } - - public function testGetAppVersionCore() { - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppInfo', - ]) - ->getMock(); - - $manager->expects(self::never()) - ->method('getAppInfo'); - - $this->serverVersion - ->expects(self::once()) - ->method('getVersionString') - ->willReturn('1.2.3-beta.4'); - - $this->assertEquals( - '1.2.3-beta.4', - $manager->getAppVersion('core'), - ); - } - - public function testGetAppVersionUnknown() { - $manager = $this->getMockBuilder(AppManager::class) - ->setConstructorArgs([ - $this->userSession, - $this->config, - $this->groupManager, - $this->cacheFactory, - $this->eventDispatcher, - $this->logger, - $this->serverVersion, - $this->configManager, - $this->dependencyAnalyzer, - ]) - ->onlyMethods([ - 'getAppInfo', - ]) - ->getMock(); - - $manager->expects(self::once()) - ->method('getAppInfo') - ->with('unknown') - ->willReturn(null); - - $this->serverVersion - ->expects(self::never()) - ->method('getVersionString'); - - $this->assertEquals( - '0', - $manager->getAppVersion('unknown'), - ); - } - - public static function dataCleanAppId(): array { - return [ - ['simple', 'simple'], - ['UPPERCASEa', 'a'], - ['MixEdCaSe', 'ixdae'], - ['007startwithdigit', 'startwithdigit'], - ['0-numb3rs-4ll0w3d-1n-m1ddle-0', 'numb3rs-4ll0w3d-1n-m1ddle-0'], - ['hyphen-and_underscore_allowed', 'hyphen-and_underscore_allowed'], - ['_but-not-at-the-end_', 'but-not-at-the-end'], - ['-but-not-at-the-end-', 'but-not-at-the-end'], - ['--_but-not-at-the-end___', 'but-not-at-the-end'], - [' also remove all spaces', 'alsoremoveallspaces'], - ['a«"«»()@+-/*=%\{}…~|&œ—<>[]^±_−÷×≠‰A', 'a-_'], - ]; - } - - #[DataProvider('dataCleanAppId')] - public function testCleanAppId(string $inputString, string $appid): void { - $this->assertEquals($appid, $this->manager->cleanAppId($inputString)); - } -} --- a/tests/lib/App/AppStore/AppStoreLinkVisibilityTest.php +++ /dev/null @@ -1,123 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->registry = $this->createMock(IRegistry::class); - - $this->visibility = new AppStoreLinkVisibility( - $this->config, - $this->appConfig, - $this->registry, - ); - } - - /** - * @param bool $stored whether an admin set the config key - * @param bool $value the stored value, or the lexicon default when $stored is false - */ - private function arrange(bool $stored, bool $value, bool $appStoreEnabled, bool $subscription): void { - $this->appConfig->method('hasKey') - ->with('core', ConfigLexicon::APPSTORE_LINK_SHOWN) - ->willReturn($stored); - $this->appConfig->method('getValueBool') - ->with('core', ConfigLexicon::APPSTORE_LINK_SHOWN) - ->willReturn($value); - $this->config->method('getSystemValueBool') - ->with('appstoreenabled', true) - ->willReturn($appStoreEnabled); - $this->registry->method('delegateHasValidSubscription') - ->willReturn($subscription); - } - - public static function dataBool(): array { - return [ - 'shown' => [true], - 'hidden' => [false], - ]; - } - - #[DataProvider('dataBool')] - public function testStoredValueWinsOverSubscriptionAndDisabledAppStore(bool $stored): void { - $this->arrange(stored: true, value: $stored, appStoreEnabled: false, subscription: true); - - self::assertSame($stored, $this->visibility->isShownToUsers()); - } - - public function testHiddenWhenAppStoreIsDisabled(): void { - $this->arrange(stored: false, value: true, appStoreEnabled: false, subscription: false); - - self::assertFalse($this->visibility->isShownToUsers()); - } - - public function testHiddenWhenSubscriptionIsAvailable(): void { - $this->arrange(stored: false, value: true, appStoreEnabled: true, subscription: true); - - self::assertFalse($this->visibility->isShownToUsers()); - } - - #[DataProvider('dataBool')] - public function testUnstoredKeyReturnsTheLexiconDefault(bool $default): void { - $this->arrange(stored: false, value: $default, appStoreEnabled: true, subscription: false); - - self::assertSame($default, $this->visibility->isShownToUsers()); - } - - public static function dataLexiconPreset(): array { - return [ - // Existing instances run without a preset and must keep the link. - [Preset::NONE, '1'], - [Preset::FAMILY, '1'], - [Preset::UNIVERSITY, '0'], - ]; - } - - /** - * The lexicon default {@see AppStoreLinkVisibility} falls back to is preset - * dependent. A fresh entry is built per case because {@see Entry::getDefault()} - * memoizes the first default it is asked for. - */ - #[DataProvider('dataLexiconPreset')] - public function testLexiconPresetDefault(Preset $preset, string $expected): void { - self::assertSame($expected, $this->lexiconEntry()->getDefault($preset)); - } - - private function lexiconEntry(): Entry { - foreach ((new ConfigLexicon())->getAppConfigs() as $entry) { - if ($entry->getKey() === ConfigLexicon::APPSTORE_LINK_SHOWN) { - return $entry; - } - } - - self::fail('No lexicon entry for ' . ConfigLexicon::APPSTORE_LINK_SHOWN); - } -} --- a/tests/lib/App/AppStore/Bundles/BundleBase.php +++ /dev/null @@ -1,45 +0,0 @@ -l10n = $this->createMock(IL10N::class); - $this->l10n->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - } - - public function testGetIdentifier(): void { - $this->assertSame($this->bundleIdentifier, $this->bundle->getIdentifier()); - } - - public function testGetName(): void { - $this->assertSame($this->bundleName, $this->bundle->getName()); - } - - public function testGetAppIdentifiers(): void { - $this->assertSame($this->bundleAppIds, $this->bundle->getAppIdentifiers()); - } -} --- a/tests/lib/App/AppStore/Bundles/BundleFetcherTest.php +++ /dev/null @@ -1,62 +0,0 @@ -l10n = $this->createMock(IL10N::class); - $factory = $this->createMock(IFactory::class); - $factory->method('get')->willReturn($this->l10n); - - $this->bundleFetcher = new BundleFetcher($factory); - } - - public function testGetBundles(): void { - $expected = [ - new EnterpriseBundle($this->l10n), - new HubBundle($this->l10n), - new GroupwareBundle($this->l10n), - new SocialSharingBundle($this->l10n), - new EducationBundle($this->l10n), - new PublicSectorBundle($this->l10n), - ]; - $this->assertEquals($expected, $this->bundleFetcher->getBundles()); - } - - public function testGetBundleByIdentifier(): void { - $this->assertEquals(new EnterpriseBundle($this->l10n), $this->bundleFetcher->getBundleByIdentifier('EnterpriseBundle')); - $this->assertEquals(new GroupwareBundle($this->l10n), $this->bundleFetcher->getBundleByIdentifier('GroupwareBundle')); - } - - public function testGetBundleByIdentifierWithException(): void { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Bundle with specified identifier does not exist'); - - $this->bundleFetcher->getBundleByIdentifier('NotExistingBundle'); - } -} --- a/tests/lib/App/AppStore/Bundles/EducationBundleTest.php +++ /dev/null @@ -1,31 +0,0 @@ -bundle = new EducationBundle($this->l10n); - $this->bundleIdentifier = 'EducationBundle'; - $this->bundleName = 'Education bundle'; - $this->bundleAppIds = [ - 'dashboard', - 'circles', - 'groupfolders', - 'announcementcenter', - 'quota_warning', - 'user_saml', - 'whiteboard', - ]; - } -} --- a/tests/lib/App/AppStore/Bundles/EnterpriseBundleTest.php +++ /dev/null @@ -1,31 +0,0 @@ -bundle = new EnterpriseBundle($this->l10n); - $this->bundleIdentifier = 'EnterpriseBundle'; - $this->bundleName = 'Enterprise bundle'; - $this->bundleAppIds = [ - 'admin_audit', - 'user_ldap', - 'files_retention', - 'files_automatedtagging', - 'user_saml', - 'files_accesscontrol', - 'terms_of_service', - ]; - } -} --- a/tests/lib/App/AppStore/Bundles/GroupwareBundleTest.php +++ /dev/null @@ -1,28 +0,0 @@ -bundle = new GroupwareBundle($this->l10n); - $this->bundleIdentifier = 'GroupwareBundle'; - $this->bundleName = 'Groupware bundle'; - $this->bundleAppIds = [ - 'calendar', - 'contacts', - 'deck', - 'mail' - ]; - } -} --- a/tests/lib/App/AppStore/Bundles/SocialSharingBundleTest.php +++ /dev/null @@ -1,28 +0,0 @@ -bundle = new SocialSharingBundle($this->l10n); - $this->bundleIdentifier = 'SocialSharingBundle'; - $this->bundleName = 'Social sharing bundle'; - $this->bundleAppIds = [ - 'socialsharing_twitter', - 'socialsharing_facebook', - 'socialsharing_email', - 'socialsharing_diaspora', - ]; - } -} --- a/tests/lib/App/AppStore/Fetcher/AppDiscoverFetcherTest.php +++ /dev/null @@ -1,118 +0,0 @@ -fileName = 'discover.json'; - $this->endpoint = 'https://apps.nextcloud.com/api/v1/discover.json'; - - $this->compareVersion = $this->createMock(CompareVersion::class); - - $this->fetcher = new AppDiscoverFetcher( - $this->appDataFactory, - $this->clientService, - $this->timeFactory, - $this->config, - $this->logger, - $this->registry, - $this->compareVersion, - ); - } - - public function testAppstoreDisabled(): void { - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreenabled') { - return false; - } - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } - - public function testNoInternet(): void { - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'has_internet_connection') { - return false; - } - return $default; - }); - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetETag')] - public function testGetEtag(?string $expected, bool $throws, string $content = ''): void { - $folder = $this->createMock(ISimpleFolder::class); - if (!$throws) { - $file = $this->createMock(ISimpleFile::class); - $file->expects($this->once()) - ->method('getContent') - ->willReturn($content); - $folder->expects($this->once()) - ->method('getFile') - ->with('discover.json') - ->willReturn($file); - } else { - $folder->expects($this->once()) - ->method('getFile') - ->with('discover.json') - ->willThrowException(new NotFoundException('')); - } - - $this->appData->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - - $etag = $this->fetcher->getETag(); - $this->assertEquals($expected, $etag); - if ($expected !== null) { - $this->assertTrue(gettype($etag) === 'string'); - } - } - - public static function dataGetETag(): array { - return [ - 'file not found' => [null, true], - 'empty file' => [null, false, ''], - 'missing etag' => [null, false, '{ "foo": "bar" }'], - 'valid etag' => ['test', false, '{ "ETag": "test" }'], - 'numeric etag' => ['132', false, '{ "ETag": 132 }'], - ]; - } -} --- a/tests/lib/App/AppStore/Fetcher/AppFetcherTest.php +++ /dev/null @@ -1,2252 +0,0 @@ -=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/juliushaertl/direct_menu/releases/download/v0.9.2/direct_menu.tar.gz","created":"2016-10-06T14:01:05.578297Z","licenses":["agpl"],"lastModified":"2016-10-06T14:01:05.643813Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"ERBS9G5bZ3vwCizz2Ht5DehsVJmb63bzF3aYcH7xjbDVMPagOFWdUAiLDwTeZR1n\ni4gdZ73J/IjHQQJoOPwtCjgbZgLPFqL5x13CLUO9mb/33dZe/+gqEDc/3AuJ4TlA\nXUdLxHRb1bwIlJOwuSr/E24452VG20WUhLXBoM0Zm7WcMxvJWo2zAWnuqnLX3dy9\ncPB4PX+6JU2lUMINj8OYQmM1QnqvjG8YV0cYHbBbnSicOGwXEnni7mojsC8T0cn7\nYEJ2O2iO9hh3fvFEXUzDcL7tDQ5bZqm63Oa991bsAJxFo/RbzeJRh//DcOrd8Ufn\nu2SqRhwybS8j4YvfjAL9RPdRfPLwf6X2gx/Y6QFrKHH0QMI/9J/ZFyoUQcqKbsHV\n85O+yuWoqVmza71tkp4n9PuMdprCinaVvHbHbNGUf2SIh9BWuEQuVvvnvB+ZW8XY\n+Cl+unzk3WgOgT0iY3uEmsQcrLIo4DSKhcNgD1NS13fR/JTSavvmOqBarUMFZfVC\nbkR1DTBCyDjdpNBidpa3/26675dz5IT5Zedp4BBBREQzX08cIhJx5mgqDdX3CU09\nuWtzoaLi71/1BWTFAN+Y9VyfZ8/Z3Pg3vKedRJ565mztIj0geL3riEsC5YnPS0+C\n+a3B9sDiiOa101EORzX3lrPqL7reEPdCxrIwN+hKFBQ=","translations":{}}],"screenshots":[{"url":"https://bitgrid.net/~jus/direct_menu_nc.png"}],"translations":{"en":{"name":"Direct Menu","summary":"Provide easy access to all apps in the header.","description":"Provide easy access to all apps in the header."}},"isFeatured":false,"authors":[{"name":"Julius Härtl","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEBjCCAu4CAhADMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwOTE0MTI1MDU0WhcNMjYxMjIxMTI1MDU0WjAWMRQwEgYD\r\nVQQDDAtkaXJlY3RfbWVudTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\r\nAMkzWsAkKP/40ktvJMpnr0IJNVoPOR0hvh24igcDskL1WKiD2eiRUenj5LE0Nvn+\r\nsiGmWsAqRVpdiz+Y8ghQqQMzKi43IrRN0AxlCrHWrSqBZT3wIAUcFz4RzEoFxc1N\r\nUZzWma6ljukGnvt4V1ZyT+H/cjqxUkBhh/y9SS0jUen1a1grND6Rw54X46V2dlCu\r\nFH+pLsfPJJGw+QLeTGHn7dqdv18cYMAlWDCzPixVnNiCXHZcUtKMmstU2xU4R2e6\r\nzimp2rgkE4TNHrafpjH8xGdNi2FG5Dmokob/L5Q2r8jyNaW7UsFfrvLTRj371b3/\r\n2FhhxoGUvDzaG2An02Exwm52LJfdTVMHAMPZub5poHfy5vAEdZGPQ/m02l8ZK/Y2\r\n7yT807GlfPMXfdfjCxR6wNtmv7rvBDdrUZmIRNJfpFSdvlH/+MOTWnabyfQv2K4Q\r\nBIwltX6Elh0lh4ntvt1ZVtvFv+PL1Dc7QLV+w19+/LJA0mnsh7GIFYKFlbA65gA0\r\nc/w+uqDy0+5MxkR9WGPpd79KRA1tKWTis4Ny1lApK5y3zIsVGa3DfBHXcwqkWHbV\r\nwIpyuyyDsFtC1b9LTFONX7iU9cbNk5C5GTM331MdA2kLcD/D5k42GNTBSca7MkPx\r\nFx/ETSn0Ct167el30symf2AxvXjw+mBYPN71rVTMDwe9AgMBAAEwDQYJKoZIhvcN\r\nAQELBQADggEBAC0fJKnbEhXA8M283jA9GxABxLyTBcQyVVNnz2L/bYYNi81Y9iZv\r\n+U0S3qaIfoqNcV9FTKAutbsKvWyolnI7MRRK6feNuFfoP2jKubM1CnawpyT/RF2Q\r\ne/zxnB1EmeI2X5D2xceJDLB7Fy5W0EGrLixRIdFaSUommWFUm9E2hSIaNlziSBdc\r\n1J/mOQeNYO5zg5ouEt1rzQW4Mhh1I2uNQmGe4ip+Jl/2LAv3FZuu4NrSEcoXH3ro\r\nG2dF9Gtu4GiQ5fuaJknaxlgXHovfqeZwZJX9o4M+Ug81AqiY7XjdiaCPdh0Tthcx\r\n2OmWZo7UBREWenjKyFZZ/iKoqH5sdenBtpo=\r\n-----END CERTIFICATE-----"},{"id":"apporder","categories":["customization"],"userDocs":"","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/juliushaertl/apporder/issues","website":"","created":"2016-10-01T09:16:47.111889Z","lastModified":"2016-10-12T19:50:16.038821Z","releases":[{"version":"0.3.3","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/juliushaertl/apporder/releases/download/0.3.3/apporder.tar.gz","created":"2016-10-12T19:14:10.802359Z","licenses":["agpl"],"lastModified":"2016-10-12T19:50:16.104357Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"nhlT9lhrmBxIsqh/e3RLm2NDw/U8ZvvoMyYQTLMM3H19DQmVcPYPYC9QWVTsowUzXblVaOXVGylbpKma9yOlOAqJtF3qyXecLl4+tA/Awb6BBhKPgHFdcLDAL5yy1K7/uei3SgEojMlJoI9vEK5I1C5YTh43yNH+//Im6MAuNYFUTlMXK426zdOe6ogpCI5GlYdXopqYANxcpT+WXWET6DDSM5Ev+MYwMcSAY4r8+tvARRU4ZAenRgjkBT6R5z6cD76emRax14tbV6vkjjwRcO+dQtM0tFPbd+5fsNInbauv50VzIMgjA6RnKTOI17gRsSdGlsV4vZn2aIxEPWauu6T/UohMvAE9HMn13vtbpPBSFwJAktj6yHASYGzupNQLprA0+OdyALoLZPpQAKNEXA42a4EVISBKu0EmduHJlUPeqhnIGkkGgVNWS8AWqzP2nFoPdXBgUWateiMcBTHxgEKDac5YmNc9lsXpzf1OxBaXHBhGYKuXPwIfyH3jTWb1OdwixJEuRe9dl63T9qOTRre8QWns/bMqKLibGfMtFhVB21ARJayBuX70eVvabG/2N7Y5t1zUlFygIKu51tvo3AVCRDdRrFWDvkAjxzIz5FIdALVZ+DReFYu/r4WF/w3V9rInFuEDSwb/OH4r8sQycs07tSlMyA74Y3FpjKTBxso=","translations":{}},{"version":"0.3.2","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/juliushaertl/apporder/releases/download/0.3.2/apporder.tar.gz","created":"2016-10-06T14:00:51.532409Z","licenses":["agpl"],"lastModified":"2016-10-06T14:00:51.598455Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"gRVFOtj9414ZNSdRH/qNB2SwVZUQh+gaFnNLFjjXjJ1MdRMCISzvwb+QU1qYuK/y\nuL8K0pn1+fFQf8A3VsC2pb6yaLQ5U9C3Guf886Flf4qtYw1P8UWRA9yOJ+6Md+PH\n6pTEiDIdm4xbmM0KkBhsE5kL8dvLIq4EwwcAh2Qq8fjytzAk1YiP+KrTaYrVwskM\nDmm0lgP4NVnjRBTX9myW6tr6N3w0tq2jJ/+a/vEDJv+5ozKJx8N5gbJNdrtI4k7I\nyaQNWJ7cngtAHmUREeoBggV5uJayDceu83PPQR6N9/WVyNyZjw1Q8/Q6e/NyiXT2\no8mGv5tHl3DBOVuv8v7gBQgDh6ppp12M81aiCZymn2XIgjw50VQ+K15KHnLHuFFw\nwuHZVcoQ7b6oR4K4TURSfPFUeNgGS4R9v6gjg1RUcSm1Pnryc6pYnh10AUY8dk6q\n1CZ6Upt6SScP2ZEGWsFwVjjQhY/ZJmNfnhaGoFOZ5L9CnzyNCkGXFg0rL36i6djb\naqFy/z+Brnklng5ct6XWoP7uDt5BaHznQj1NHSfHn0GUQ0N968zWm9vQvy+dyXyC\nxR7vKeu2ppZ2ydoeQ9CVwfhOEsGs5OvrpapQdh9KbUHcX7b7ql01J7/P6dFuNuHe\n+7/y4ex3sEVg5YBmDtF8iZ6d7zsHd6peL1s1EsLnenQ=","translations":{}}],"screenshots":[{"url":"https://bitgrid.net/~jus/apporder-nc.gif"}],"translations":{"en":{"name":"AppOrder","summary":"Sort apps in the menu with drag and drop","description":"\nEnable sorting for icons inside the app menu. The order will be saved for each\nuser individually. Administrators can define a custom default order.\nAppOrder works with the default owncloud menu as well as with the direct_menu\napp.\n\n## Set a default order for all new users\n\nGo to the Admin settings > Additional settings and drag the icons under App order.\n\n## Use first app as default app\n\nYou can easily let Nextcloud redirect your user to the first app in their\npersonal order by changing the following parameter in your config/config.php:\n\n'defaultapp' => 'apporder',\n\nUsers will now get redirected to the first app of the default order or to the\nfirst app of the user order.\n "}},"isFeatured":false,"authors":[{"name":"Julius Härtl","mail":"jus@bitgrid.net","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEAzCCAusCAhAEMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwOTE0MTI1MjQ4WhcNMjYxMjIxMTI1MjQ4WjATMREwDwYD\r\nVQQDDAhhcHBvcmRlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKVK\r\nKn5jivCu+eRfe5BECjDOzNaGHlpiegb49Hf4nh0W7DqcoLHip5c1O2BcEYdH6rkw\r\n20WclvjoQpgavG5aFXzXzur6eKTT5TpgY5oZTLoWjbx4e+fKdhyDPTpqNZzs1pxz\r\nsZLDL/ElpbSErE0s+QK/pzP11WNPylAkI9AKSyDMO3Mbllg8I8Bt+bT7LJKYOO/T\r\nLhv9m0anLZ4HrdfimhVIoMiu3RpyRigk8titXZA94+F8Fbf7ZQ9f14Y/v3+rfJFQ\r\nii9cDoox5uUrjplH2LrMr5BodfCfydLu4uVpPWYkdccgvcZ1sugjvNXyCQgdzQDK\r\npOpiwVkkiQFaQJYbGSDblFWPl/cLtA/P/qS7s8tWyTQuc1rYlEpCHG/fG8ZFkSVK\r\n9eCMGxK908VB4IU2DHZHOHi7JvtOz8X/Ak6pIIFdARoW3rfKlrz6DD4T9jEgYq0n\r\nRe7YwCKEIU3liZJ+qG6LCa+rMlp/7sCzAmqBhaaaJyX4nnZCa2Q2cNZpItEAdwVc\r\nqxLYL1FiNFMSeeYhzJJoq5iMC3vp2LScUJJNoXZj9zv+uqTNGHr+bimchR2rHUBo\r\nPzDLFJmat03KdWcMYxcK5mxJNGHpgyqM7gySlbppY/cgAospE8/ygU2FlFWIC9N0\r\neDaY+T8QA1msnzsfMhYuOI8CRYigan1agGOMDgGxAgMBAAEwDQYJKoZIhvcNAQEL\r\nBQADggEBAGsECd+meXHg1rr8Wb6qrkDz/uxkY1J+pa5WxnkVcB6QrF3+HDtLMvYm\r\nTTS02ffLLyATNTOALZFSy4fh4At4SrNzl8dUaapgqk1T8f+y1FhfpZrEBsarrq+2\r\nCSKtBro2jcnxzI3BvHdQcx4RAGo8sUzaqKBmsy+JmAqpCSk8f1zHR94x4Akp7n44\r\n8Ha7u1GcHMPzSeScRMGJX/x06B45cLVGHH5GF2Bu/8JaCSEAsgETCMkc/XFMYrRd\r\nTu+WGOL2Ee5U4k4XFdzeSLODWby08iU+Gx3bXTR6WIvXCYeIVsCPKK/luvfGkiSR\r\nCpW1GUIA1cyulT4uyHf9g6BMdYVOsFQ=\r\n-----END CERTIFICATE-----"},{"id":"twofactor_totp","categories":["tools"],"userDocs":"","adminDocs":"","developerDocs":"","issueTracker":"","website":"","created":"2016-10-08T14:13:54.356716Z","lastModified":"2016-10-12T14:38:56.186269Z","releases":[{"version":"0.4.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0 <7.1.0","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/ChristophWurst/twofactor_totp/releases/download/0.4.1/twofactor_totp.tar.gz","created":"2016-10-12T14:38:56.174612Z","licenses":["agpl"],"lastModified":"2016-10-12T14:38:56.248223Z","isNightly":false,"rawPhpVersionSpec":">=5.4 <=7.0","rawPlatformVersionSpec":">=10 <=11","signature":"bnwWxmHEn8xkoWbtwhC1kIrJ0dQfAI3PUtU62k+Tru/BHt1G2aVxqO8bCdghojZ7\nzdFMlIJw4kekYFsVfLk8jzjUTZKVbNVKCdkHrVTQ0bUUryMAMLqGQ3PSRI5NX6D5\nFpkvwO1coYwU0XVWF8KAS0meX0ztSkT3Mv96LLrxr8F8SrB/MGmKIE4WTjt1fAIa\nZLAVEUo/3sNFTGLYBtL3wjctrkZvJltP8abeRfls9FkRHu+rN7R3uLFzk42uZn3X\nWpt5BBmlYm5ORbnJ2ApsxEkMNK+rOy8GIePaz5277ozTNrOnO04id1FXnS9mIsKD\n20nRzjekZH+nneQYoCTfnEFg2QXpW+a+zINbqCD5hivEU8utdpDAHFpNjIJdjXcS\n8MiCA/yvtwRnfqJ5Fy9BxJ6Gt05/GPUqT8DS7P1I1N+qxhsvFEdxhrm2yIOhif8o\nh7ro5ls+d3OQ8i3i4vdZm821Ytxdu/DQBHiVoOBarvFWwWAv2zd2VAvpTmk6J5yv\n3y+csRqpEJYd9fcVMPsTu7WBRRrpBsAqdAHJcZEwak2kz1kdOgSf8FIzP1z6Q71d\nMl2RKcPeutMHHSLiGIN/h7fM5aSs49wGgGZmfz28fHVd7/U0HFSMYmkT/GMq5tMP\nIyc+QZAN4qbX8G0k/QSTkK/L4lOT2hQiQqiSqmWItMk=","translations":{}}],"screenshots":[],"translations":{"en":{"name":"Two Factor TOTP Provider","summary":"A Two-Factor-Auth Provider for TOTP (e.g. Google Authenticator)","description":"A Two-Factor-Auth Provider for TOTP (e.g. Google Authenticator)"}},"isFeatured":true,"authors":[{"name":"Christoph Wurst","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIECTCCAvECAhASMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYxMDEyMDkzNDMxWhcNMjcwMTE4MDkzNDMxWjAZMRcwFQYD\r\nVQQDDA50d29mYWN0b3JfdG90cDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC\r\nggIBALC1K94104L/nOtmTygx7QNjUcnHs3yrn71mw4pMxTlonXOnMTpwxsfL1Hhu\r\n/5GMSgupTbQPlevSl6J86UMs455/sPShd6ifmAuhb8VFaAsjpizjs0RMaUg1sjmF\r\nuV18PD9FXLourx51V/c4MG5kpavlV+bLUrVMAjbsJY2+k30tCC/XkP5u8jUWmM/T\r\n5REChn7/obPgaeddhuJoILYhKEW3VNrR8Fm9SYiviB3FLhM7URDZ97IBnXYqbvbT\r\nZnvq+E74Zc7HgYwQwrjU/AqQAInhNpAR4ZM6CkWWWWaL96O1q3lCfKJNaxqC0Kg/\r\nkGn/pxYkl9062jtMUz60s9OPDyuisfyl68UyM68Ozyz4SMRLmDVbewOqQAwmAbtz\r\n8p9AQrX3Pr9tXhARR4pDSsQz1z+8ExEd6EKbhMyiTtHtZQ1Vm9qfoR52snpznb5N\r\ne4TcT2qHAkOWV9+a9ESXmQz2bNjgThxEl5edTVY9m4t248lK5aBTGq5ZKGULNHSQ\r\nGGpr/ftMFpII45tSvadexUvzcR/BHt3QwBAlPmA4rWtjmOMuJGDGk+mKw4pUgtT8\r\nKvUMPQpnrbXSjKctxb3V5Ppg0UGntlSG71aVdxY1raLvKSmYeoMxUTnNeS6UYAF6\r\nI3FiuPnrjVFsZa2gwZfG8NmUPVPdv1O/IvLbToXvyieo8MbZAgMBAAEwDQYJKoZI\r\nhvcNAQELBQADggEBAEb6ajdng0bnNRuqL/GbmDC2hyy3exqPoZB/P5u0nZZzDZ18\r\nLFgiWr8DOYvS+9i6kdwWscMwNJsLEUQ2rdrAi+fGr6dlazn3sCCXrskLURKn5qCU\r\nfIFZbr2bGjSg93JGnvNorfsdJkwpFW2Z9gOwMwa9tAzSkR9CsSdOeYrmdtBdodAR\r\ndIu2MkhxAZk9FZfnFkjTaAXcBHafJce7H/IEjHDEoIkFp5KnAQLHsJb4n8JeXmi9\r\nVMgQ6yUWNuzOQMZpMIV7RMOUZHvxiX/ZWUFzXNYX0GYub6p4O2uh3LJE+xXyDf77\r\nRBO7PLY3m4TXCeKesxZlkoGke+lnq7B8tkADdPI=\r\n-----END CERTIFICATE-----"},{"id":"contacts","categories":["office","organization","social"],"userDocs":"https://docs.nextcloud.com/server/11/user_manual/pim/contacts.html","adminDocs":"https://docs.nextcloud.com/server/11/admin_manual/configuration_server/occ_command.html?highlight=occ%20commands#dav-label","developerDocs":"https://github.com/nextcloud/contacts#building-the-app","issueTracker":"https://github.com/nextcloud/contacts/issues","website":"https://github.com/nextcloud/contacts#readme","created":"2016-10-30T14:00:58.922766Z","lastModified":"2016-11-22T22:08:01.904319Z","releases":[{"version":"1.5.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/contacts/releases/download/v1.5.0/contacts.tar.gz","created":"2016-11-22T22:08:01.861942Z","licenses":["agpl"],"lastModified":"2016-11-22T22:08:02.306939Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"ZqqhqtbHcNB+rzGCQ7FDIjjvHjit+dhAE1UhFgiXApkx3tmPP4nJOBAGNjHe+2Ao\nVcTIX2SrWEfieRrA4Gp+0k7pUPWag1Z0T1OVOwO4cmS1AVFyGIOE1bRvDhMfsWTU\n4CI4oXaKBFAY6mtnf7VJ7EeIdNqhImkohyWDQ88NiPRLM1XNkJJk6AvZBcT0fvCv\no145X4dLpbixSXsN99QFNJ/oXvK+9tBGwTd5i/WnNFY90vcNRLia8aRo7SA0YJRx\nLnxnj2HMqwTTDQEKE+1elYKWsqQ2DeqwScP97UIKe5bZXnrwOi9kH9PDmR4abtzd\nlHL8E1Wgw25ePDeHG7APrx0tVOJy1bP+g8vcarpGynWZoizDkBvYZD+xtxizpBXC\nJsDOSzczApptY6dnOtv0Vat8oh/Z/F99gBUahEu4WZ16ZgR1nj40PDK1Snl18Cgk\nMe1EZcde8SLEpTbCWYIfIw/O9Fkp5cWD/dAqoiO6g+gNxSZ/gGp57qoGfFxn7d/x\nH3aH8GljatAFjrwItw1JzR0THt0ukkOK+bw/pfCslk10sjHMitmz/GXa4qMS91DZ\nBKLUd0dSfQUQzkfwcojImbzJRvca4/DYe3mfG7+RCH0tDL6t72dKL9joB++u5R1u\nVZPgkToexlXcKWpiDB8H2/SEShKr4udAOjR5de9CYWM=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/nextcloud/screenshots/master/apps/Contacts/contacts.png"}],"translations":{"en":{"name":"Contacts","summary":"The new and improved app for your Contacts.","description":"The new and improved app for your Contacts."}},"isFeatured":true,"authors":[{"name":"Alexander Weidinger","mail":"","homepage":""},{"name":"Jan-Christoph Borchardt","mail":"","homepage":""},{"name":"Hendrik Leppelsack","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEAzCCAusCAhATMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYxMDEyMjAzNzIyWhcNMjcwMTE4MjAzNzIyWjATMREwDwYD\r\nVQQDDAhjb250YWN0czCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANzx\r\n/zJF+5/s4lOJLWIlfKQgTy+UpvIpiUXCgrsHsDZTx+hjQAhIWukH88a+7NVAL7Ys\r\nkQNC0Tlm755FJi/T6EdR7edOwIRdo2ZwakOWLZXd209+6cCd2UloHL0bgnbWepTl\r\nR/4YgbLg/G+FVKCfkEiYc3PuDZ3EVrcwQFcg7h74X9ne6CHH0Z1WQLydxJuVEb2n\r\nX9I+nIRpPHcVostkSiUmzHR7C5TjTIo2PzzgnCU6GC0iBa6z6dDYfz24QEp/b8UA\r\nZaLhjkyKghVGMnoF/s9KPgH4NM8pvvtadQN8pxlOPju4wbmKPUrsXo4ujurDXbbc\r\nYkzNt8ojobGwdTXoyDogAsGZLQd2FQksWpRvY+I3zVPokBfPMdUPLllG5VcV0VA5\r\nDRK+h2ms+XmspdBvGonjF+XdbFm9hEmDoFmoi9aU6C6AdofjmG/e9+pw/20dXUWk\r\nmMorWwXQ5yLmIn5LnpRXrOuK7CS28VRhBYuVNMlsyKhzU0rophbsD9OFXxYLjr6s\r\n7UPNwZ5h+kjXZDBKD89QctBSViT8RhLe8nulRIm0iJn1sb9hca/CF63KmsFzENfK\r\nQeM6MO0H34PB84iNyz5AX1OIy+1wHD4Wrzt9O/i2LkWK6tBhL69aZiBqdLXWKffj\r\nARDCxxIfews51EZFyHzwsw65I97y46aBKxY382q7AgMBAAEwDQYJKoZIhvcNAQEL\r\nBQADggEBACLypX0spxAVAwQIS9dlC9bh1X/XdW2nAvSju2taUTBzbp074SnW6niI\r\nbnY4ihYs4yOuGvzXxnp/OlvWH7qhOIchJUq/XPcEFMa7P03XjVpcNnD3k0zQWlZb\r\ntGonX9EUOeLZKdqI4fkrCkMLScfjgJzoHGYQrm8vlIg0IVuRLCKd5+x4bS7KagbG\r\niuPit2pjkw3nWz0JRHneRXz/BNoAWBnJiV7JMF2xwBAHN4ghTM8NSJzrGTurmpMI\r\nGld7yCP47xNPaAZEC66odcClvNtJ2Clgp8739jD6uJJCqcKDejeef0VU1PG7AXId\r\n52bVrGMxJwOuL1393vKxGH0PHDzcB1M=\r\n-----END CERTIFICATE-----"},{"id":"mail","categories":["tools"],"userDocs":"","adminDocs":"https://github.com/nextcloud/mail#readme","developerDocs":"","issueTracker":"","website":"","created":"2016-10-19T19:41:41.710285Z","lastModified":"2016-10-19T19:57:33.689238Z","releases":[{"version":"0.6.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0 <7.1.0","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/mail/releases/download/v0.6.0/mail.tar.gz","created":"2016-10-19T19:57:33.676730Z","licenses":["agpl"],"lastModified":"2016-10-19T19:57:33.834580Z","isNightly":false,"rawPhpVersionSpec":">=5.4 <=7.0","rawPlatformVersionSpec":">=10 <=11","signature":"VbMsvDpt+gSPeFM8LrZXEK10rk8kkLlgCcblgqNdCSeGZeVpwDAYv3CccVSLa0+l\nlTSqQ0VIoH+OIU6vIQNBKHmSCzTplk7OrY0+L5FajXx8LnBaOh892GfGSlEt1neN\nKyM0i0uOjO/xpCP/NoUlgkz6hnmYY5XEdN6DTsJtJ/XZhDQ45IYuIkMkHE/eFehS\n0JnOagIz+PSipeBY2Ry+tV8YbRa7bC1JAvZzlod0dyI015AHZESeitRUY+MwMWkt\nN/me7g7/Kev0wggIQQZm9aYcw63GMk/1VHUPB7Y0ESW9tx2nR5+KwTDn/Jy4DGf1\nrg8h0t5I+aPhHOBLrpczH0qaZWY2lsVZWq8KWjJI9aR9P0v2f2aXixXzD/Cuz1cK\nhvhKWkOSla4D+/FxeyHGjQvdXMG8gXm0ZmTimKChCoVuCbncDd8pzkdyNoGXcvuk\nsP8OrkQFooL4E7S4BWfdSiN/a8jUITJQkuXp/OVrVGeCupLWJh7qegUw6DvoqyGy\nD4c6b+qYn68kx3CLaPPiz+tFAZQZQdj7+Kx/lohso8yTnVSiGYrMj4IvvCbpsQjg\nWF3WSqF/K/tTnPYTWb9NUPSihTbVNv6AXOfTsPEp/ba2YSS5DjvjVjkr5vhR9eg1\nikQ3Cw6lW3vaA4LVCC+hFkMRnI4N0bo5qQavP3PnZPc=","translations":{"en":{"changelog":"### Added\n- Alias support\n [#1523](https://github.com/owncloud/mail/pull/1523) @tahaalibra\n- New incoming messages are prefetched\n [#1631](https://github.com/owncloud/mail/pull/1631) @ChristophWurst\n- Custom app folder support\n [#1627](https://github.com/owncloud/mail/pull/1627) @juliushaertl\n- Improved search\n [#1609](https://github.com/owncloud/mail/pull/1609) @ChristophWurst\n- Scroll to refresh\n [#1595](https://github.com/owncloud/mail/pull/1593) @ChristophWurst\n- Shortcuts to star and mark messages as unread\n [#1590](https://github.com/owncloud/mail/pull/1590) @ChristophWurst\n- Shortcuts to select previous/next messsage\n [#1557](https://github.com/owncloud/mail/pull/1557) @ChristophWurst\n\n## Changed\n- Minimum server is Nextcloud 10/ownCloud 9.1\n [#84](https://github.com/nextcloud/mail/pull/84) @ChristophWurst\n- Use session storage instead of local storage for client-side cache\n [#1612](https://github.com/owncloud/mail/pull/1612) @ChristophWurst\n- When deleting the current message, the next one is selected immediatelly\n [#1585](https://github.com/owncloud/mail/pull/1585) @ChristophWurst\n\n## Fixed\n- Client error while composing a new message\n [#1609](https://github.com/owncloud/mail/pull/1609) @ChristophWurst\n- Delay app start until page has finished loading\n [#1634](https://github.com/owncloud/mail/pull/1634) @ChristophWurst\n- Auto-redirection of HTML mail links\n [#1603](https://github.com/owncloud/mail/pull/1603) @ChristophWurst\n- Update folder counters when reading/deleting messages\n [#1585](https://github.com/owncloud/mail/pull/1585)"}}}],"screenshots":[],"translations":{"en":{"name":"Mail","summary":"Easy to use email client which connects to your mail server via IMAP and SMTP.","description":"Easy to use email client which connects to your mail server via IMAP and SMTP."}},"isFeatured":false,"authors":[{"name":"Christoph Wurst, Thomas Müller, Jan-Christoph Borchardt, Steffen Lindner & many more …","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIID/zCCAucCAhAVMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYxMDE5MTkzMDM0WhcNMjcwMTI1MTkzMDM0WjAPMQ0wCwYD\nVQQDDARtYWlsMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp++RuliQ\nlBeeiPtP0ecBn00OaU1UCpft/NVI5pnSiT9nU4l2kc5IvKjA8UxDB3gWfYTOeBFh\ntUHQ2P6UKCmHZT9sApHhqLu2n0V+YhlFIViuaxndSID/M414cl56xOYQusV3Pcae\no2dOSeRRzLab3tEaVHlkBSFkGmAwPZItsmTklvV3h1sUysDicYgfXPCkf7K+JgWA\nBP7vsWC8B7MDRhcB3enYv5tTcpsyvtGX7bb1oTIWVypcmKsGYfTX12VNBxKzNBIG\n8pwdb8Xo0o14TytWsWN7mSHf1XbwfwYMjDWOlMqiRc+mcoKMBH41TfM/CXslSivI\nsyvxasEaFdlj8lmKPENdzw1OfYRs43usIf4szwyt4rb8ocXfDipnY3P2hccN6YcZ\nl8y8Vsr69ASluDj2A2Pl5vH6xp6tNybZRnN5G6sghhaYaLNDU/TdMyYzz4AY33Ra\nHSaMypfcXjd76Aj8jZvcwk1BH+ZsvFqNK7ZKCb7WVcMH8KRcU1sxZ4rp9vviM2fL\nL7EVtznm3bSI9jjHXbiwq7RvNRRy+F6YRpAdWGwTU8uUkDabPFi41FikYyzNWauK\nJhlDJXl514XjKyMVBjAZYVr5gZZkO1J7C4XzLFbC5UzYNSzW5Iwx/1j5OeYJRxh6\n5rhiUwR+COT1wdVsl6khMC8MfBR4unSd338CAwEAATANBgkqhkiG9w0BAQsFAAOC\nAQEATBvpqz75PUOFPy7Tsj9bJPaKOlvBSklHH7s43fDDmQbJwswXarZi3gNdKf8D\nyO/ACZvO8ANWAWL/WahkOyQtKOYzffaABGcEIP7636jzBdKtgwSGzW3fMwDghG10\nqBr2dE6ruOEdSpuZxgMgh2EulgknZUXaHAMI2HjjtAMOBScLQVjOgUqiOHmICrXy\nZETmzhx0BXDt5enJYs8R2KMYJNIme1easQRYmWKliXogNY09W7ifT9FHtVW1HX+K\nxRS4JXbapjolkxyGSpP+iYSgItVnYzl6o9KZResR4yDsBv7G/8fpV4GQU9IS3zLD\nPiZOosVHWJdpUKCw9V4P1prGTQ==\n-----END CERTIFICATE-----"},{"id":"audioplayer","categories":["multimedia"],"userDocs":"https://github.com/rello/audioplayer/wiki#user-documentation","adminDocs":"https://github.com/rello/audioplayer/wiki#admin-documentation","developerDocs":"","issueTracker":"https://github.com/rello/audioplayer/issues","website":"https://github.com/rello/audioplayer","created":"2016-09-16T05:44:24.857567Z","lastModified":"2016-11-17T22:34:34.637028Z","releases":[{"version":"1.3.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/Rello/audioplayer/releases/download/1.3.1/audioplayer-1.3.1.tar.gz","created":"2016-11-17T22:34:34.215350Z","licenses":["agpl"],"lastModified":"2016-11-17T22:34:34.867778Z","isNightly":false,"rawPhpVersionSpec":">=5.4","rawPlatformVersionSpec":">=9 <=11","signature":"p6Zz0IEFrxvw6y/3jHgGWWCxR6qpMzvU2HKfxcIVsK6sJnoRUhWLeAXwZ432fH2a S2llj+IGS9OvW+5VQElrXgPtEjDK1BT00DRJnp5RFCRlUv0LNoedJMzx6B6AHqPP JBufk3cG1O/CO0M0L1ITGSmSOzfKvWTRo3lxVGF792NyBaP/SyZCkH1N1TzBQzUi Ywl3+HiglPcXbHjtJm/arnKorbJWVKoaN93xFuaBapd2ozQSpi0fE0uGRsici+U7 HNa1M5WFE1rzUJoufE0E9246At07rFY1e+TdNEq8IlLgCXg5vGCKkEyuWpWno6aX LfRaIiT9x39UTAwNvuDKS0c+n4uWDYPsGfKhDx9N7CXpUrthfXVEWRzZEXG7as10 6ANvrRPJemSZH8FUSrdJhD7k12qa9R825y7mIG68Li8P71V92EOxFfo9tNXqXwBt VuDGxBqByFVPqSCj5I8hrzJzQl2Xt40g8+8ZcSF96RMg/pM+bwRMTv+mz0V+vQQ4 DWjqnWVPalaJ1PPD5/QFFErtXuNRbyxKZ6BMWxfJlLM9Kz66P75K+8fYaSwz+2KG NxY7I3svzS2K9LGH3fBLUy1t3Hl+c3zGFq/ll1MJrf9NswV4yxt2WmISfM/KDww8 yELkGs2cRzbw2tCvnmYlJJhIqRLkyFAbDzWRH/XdVx4=","translations":{"en":{"changelog":"2016-11-17\n- fix: one-click-play for wav not working\n- fix: wrong sql statement for PostgreSQL [#90](https://github.com/rello/audioplayer/issues/90)"}}},{"version":"1.3.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/Rello/audioplayer/releases/download/1.3.0/audioplayer-1.3.0.tar.gz","created":"2016-11-15T18:11:19.539636Z","licenses":["agpl"],"lastModified":"2016-11-15T18:11:19.592881Z","isNightly":false,"rawPhpVersionSpec":">=5.4","rawPlatformVersionSpec":">=9 <=11","signature":"lbp7wd3JhMHW5mC8kVnQFvcwzf3aTIhYhq3ak/C/vfDXJDIPFuQ1odVRWtaHXEKQ XmKYIoTobV1TAU5q9G0O0Kds73T/XtHG4ATLxMZE8RsUWNSj5v3H4YDub6A0uoX6 rzyLEYV6SGEtdPFMwLcUjDExKpzAzKpgxcd9uyz2NhcHJEO8FJmirn34bm69//TO vjjiMW4zpL+dho+7LQbOX+L1SmwmdBqwypE9zzeuIuhUWDEQtImHAvjIO6Temajm lX0H5JaowJa8kvP6Jkh3KAvsHQ4sJklvWTPGcv0gboN+o6CmjWNOb+3LeSH0nhe6 BmiPloUDJcPQwq2gQejH2pY+qJEdRcULSKS09/dRbE3gOSlG36FThN0INpv6uNP4 qVIiYs3/SEHMmlS5CHvJDt2S2XN9LT9IX7QPeuS/0CMcuopaG/+cdC4KscVCq4D4 bllgew9asiBqix8iV8C4oerYOiC5vWcgBrZhGShoJT1Qq+NKz+H10dFgjFCAZuPj nVagJkbXmf2NdcvpSC7qsufhyEZyCSp+I7QEYsbo1PW3aUU35Syt47lgeVFX0hVQ jC1wMIhEW5Rm2nCkRSZkRupKDQ+N6uWuB0lykeMV2ShcDvvUZrhN3c49sooWgigB yIqKryzM4fLErjjNHdYgwCq6bbgPDLK3ic9b3B4rF3E=","translations":{"en":{"changelog":"2016-11-15\n- fix: handling of temporary scanner files [#68](https://github.com/rello/audioplayer/issues/68)\n- fix: simpler analysis of incorrect files in scanner [#57](https://github.com/rello/audioplayer/issues/57)\n- fix: album sorted correctly by artist and album [#80](https://github.com/rello/audioplayer/issues/80)\n- fix: neutral cover for unknown album [#16](https://github.com/rello/audioplayer/issues/16)\n- fix: error message from ID3 editor shown in front-end [#77](https://github.com/rello/audioplayer/issues/77)\n- enhancement: occ support for library scan and reset [#72](https://github.com/rello/audioplayer/issues/72)\n- enhancement: select a dedicated folder for scanning in personal settings [#79](https://github.com/rello/audioplayer/issues/79)\n- enhancement: exclude folders from scanning via .noaudio file [#79](https://github.com/rello/audioplayer/issues/79)\n- enhancement: significantly reduce database reads during scanning [#79](https://github.com/rello/audioplayer/issues/79)\n- enhancement: cleanup of classes; move from \\OC\\Files\\View to \\OCP\\Files\\IRootFolder [#72](https://github.com/rello/audioplayer/issues/72)"}}},{"version":"1.2.2","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/Rello/audioplayer/releases/download/1.2.2/audioplayer-1.2.2.tar.gz","created":"2016-10-06T21:21:05.414691Z","licenses":["agpl"],"lastModified":"2016-10-06T21:21:05.483224Z","isNightly":false,"rawPhpVersionSpec":">=5.4","rawPlatformVersionSpec":">=9 <=11","signature":"toeS45z50Lm0djgrQokOTN7gA8a113IZtiKKiLsGUKWrCV/6AKJBmEFcSun6rhLH\nbz/RtIdFKwQql6O3E0m1Zch2y1A8aLWHzFTO+5orLCVi7y15SshrJYbb9aI5Pj3i\nSR7+kMHGS8uNx2uIn3B4mO6UYF8AzCfp+ule18DOjnpu86KWvEOGtFXsQkLtgepp\nbJhOGWW/uOVIos/T1xPP4GCmtVmgn7U3b9q0pMYRH7ONXEiNsfQxDrR66EZH3Jfo\nlVyM9UvQmMKoDSdBUlvLlhCEGJGqFOD7hFntLYloI4oXv9uGqcagaJVh3TkEysY2\nMbBZpVhch5zRJ/HGlZOvmEzZ8Inxkk3iap+JmJ5/gZTefwfKUyYHALsILlh820U2\nNA/1B5A015XH5a5uflGE/tnlPtrOeecIN4mg+1njo2RG89HJWJNHM2ZDO4SkXjSR\njgygmAS5aR5+KKifiA/pwjhLozDWPU4lNzsj3Foz3bx3Okopy7eq83LORqieT4Jp\nFvP64q/45LOSRBWIWLitYzRzZp7HYywMsnz12WpxtqxIjO7+7y/ByeWWOBNU1IJC\nK2D+035ZGWZr0CxDJte33WOISwjVoSwrcov++O3BQW8lM5IkcDNcJFyzNPKAXcQU\nPUXmQpYurHoIw6odAYcbrG6iOiSesuNOf2kQWbjV3/c=","translations":{"en":{"changelog":"2016-09-18\n- fix: icon issues with alternative apps folder [#65](https://github.com/rello/audioplayer/issues/65)"}}}],"screenshots":[{"url":"https://github.com/rello/screenshots/raw/master/audioplayer_main.png"},{"url":"https://github.com/rello/screenshots/raw/master/audioplayer_lists.png"},{"url":"https://github.com/rello/screenshots/raw/master/audioplayer_share.png"}],"translations":{"en":{"name":"Audio Player","summary":"Audio Player for ownCloud and Nextcloud","description":"Audio Player for MP3, MP4, Ogg, and Wave with a lot of flexibility for all your needs."}},"isFeatured":false,"authors":[{"name":"Marcel Scherello","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEBjCCAu4CAhAIMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwOTE1MjExMjA4WhcNMjYxMjIyMjExMjA4WjAWMRQwEgYD\r\nVQQDDAthdWRpb3BsYXllcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\r\nALyC+iLscLs62NeNmUXEBmg+xMuUtDmZKr+xzJWtl6SSNRz+8K1JygvUIXFJ3RIL\r\nCYA3xyq8/wyZH1gNrLKyz5eTeYawG+eT3ges/FT6MWGUbZoRrBrikVcLC94QzxTH\r\nxOl8Dn+SCV/2bhcvPTQdhK+dqtvGilOtjHa40iMrk9gSdlKVys5CK/xdlEp8uiMa\r\nkz1WENn8MVCCJV58bAUbaCupDWXR9CCoSsw8XinNsCenZ2B2XlnmbM44280w0ojs\r\n72rfQRgj3yDG+ZUUyUOuxIuodu8liXYciLf0ph6t/f/qoSmctbBdsR5Fl1Upj1Ac\r\nqeHb5Yf/B3Vi6Mn3XfDx0H2EHk1v9Dhzxay+v9BHUzyIX2iH/q+7TE0/Jzo5AwBW\r\nvFKWXvG7wXaALcHYZf5v/M93IE0iCHsv2EsZKQPBnzXVGmp4DwFSP4po1B7hcog1\r\ngAMaellAzzvUAizgCovN6Qct3qDEANYniPlvtnlcaQGonajW4N019kFQRHLIzPFR\r\njab5iUMMwSnT8FhZO2ZOWuWhJven+gXjxC8mfMVgBfZnAVgydNfx9rN+KzTc88ke\r\nobUdZ0OOeBzA7pIxGEFg9V6KTEEWZ+qH048vxXz4HI9B1I+2wQLBrZl8CvweEZ5U\r\n5ID8XrrE/UaNZ1CvLKtCgB24gj/m1Elkh7wA3gEcEo2JAgMBAAEwDQYJKoZIhvcN\r\nAQELBQADggEBACtgUp+FCmjWIkQUuWSdzKWdO+IH4v9wBIrF9mo0OLIakFyDYyM5\r\nLlkYZXbplGXd4cfn3ruIqJNzlIb4xa5CU0bM4TMbD4oOSlLMKM/EamKPHI3bzr++\r\nzi7mQDFxmAE6FWSMBgKKUb4tqLc5oBap8e12tPEZl/UR6d9iUB2ltvrm3T3vrjjl\r\n2Worm0eYBNqnagXmX5+wS11AQqeJemGqRy5e1yXRlTgB0IJhH0dCsFNwifEigutp\r\nFNvGFVBn4r5qCiChEoq+rCXHRjPi/eCfbW21XeLFDiLxapcZyc85JIcA7znUYoFe\r\nP7Y/ekMscwWhLbF91OaQlcWpRtEMyde/DaI=\r\n-----END CERTIFICATE-----"},{"id":"calendar","categories":["organization"],"userDocs":"https://docs.nextcloud.com/server/10/user_manual/pim/calendar.html","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/nextcloud/calendar/issues","website":"https://github.com/nextcloud/calendar/","created":"2016-10-01T12:40:39.060903Z","lastModified":"2016-11-22T20:31:13.029921Z","releases":[{"version":"1.4.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/calendar/releases/download/v1.4.1/calendar.tar.gz","created":"2016-11-22T20:31:13.020268Z","licenses":["agpl"],"lastModified":"2016-11-22T20:31:13.087340Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"nThwe9CJBCan9nuDLdhfBiQyPhmum6Aa0UcYsIDdhGMw+C2acf81KhEmBJuTTWxo\nWGby6WcrcJJmeuCW+ePU91ju7Pd76RirprhVXIEceIDzSCxin+K0oZCZ1IGVIJjP\nIkVehTsLuCeTBbjvz1b3k5QFyhUhvd32Xt7k5d7VARyI4OqnqYYNBtH9vvgeRrFw\nAxsQr4o4axof6i3iykLg6WfWarYArY4dIuu5DkPuGPWf2bbgjwWEra4sQejhOs7G\nsk1xcsfYv2NpArIbpw/wnATdjiax+Gjz1URMD3NgL5ky0ecuZmNvN25QErg3nlVr\nhh1FBfA5pvCJbkJ6nr5bU4bKaffwDX1nr5h77FS5zzn0Pyd7ZIExmVmNtaeJfnfV\n5vnclapzXMNU+R6t/ATJQd1srvSJhyljQapzsqnrjNMEUojOEvqqygJp0KwNVPqs\n3g9XGSoOnO+WULWBeISW7UVOg8BOF8pwvHIU2++bSzOdpypW0Eq6p2DPWO6qL/H1\neFLKrUg3EvnTjvknbBxMB55h9jNJr0SAlkrmyEVm6+CE3BwRWpKB+cJMBuGiwPwv\nr/ASRiJrkDThbNWAUtX70rUmCqDV6/MujLXViqOc/Q2OHvcXd1oGDccJSQT92/1z\n7nonnedyYQIDqUt7u68WL8JRxp7pFsEqKLVuWSgxW3c=","translations":{}},{"version":"1.4.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/calendar/releases/download/v1.4.0/calendar.tar.gz","created":"2016-10-06T19:58:12.724588Z","licenses":["agpl"],"lastModified":"2016-10-06T19:58:12.790604Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"b//hJbICFMLR0Va1BGMzpLpaPREOo9QhjgfrHvDOfXVpddhvCM8ocz74X1s5hKyy\nGg67EE0pOp0dBf6RyJjduI+Dz1wQX55My7J9G1vXGCN30C/8zAcKSJoA218IWcub\nICavLkz2PkiitIOSZyBCAfEiSAeHPop/JGkq3KxQDC7QwFC78BnE9/adD9tO55c/\nDGLhvP/uTJIeH8RUifikTqVMmPH+aP3uPbZzl+AxhUezRiNCpEtZPA5QGqtQdJU4\nFc6x3d9y4IWbJV4TEIAP8jdfqtLVUQ6szFVnN8Oi1wtN9e8LIylBSYbmIZRj0+qh\nZcgntzEq6U843ZwXcAnL5jNYV0m+KNI+EkXFeWHkjvbwfCdvGPBvgFVbhc0YPzXU\nqHOe4Lvcx9X20ALG/MacV9zX69GzNnWgbBp9RnIHuaSRPFEKrNXUeXl2THuKsTyQ\nF9QtTwS5U5DcMyTO2RAN45NrRxIh8IL4stoIg5rmF7/ZaOm/Jza2gnUquOTarDE/\ntiWnNW5kWUAWyYYHvQgQix/of9qXvc2hhZaw0y623WDNrEwA+rngnjDMLA/vNv3B\nhgwQ6NbCOuHWsRK3S8DcJFpB9Kj/i7CDvDLEuJYnjSTvQ/q1XqawbJPDoRlydX43\n3/L0LvHvKVakYybv2OE5gy6bQ2Dw8e7D27DtZ6XTaBY=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/1.png"},{"url":"https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/2.png"},{"url":"https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/3.png"},{"url":"https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/4.png"}],"translations":{"en":{"name":"Calendar","summary":"Calendar GUI for Nextcloud's CalDAV server","description":"The Nextcloud calendar app is a user interface for Nextcloud's CalDAV server.\n\nIt integrates with other apps, allows you to manage calendars and events, display external calendars and invite attendees to your events"}},"isFeatured":true,"authors":[{"name":"Georg Ehrke","mail":"","homepage":"https://georg.coffee"},{"name":"Raghu Nayyar","mail":"","homepage":"http://raghunayyar.com"},{"name":"Thomas Citharel","mail":"","homepage":"https://tcit.fr"}],"ratingRecent":0.944444444444444,"ratingOverall":0.944444444444444,"ratingNumRecent":9,"ratingNumOverall":9,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEAzCCAusCAhARMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYxMDAzMTMyNjQwWhcNMjcwMTA5MTMyNjQwWjATMREwDwYD\r\nVQQDEwhjYWxlbmRhcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMm6\r\nFTeqgzCXprkU83VM4/DrZWn3kqtfaR/edkC4gYT3ug7RHa/Uv1C/S++vr7pvgpnk\r\nYzQoavl/0Qlh5sKEYX+0ud/LQDoiidwBRDckFUQ1bRfVLxAD9UAVvDRHxDqJMOx2\r\ngZArbeQ3ztdSHZp4ThzBFWq2FILsJD86weG7LwHjzhW6SWgLb/YTLbuuW6tRCDVV\r\nbtB0I/a0vCwj2u91Chw3u6pWWjPakc9DQrIDH4HCIBKQ4zVrYDxAmJDRFGDvVVWx\r\nuIAeux8sd8drqSMqAhX+XMcZPRD71NQTWbCupSwWO8kgjmZnBpIiBNpzvMQzJf3A\r\nQloZtjZ2RDXAQG88eTeT8pp8yEOCEoDLpGdraKxJrh/z2Dsk30JP3lOiNYJ9vBaB\r\nC8NJbJ3oAlG7THwUaFF9fsdAKoTwzs5Xms04TI7W/v4Z/GClOzMymnR1T4sR72Oy\r\n3WaMNHv/1QGffvQn2/TtZt23Ou3P083xWx2vn5FgTcac8+x85vRgWsVCA4hq9v6m\r\nAlktB0+UWDEXpDTKD9BdFNWM8Ig9jQf7EJrvTLNnS7FIJZMB4GK8lpvPxyvACWnh\r\nR2hQOe987Zvl3B1JZNO5RvtSeYld9Y9UfMgW1aPRweDNjSuZYAKlugx1ZoyI5HyA\r\nQjfzAwicIMwZsCJDV/P5ZO8FE+23rdWaoJczpBqDAgMBAAEwDQYJKoZIhvcNAQEL\r\nBQADggEBAHQXwvj8q5khWR/ilg3JGYpmMNBYHE9OeDaOcNArkKaGMd478SDPOXeu\r\nyW7hCvNEpiTk5g0h3g3yleZFws0xH8fPsQgZANgvQXb3RCcD61NL77d0cMTr7Xzr\r\nN3Lq/ML1YLc/WwL4uV1XvpMQMwALFL1p63BU2c0ysO31zbLOjMKAJi0hHFDYz5ZQ\r\nD3xxtc17ll3B5IqrMnMHRqmOQ39Sbe56Y7T4agaIz/sUWpseo85D5kt7UAIOR+Mr\r\nQ0Bl/QinETk72afGR46Qvc7tC1t9JjQQD3AUbEGuJdGvXjJJ9GREYu01XoODmPdT\r\njXXOI8XIOK6kxXhPHUc3iWu9b4KqGm0=\r\n-----END CERTIFICATE-----"},{"id":"gpxpod","categories":["multimedia","tools"],"userDocs":"https://gitlab.com/eneiluj/gpxpod-oc/wikis/userdoc","adminDocs":"https://gitlab.com/eneiluj/gpxpod-oc/wikis/admindoc","developerDocs":"https://gitlab.com/eneiluj/gpxpod-oc/wikis/devdoc","issueTracker":"https://gitlab.com/eneiluj/gpxpod-oc/issues","website":"https://gitlab.com/eneiluj/gpxpod-oc","created":"2016-10-31T10:57:44.387319Z","lastModified":"2016-11-23T17:27:37.793159Z","releases":[{"version":"1.0.8","phpExtensions":[],"databases":[{"id":"pgsql","versionSpec":">=9.4.0","rawVersionSpec":">=9.4"},{"id":"sqlite","versionSpec":"*","rawVersionSpec":"*"},{"id":"mysql","versionSpec":">=5.5.0","rawVersionSpec":">=5.5"}],"shellCommands":[],"phpVersionSpec":">=5.6.0","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://gitlab.com/eneiluj/gpxpod-oc/uploads/963bbf246412bcbe8979bccadb3b8d03/gpxpod-1.0.8.tar.gz","created":"2016-11-23T17:27:37.783365Z","licenses":["agpl"],"lastModified":"2016-11-23T17:27:37.862469Z","isNightly":false,"rawPhpVersionSpec":">=5.6","rawPlatformVersionSpec":">=9.0","signature":"hqhMh1l/mnwbYf4uPzEjjLFtZWHidzgR57X471OuXv2K/s87T5WhIkTSKk+2r8sp\nS7CtrF5+Pc5AgCCHvwzawN3e2+4eO4cK0+HD9CCzygzzHZEbSjufNHMMQucVoSD8\nPqR6MV9azzUpwHa/5d8fp3cFLVAle+aG0o4v5eHky9c7eaKxVJcgfjw3pjDE73N6\ngJVdtw1jf1kOFYk5pZQxDfBKFDrO5BRo5ZfZGuOuP2u/SmTwj42oTZiT7oTVWhqd\nLvJw+2TPv7B8s0Gin+J5e9K1Rs6CEWQ6WBxM+NhS5KgWB5Ig3pwm0QvMgza2cvoh\nlwVobOotfKLUBJzg0+wR7B2YH9Ao+m94h93vg7H0OKPReoTKhlDj2UExoTyeurV8\nhJdQv8sKVAxjC7/xrVaGSjM4YxFdBpzq8Zl8z4zq1o2voH5+u4ko3c62C1loDpsC\n8KrL1t6A7QpMk/XAMrPqwEPmFqlLEdv6FhzpOGyt4IEVnv6vdMTShcYw3tPvU/mD\njPtiVwpo8gWbGVIfpmwBg4wPaTrWK8V3+/1iTahIQHZfu4Lebb5mzht80HLQIcd8\n+oB4cGDEX4Rix1WxnCmE5ZzURY8xQXcvqYN+mTrUDh/3OtxQPSm5yC945SGoFNpr\nBYxfEyQcwulZrOMBdY0Ssj1AB5NOeC9OHwjJrnVe7dQ=","translations":{"en":{"changelog":"### Added\n- save/restore options for logged user\n- option to choose picture style (popup/small/big marker)\n [#25](https://gitlab.com/eneiluj/gpxpod-oc/issues/25) @eneiluj\n- add average speed and average moving speed in comparison table\n\n### Changed\n\n### Fixed\n- bug when python PIL is not available\n- deletion of bad parameter given to getGeoPicsFromFolder() in controller\n [#20](https://gitlab.com/eneiluj/gpxpod-oc/issues/20) @eneiluj\n- bug in file cleaning, bad use of array\\_unique\n [#22](https://gitlab.com/eneiluj/gpxpod-oc/issues/22) @eneiluj\n- python script do not need to be exectuable now\n [#23](https://gitlab.com/eneiluj/gpxpod-oc/issues/23) @eneiluj\n- jquery.colorbox was brought by \"First run wizard\" app, now included\n [#21](https://gitlab.com/eneiluj/gpxpod-oc/issues/21) @eneiluj\n- avoid JS error when failed to get options values by ajax"}}},{"version":"1.0.8","phpExtensions":[],"databases":[{"id":"pgsql","versionSpec":">=9.4.0","rawVersionSpec":">=9.4"},{"id":"sqlite","versionSpec":"*","rawVersionSpec":"*"},{"id":"mysql","versionSpec":">=5.5.0","rawVersionSpec":">=5.5"}],"shellCommands":[],"phpVersionSpec":">=5.6.0","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://pluton.cassio.pe/~julien/gpxpod-nightly.tar.gz","created":"2016-11-16T14:06:33.937534Z","licenses":["agpl"],"lastModified":"2016-11-16T14:06:33.971502Z","isNightly":true,"rawPhpVersionSpec":">=5.6","rawPlatformVersionSpec":">=9.0","signature":"JtUhKRDFGYDx9xtHjdfEUFOb0O4idexUYw6ixlBhKPP8Dn7NfyBfV6KH6MJTIVLU\nQ5Jw6tv/Nr1YDOvVikcWPG0p23mQdn1+7w8DzzIGKmknxCat9/vKr83oJZdWYxS7\nTJ4I7qTvWNlbMfK8OEdl13VJXgc6ftX+1isluLYqLjEm3aBFCS+/awYNMmXO55a1\nyG0NgJRu3pw1CBCMhDaRLsunhpRNDVLsamZj1SPmeT8qy0I/arFaG6hQnAo6JosE\ndi1XkvK6TEt9g16L6eizd+JpGE7xiWFP9ZEmMmmQSOLQYwU5Sk1YWcrW3EX4vtz5\nWnEIC0SENyyAyzBO6YJfu/EP2lLnlbNJiuc4zzKLqRw/zyz3j+imJLcXHIA78ZkQ\nuyUOBkkk3xeyBGeUcYfDuBqYQOQs+F/7+cNMsIBKJhx9ef3OPURBc7X16upk3mxV\n6GsOktbHkgUwWk3WiXRriBIqbAZocvDp0+PN++PAEZVWFEZEJzztd4Fxaeo+QSN5\n5Pz/9yXYRsoSPZv82Tlh7dx5tIPUvYb+UsANh5eGWUGufTSwgYBN0H2KT/iO35D7\nkDzNyh1qNakfBhAgPjrC2p4mBKBJJjlM0D9erDwr5D4GSTW2fp92vlRHeD0X8sqo\n3kBbwGuWnmhdJhbd7zYy0jVM6tVX/zgbhycimNALG0I=","translations":{"en":{"changelog":"### Added\n- save/restore options for logged user\n- option to choose picture style (popup/small/big marker)\n [#25](https://gitlab.com/eneiluj/gpxpod-oc/issues/25) @eneiluj\n\n### Changed\n\n### Fixed\n- bug when python PIL is not available\n- deletion of bad parameter given to getGeoPicsFromFolder() in controller\n [#20](https://gitlab.com/eneiluj/gpxpod-oc/issues/20) @eneiluj\n- bug in file cleaning, bad use of array\\_unique\n [#22](https://gitlab.com/eneiluj/gpxpod-oc/issues/22) @eneiluj\n- python script do not need to be exectuable now\n [#23](https://gitlab.com/eneiluj/gpxpod-oc/issues/23) @eneiluj\n- jquery.colorbox was brought by \"First run wizard\" app, now included\n [#21](https://gitlab.com/eneiluj/gpxpod-oc/issues/21) @eneiluj"}}},{"version":"1.0.7","phpExtensions":[],"databases":[{"id":"pgsql","versionSpec":">=9.4.0","rawVersionSpec":">=9.4"},{"id":"sqlite","versionSpec":"*","rawVersionSpec":"*"},{"id":"mysql","versionSpec":">=5.5.0","rawVersionSpec":">=5.5"}],"shellCommands":[],"phpVersionSpec":">=5.6.0","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://pluton.cassio.pe/~julien/gpxpod-1.0.7.tar.gz","created":"2016-11-14T00:57:37.521001Z","licenses":["agpl"],"lastModified":"2016-11-14T20:35:45.363487Z","isNightly":false,"rawPhpVersionSpec":">=5.6","rawPlatformVersionSpec":">=9.0","signature":"SigBof6QJZ9IMZyFgc+B3LO2+EXaAPvnxUHjJQjIl3jLzomocpDGR6WjO6gtvB81\nzXUHjJ8+huc+P9TvgjUGRTmn9a/29HZ4IKTXnYBKIUY7wSLcJNMbJSp2Zd3OFHAG\nJwRaEdh/cIRk2X6NE1VT6dFCxB+LhTM4BXOEwuNYQvU1lZDVQgTz/r68zFLWBt6R\nqhBCNJHrVp87ecS4+XaGq/CfT4k1ihiOv+f4eX9iaPzUhxBJ71iYKF7wHpDoVmIk\nNrzWFUJH3BLBuW9oiC0PApli6Xu5RXrWUsOV7OAmxXgylRCPuTFwe09hw16JMbiS\nii8WFiUtp4qW53+7eoS7Fllm7CRi/Dg6Jvjtp3msrf1m+OiYM7dLyoKw22/S4P/a\nBIErZpSCHaCfrZ+DBXrAYcas27GWE7HizzG3yXk3aDJMa0Otcsq56bSPo01JDfNx\nm1y9iuwmlon8zKKoxsJCwxaFDnQpqazaLcUO0ATHUk8LdomTA7MCXVvNFPaO86Ea\n16iyw7Cfs0k3GrvN71+SdpvWss359CEEwBMpDwJZqwSFbLRyHtogUgbRWLIJqR4n\n5uVvJqirxWkr/EtXw6UkDWAI3ZoMhMRtjn4H4ekANP5mC8R0yp+UuFs2RkEC5uA0\nKzzh73WmxmpeUl6jcMZ49gXn3PTCC2fJNrdmSYch5Dc=","translations":{"en":{"changelog":"### Added\n- option to choose waypoint style\n- show elevation, lat, lng in waypoint popup\n- ability to display geotagged jpg pictures on the map\n- pictures slideshow with colorbox\n- pictures work in public dir link\n- use NC/OC thumbnails to display pictures on the map\n- options block hidden by default\n\n### Fixed\n- fix bug in geojson generation for waypoint-only files"}}},{"version":"1.0.6","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.6.0","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://pluton.cassio.pe/~julien/gpxpod-1.0.6.tar.gz","created":"2016-11-07T12:11:00.619161Z","licenses":["agpl"],"lastModified":"2016-11-07T12:11:00.699921Z","isNightly":false,"rawPhpVersionSpec":">=5.6","rawPlatformVersionSpec":">=9.0","signature":"WBts2fm2rW/7LMHYjcx9a0k0WTXV6PnGRxTl+158cjfV7ruMpNvhK58iTjrox69k\nFWAoIi1wNAlLBu9Xet1j7HKi4TC9q61IEN+lPlnwBCu0uHawiqS2gqB4i8A019Ei\noLsgAPWh8ndy6+gyUtPhVLVduLH76aT6KTwAiHPknV0zPtxsUy1P6nbbNOS5A5rG\nSQBqljy0TbcjOctTudEPp1IqjJIwcd12eZ9MLG4CEIO13n53pMAsuXJf4jnKSCm0\ngimvsFOwFRjBab3ZPwtOqeVw6aIh/lYF3U3/k8YBpaDN74m30nDtkp8teXBgshSY\nVYvX3yOAYe0PIR419IX0eoHb61K0VfZYvPT4FsOqjDr0zlVB8Rjq+6SiK4vMD2+6\neGE0aHbjR9HV5jymUnFYdm/hlhcZGaKrAiQKfBY6Vh0SWKfIv7bdGfQYauePAdZt\njlsV8lIwOy7FGAeP81CcjzWWfDeBgYr+MSzfoDNoTi41MvUaT14iWPIU/s5P1/Qv\nALGxgsbmB19wEgNbdh1UtTUY3xARLpWPYdUqU7yDcsddX9vCoCG2G5wCcbYJRj8o\nC+H7wdgPJoiMY/p4Go/lyWkvmzhfXrOeXytZIFXjb3ERVd1vD9WSt1DSy/7gsFYt\nxzzOPgqMvL3BbeluNuzNv366oT872s3OuFKa1ZOYY7A=","translations":{}},{"version":"1.0.5","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.6.0","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://pluton.cassio.pe/~julien/gpxpod-1.0.5.tar.gz","created":"2016-10-31T11:08:41.017766Z","licenses":["agpl"],"lastModified":"2016-10-31T11:08:41.068782Z","isNightly":false,"rawPhpVersionSpec":">=5.6","rawPlatformVersionSpec":">=9.0","signature":"UXeZVh5f0/WZE+r/dHWc1pu9q4qr/zACX6XraMhhIuKIp7vNCwUESeqJtVc99QZw\nw9kJp0isvib6l0zbQBRS1vI7xwKKBQyeaEhIciEs1JjsaCiato1Gyi26N+fY2N0Z\nFWlTwCsF3DdlwERXTYfeCpsOWCoxLxHKhnJIjUc0PVme/Ste4zxYj+5Su1RpadSw\n4vGnkW8zy/0tzua50NQCrOg+B4jXzH9kMWAP47w3sdP5CYalHSHl8EX0D1RjgGU5\n7vZYX3wF853FvQDbL4JXXCJupj3wZe8py8McWpQIcgz1p3KmE7A7d/rdNWExT+T+\nDxtStJ56qTRMz4aFwoSFxJrrEfgHIsE9Gv+Vo7nshCUYA8gkfHeckiaUtH1EiFTh\nVNeO6mTIqGpRosFvfUrZMKcuF5j74vGQjNM1o+M5N31gtLoPSkU605f/U4v+j2oC\n3/N1rYF2SEDFO0EgAGXaXVhB0ltSDkHJw6vZJ1L8Qz6tooUMDxaMri8vycA6LHvE\nqN+z+S6TXMfLvN/6ATYPGhicrWmkMT/k7v1az/hcnfH+zRyLZyFx94s88JWF7Jf+\nI+tgDbfqTMAIcytJUC+KfdQW1ueXh5F0owrOYM6jgBRvhqj1T8s+Twgw8orGmRPe\n9h8G9Z3wZAooQvmC0KdVhLuOeIkqt/S5krELNFFBRnk=","translations":{}}],"screenshots":[{"url":"https://gitlab.com/eneiluj/gpxpod-oc/uploads/db5af6ba7ae1cd4d22ea81d418f5c762/screen1.jpg"},{"url":"https://gitlab.com/eneiluj/gpxpod-oc/uploads/123588561a8067185572a8d1887ef906/screen2.jpg"},{"url":"https://gitlab.com/eneiluj/gpxpod-oc/uploads/427688b80bf8428dd45bd15d69b19075/screen3.jpg"}],"translations":{"en":{"name":"GpxPod","summary":"Display, analyse, compare and share gpx tracks","description":"\n# GpxPod owncloud/nextcloud application\n\nIf you want to help to translate this app in your language, take the english=>french files in \"l10n\" directory as examples.\n\nThis app's purpose is to display gpx, kml and tcx files collections,\nview elevation profiles and tracks stats, filter tracks,\n color tracks by speed, slope, elevation and compare divergent parts of similar tracks.\n\nIt's compatible with SQLite, MySQL and PostgreSQL databases.\n\nIt works with gpx/kml/tcx files anywhere in your files, files shared with you, files in folders shared with you.\nkml and tcx files will be displayed only if GpsBabel is found on the server system.\nElevations can be corrected for entire folders or specific track if SRTM.py (gpxelevations) is found.\nPersonal map tile servers can be added.\nIt works with encrypted data folder (server side encryption).\nA public link pointing to a specific track/folder can be shared if the corresponding gpx file/folder is already shared by public link.\n!!! GpxPod now uses the owncloud database to store meta-information. If you want to get rid of the .geojson, .geojson.colored and .markers produced by previous gpxpod versions, there are two buttons at the bottom of the \"Settings\" tab in user interface. !!!\nGeolocated pictures can be displayed if python PIL is installed on the server.\n\nGpxPod proudly uses Leaflet with lots of plugins to display the map.\n\nThis app is tested under Owncloud/Nextcloud 9.0/10 with Firefox and Chromium.\nThis app is under development.\n\nLink to Owncloud application website : https://apps.owncloud.com/content/show.php/GpxPod?content=174248\n\n## Install\n\nNo special installation instruction except :\n!! Server needs python2.x or 3.x \"gpxpy\" and \"geojson\" module to work !!\nThey may be installed with pip.\n\nFor example, on Debian-like systems :\n\n```\nsudo apt-get install python-pip\nsudo pip install gpxpy geojson\n```\nor on Redhat-like systems :\n```\nsudo yum install python-pip\nsudo pip install gpxpy geojson\n```\n\nThen put gpxpod directory in the Owncloud/Nextcloud apps to install.\nThere are several ways to do that.\n\n### Clone the git repository\n\n```\ncd /path/to/owncloud/apps\ngit clone https://gitlab.com/eneiluj/gpxpod-oc.git gpxpod\n```\n\n### Download from apps.owncloud.org\n\nExtract gpxpod archive you just downloaded from apps.owncloud.org :\n```\ncd /path/to/owncloud/apps\ntar xvf 174733-gpxpod-1.0.0.tar.gz\n```\n\n### Post install precautions\n\nJust in case, make python scripts executables :\n```\ncd /path/to/owncloud/apps\nchmod +x gpxpod/*.py\n```\n\n## Known issues\n\n* bad management of file names including simple or double quotes\n* _WARNING_, kml conversion will NOT work with recent kml files using the proprietary \"gx:track\" extension tag.\n\nAny feedback will be appreciated.\n\n "}},"isFeatured":false,"authors":[{"name":"Julien Veyssier","mail":"","homepage":""},{"name":"Fritz Kleinschroth","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIIEATCCAukCAhAaMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYxMDMxMTA1MTI2WhcNMjcwMjA2MTA1MTI2WjARMQ8wDQYD\nVQQDDAZncHhwb2QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCq9p5l\nzNzR98l/xPgrplWrGQBhF6aQSLpnIyCdLAYKk+CmXn47W1pHh5CRYyCCyB8YPBBG\nTwbpP69pP1updfK2vUt+ShYmCXLxOLB3qEdhnwgqFfwpC48Ocev9d6XcacYp7jwu\nRNtv0ocMkm5o0TWWupcutQWkmqzCVq+OkrqM0xrf3mfPymuM6edEREshukoL86Ei\ngTuMMGT0XO99LikszvdceNQYEATix1MHzSVhkE7jHCNBXb95H6nQGr0v7R1MIbrI\nGFlgqxwwNNKwBFNfPMWZVnKwz9hoIwW6WOuu7ntwVcPqwB/gUsRZJTu7EjIW0trX\nnhA6xLlc4X66W1sdUCkJOxsV+Y21akz6wynI0SzIfjALLI2Ls4QWrPM8GOX8nPVM\nm+Y5WXzqLJScdWYoefFJKS7kxwUJRewREB9ykCG5OdDubV+Iu/6jh6HWx3h4p3ih\nqkDypPWoxpfLgA8VZkLD1RRKGkRa858QBGdF/RHbYT3JfLEp9l9gJVKZE/yw7HKk\nwsZ/T6CMpLyorpd1XWtp2wLX8lr3pp9ecVDOdAMSqD2thDMDsZA82JrJ/vITwkCF\nBlqtDZmT0UnpxYNYTfYBam5Cd00jsqCt+Hr+QkODNe8Yae9c/D0zE3h2Vt7g9H+W\n7Ei+rF5nDYTBAApoETxK7+aUZpycBf3THAJOcwIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQBbCGAEwg3M5QJDMnZgu0zNOH2f9bamAS9ksyCZqzLoeQic1W7GYYe9NqAi\n7lO5jXRZpTN4L133IUQPtxCxuDooD2vFmCne92tLxJbc7uqlSVfhL8uMVOlnrA99\nKTAhySTZU5so8/OibrngnBmcdWwbhaWoCQ671M8aXM1wg2FVYDqB2GP3RvbpW11L\nOc+4tfh4mO4TwXygf7KYMOJyJW8mNNY7PZ+XW2Qe3vSXR3DuN8H8fgMh5wppXPJf\nE0+yNs42hwFjSojtI8BCb0s5DTleaakpDo8HQGNzEXP8tBlUYudtjzdP0jxFXbFa\nsT9pcMdeJ0/t5HqJSx1EjUCLYS4y\n-----END CERTIFICATE-----"},{"id":"ownpad","categories":["tools"],"userDocs":"https://github.com/otetard/ownpad/blob/master/README.md#mimetype-detection","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/otetard/ownpad/issues","website":"","created":"2016-09-29T15:58:52.814912Z","lastModified":"2016-11-19T17:37:52.278497Z","releases":[{"version":"0.5.6","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/otetard/ownpad/releases/download/v0.5.6/ownpad.tar.gz","created":"2016-11-19T17:37:52.234684Z","licenses":["agpl"],"lastModified":"2016-11-19T17:37:52.423930Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"dh+Txg1iVfqXr8+cxplNQuBZGErSnXUo0ewGwnybNMJqp8/EjEo72+zPpW3dVnhY\n67YCvhrm2bo+VRdFFymEfymzSJu9nWVFkGJhEwvTxPyIdAtuD5YAVrzmnR6L+H7m\n7Q1nXE63ICPCAQpHkxIfIXLh25OhWeyofBB8AVsjDUNn58FEYJ8fFkr6dCgPriZS\nsM2J+xtZMDYufy+xFMsVf/Q3WopjFuBjMC3qOecW76ZTwtREaswOC2RtpzUku2r1\nsogrfFlFer3Ii9/CWgOktnLfjB1DzbTwdEkM2xNVBRJgdMXt2VLA9FsxFFkjmr5A\nl7x9cNLWA8RLpOIpIMBbaef75u5HgRBvSvq114UsA9GCu/EYbIgD8YxEt7xuKd4t\nenksJB5gJ2IQNdHrPbsil59AsJ/dismDN6ktYgWQEk5dINzvm9EAvucueW0Gt+Jr\nqEC5WBgJucsFxSvkHh52v43M8jgPYBfHWEL/M/+377z3+mbuIh+BcQ+vcDdiqxTF\no3n0+gw3QYIhLEe75sUhxG6ynVUdW25AKKju1kVj3KJnZTBH1R8t8/zy4DnJG8d4\nuRGqyU4BXpZjEC3nVlsC7vCncWWhxl0WZQ/MWKqsfjVAU4I88E518D6NioqMnPEJ\niCZ2x+69UCDEQyfCSKajgAYT17r3OhZei8F9KSCH8Vw=","translations":{}},{"version":"0.5.5","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <11.0.0","minIntSize":32,"download":"https://github.com/otetard/ownpad/releases/download/v0.5.5/ownpad.tar.gz","created":"2016-10-06T07:51:05.278637Z","licenses":["agpl"],"lastModified":"2016-10-06T07:51:05.348825Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=10","signature":"nYsQ9U5r7uXwtcquaWGm2XMJBNYCcA95aUx9gLZ/wEmjCHqId7+MzcCdBnom33+j\nat2XR2a4R96UAUP78bfSC4Yb7nPepFT51Y8CSpV3mDT85/+SgJdq500fXldq+qGY\nffXLneadAztyOfZO9TXljaCLdREYI0LJIGVENsxBQVKM/eyGIuZO7fF70cH5vbfS\ns37+BXB+fxcDTlP2Xuulra8HsNoS81bzjsdVMLM7B7QwwO6rZ1zd5c3UzQ1LmY5g\npQUBNd0KjfHfZ6+Fd64XZO6NGfgucGNmL3lgxdsfUqKiLtikvFxK39dYW5MckV8p\nvLoS2nZ7cgETQmAW9Ahn3ro7gXWcPxzL41oWtZOOHRRC2Yz5zlZ3Bky1o+bF9g5a\nYdDF13zV6utUkhlplZhWbjKaXa04rzOvmut8Iqhx/tmDtZRYtaQXJZWutVJYtPC3\nH86uJJnUHHNFHeoT560mp1Hq0dTeR+G+yWsPacPD1rTYgZOUVEtj3Y+YdbTODR2o\nOdGzeYFl+6CL/OcY4wPGRUCTFwvc31lIUd4DK5SPfN+IGtuuXhAqVhwy6lpkcKRs\ncj8sBoVXbMvEtMnt5uARBvA4tyVffUL4oyoIsUnvXz4u+q4WVt3T17swLm6HjGVC\nNVqU0srHN7EeBRhHlXP1CrKQWGQlS4k9j9Li4Iw+X8s=","translations":{}}],"screenshots":[],"translations":{"en":{"name":"Ownpad","summary":"\n Create and open Etherpad and Ethercalc documents.\n ","description":"\n Ownpad is an ownCloud application that allows to create and open\n Etherpad and Ethercalc documents.\n\n This application requires to have access to an instance of\n Etherpad and/or Ethercalc to work properly.\n "}},"isFeatured":false,"authors":[{"name":"Olivier Tétard","mail":"olivier.tetard@miskin.fr","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIIEATCCAukCAhAPMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYwOTI5MTU1NDA3WhcNMjcwMTA1MTU1NDA3WjARMQ8wDQYD\nVQQDDAZvd25wYWQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6CY7I\nHRJTaqDu376vt+kruX+QOL864joScxRuh3IOVcQktCvxasuA0EtrX7TCAQrV1tBK\nfkqJxU9uOV54RTgyh30yH/ZtnF2bYQwViGM06Snc0riqWydFrN5fxK52dpZWs63o\nUFCNhHxrX4aUGyfXu5nQMISLm4QHoZ3LDLofk1ZsiK62fM/Jz8N2PM8qeHzf1ATo\nSKcAOd3UeaS9C8bv2DuiZM7unkSO/tjrBzkMiq8ds9sIzBBsyk6BRh2HQjHPOtmO\ned+pS9mIZmc2xhssXoHL4IfZwTqwhktpsaTl7v0ROw2dwDATz/QoKMkUpboQ5lkz\nwgLQhoIZw6uAZ1R/Qjze59I3iU8zIo9quDarHBotZNXboYCmg9FRfE4mHtegVaa8\nv1a1JvFQ5gvsWEsKSV6Bzb65GTp4KG4q7YnUrzh6HJyDCGLvLlWm5OWsFj6sNzXX\nwLOv6JLORMbF4ZIo2iybb3x7gdfCu9JxMZ4JtOUC8KSJ6+ub15C1Aia3lN68dNts\nY6KwUF1Ted0o4OQPAulq5pUc+g6dTYmIKsavIiPKhMtl86AbUK50vRTeuGdFsT7X\nav73IanPdFI9bKth+tajgvB6dxcVnvBXbrsLUyEcsxsxtBJvQcMYS4aZ6ZJYLTep\n7AdK0Zt1iMdXB8+4PCps4rcG6bYB/uJeEAVm7QIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQCM10O+sCYhIExnx01vGzKlnRS7MSQNx8ZMmbR5Elfz4AVJAEJ96ytS2DXH\n2c+hcD0wAenXQEFk920AEqFQBT8DP34p0FmF83aMHW08ovzFiu4MdlhcqrLnko0h\ncZTXHVyS/8JZh+o6SVm8R0/BBLF1MQQ5TqRkJehbmk6gL0+MSYxehUDKWTjJITkR\nifneTw/Ba1d0AXBOq0c0HFyGxMPIlWe4qn5LtxH5t0wyVGeSj4jyv4nvd3ZGuAgY\nEUa2uYht/z475k4+vf0YhV98iQH07GnmlfD2TDZgmOCQGKlNfJh1v88OZyLLa3dz\ngRHzGwKbAiJ8T8bbpZ3e2ozXxADr\n-----END CERTIFICATE-----"},{"id":"announcementcenter","categories":["organization"],"userDocs":"","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/nextcloud/announcementcenter/issues","website":"https://github.com/nextcloud/announcementcenter","created":"2016-09-14T10:38:53.939634Z","lastModified":"2016-11-24T11:21:50.324839Z","releases":[{"version":"2.0.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=10.0.0 <11.0.0","minIntSize":32,"download":"https://github.com/nextcloud/announcementcenter/releases/download/v2.0.1/announcementcenter-2.0.1.tar.gz","created":"2016-11-24T11:21:50.317635Z","licenses":["agpl"],"lastModified":"2016-11-24T11:21:50.386203Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=10 <=10","signature":"lmqeE6xBqUJfhuXPbjCfuWiIP0FEB4V/SsF/OvYar6rLpvDpJVf3DJoeIoxXurRP\nE9/xCcNN44P8PreRRDnFLCa0XsKOtwoGa56Lxk7IKvtiQG6xu4J6PKM+q/tIeF9K\nakw0LQXtjZB5InPhnCDDbY5YS9jgGEBylSHsgNgrElipcW+BzOBu1Amw4FECVlQw\ncQ83bio+YPZvsnE5+v3/bAx0m6QNxfyN9Sn6rMEqRkY3jfA3vejXGQH/XkputfV+\n5hOz48KbOVg7cKxg+ieJlSwC0aYjb+RXiopjc3icCoIF1llltOOeSsVYSflOb080\nupociPgQ6qIab/VNNXa2YQ==","translations":{}},{"version":"2.0.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/announcementcenter/releases/download/v2.0.0/announcementcenter-2.0.0.tar.gz","created":"2016-10-06T12:41:56.195206Z","licenses":["agpl"],"lastModified":"2016-10-06T12:41:56.263124Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=10 <=11","signature":"NVWYz73KtuoZ7ti2sluztJO5aFUc7PzhlDcg0VWyAQd1H7sk5wjw7i0bhrjw8O7M\nLsrb+PegnsL9eMlYM2WrRom+RF1PDP482xymZf1T8vh8qcTCm3TK89xSuiSm8yoA\niWUb/Uv/ODj74wVDWqWxAFKaAG/FestCB3InOOZQqQZLzlAV0U9ziYDGNzBjFqof\n9rLNxJ2IOqZOA7hhMIKhSrpA0KkSfNhBsVf8CWClYnVkZQiq0LoYkHkHIlXmXUr3\nOfQFKEjtsx+bNLa6CkAaocHGHJXAofX3GQZ9cjBsjZqiTfbXfcVk0kRfz7pwL92L\nI1McfJYvgMxDQG5bjRpNJw==","translations":{}}],"screenshots":[{"url":"https://github.com/nextcloud/announcementcenter/raw/stable10/docs/AnnouncementCenterFrontpage.png"}],"translations":{"en":{"name":"Announcement Center","summary":"An announcement center for Nextcloud","description":"An announcement center for Nextcloud"}},"isFeatured":true,"authors":[{"name":"Joas Schilling","mail":"","homepage":""}],"ratingRecent":0.75,"ratingOverall":0.75,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDDTCCAfUCAhABMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwODIzMDkyNTQ0WhcNMjYxMTI5MDkyNTQ0WjAdMRswGQYD\r\nVQQDDBJhbm5vdW5jZW1lbnRjZW50ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\r\nggEKAoIBAQDPx4Hp1HdBo5v7bDEiVcv2UrVjNW+fPPKS/5rwbagtPcE/1v3WDcwX\r\nvFwaXk8qCn2UpPSQ2b1rTuTDm51G1ZmEZhNiio+rBfEe9F+3tLsq9lElqIPKhkAq\r\nEUVI6dcN+jSqvLmLhuwloEoQQSYaLrX75mY3lGqTb83h1l2Pk/brVixuVf4vJW31\r\nTgeieuGKnC+keLzKOrvTHffJakU8ktwB2Nuu1o+jN5a7u1bxKkP3LjEWPjq236hk\r\nAoOcW/wi1dUEyUKUZsZQeJyvTJh1UXdLHKwYywtUu1/VLZ1IUtNyPBfiQ8ukPp3T\r\nTnSSmG3ZnvsfM6DmAvLZ8bBQkMBzEcTLAgMBAAEwDQYJKoZIhvcNAQELBQADggEB\r\nAAB3i2NgiZ4rpNag7cXYdaFxAxdDWnke1+LX2V2R3hzGmx73/W6cKLpo3JBn9+zT\r\n1aEjlqkt0yHu4aAPVYQzOa5zIV8mjP84p3ODSyV9J8lfjFNXT7wdA8+9PVx3lVki\r\n2ONoCNBh1kOxnxI4+BsMlQfF00ZbBSuGcMm3Ep3lTFWXzuUn3MQITzPwkL5LkW6a\r\nsli/yAYQRTVDsXD8A3ACYT7BG31jGxyXtIHzqCci0MhZFdKKayMYkwfjZchIUtGN\r\nJJoU8LQoHwGRtp3wutk0GlFzpEQEvSsn/Lsvvot5IfIe46tnzA6MVj5s64s5G8+Q\r\nphhXFlzXqO/VxquPdbfYjvU=\r\n-----END CERTIFICATE-----"},{"id":"ocsms","categories":["tools"],"userDocs":"","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/nerzhul/ocsms/issues","website":"https://github.com/nerzhul/ocsms","created":"2016-09-19T21:56:04.745481Z","lastModified":"2016-11-11T16:29:55.864273Z","releases":[{"version":"1.10.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0","minIntSize":32,"download":"https://ftp.unix-experience.fr/owncloud-sms/v1.10.1.tar.gz","created":"2016-11-11T16:29:55.856768Z","licenses":["agpl"],"lastModified":"2016-11-11T16:29:55.947926Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9.0","signature":"hVzbkmmtJCtiOkZGe1mkWElqS3IPQ8wLtSzikVvoKmcg+Zq3YLQjpQWzy0t3UVjo\n9I/BfnL0bF+kjtGc9xF6M8IQaFqPrfJmN+lNT8WYIKLI97TTsLmJGg8Q8PAux3nY\n8/NxMjWdByMw9nVBClKo0o9eSW4+EnaUSJ62Gl/XWjq728kbB16WZm+iesk8LjJJ\nqqAgczWGwz6lkZTCN5o9n0a/YoLJTf4iT+OItHZyS609Cqaxx9CAmZPj/Xn5g1fm\n8hqO2ITAXLoBj4rYS/QsZTMcubtGkJ8fq3XYKVSv2UXZfvGsNWbbGV7puKN33uWJ\n5MrdoMlJ8XnJRPDlCBcb00LY+AB+hAMooLnNy765/Ew6ztp4KNLEPWGG+Ut8/Lkk\n0jIULl1RF/FjlW8P26NfwH36K30RCJFY06OFcWnxGBkkQaNFORDIsKcqTAxkl4x5\nnfKBkNdQZppCVfOSKOZj4NkWfWx75Ouq1S0QksmOsMZoOcjy1TbBKR8h6dt9DQub\nWpYBL0QwyQShGp0Vb1qCKkP69ZQAHVUJNzIFPz9LyoguvFyv8iZmAPLYDnFBvlf2\nnSHtA19rnJmZ4H7RJ02r6BdkstxISvEiHU7RLjNQxcb+DptIWX5C03wH87HTNIhr\nvptPorEoSY1KwW9fqUvvLE/c+vfkr5cvIEwZlyVKVXU=","translations":{}}],"screenshots":[],"translations":{"en":{"name":"ownCloud SMS","summary":"A app to sync SMS with your ownCloud","description":"A app to sync SMS with your ownCloud"}},"isFeatured":false,"authors":[{"name":"Loic Blot","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEADCCAugCAhALMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwOTE5MjE1MzU5WhcNMjYxMjI2MjE1MzU5WjAQMQ4wDAYD\r\nVQQDDAVvY3NtczCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqZVIzM\r\nwBJuacuvgYKr2KnXuYhjKjZ58nfspSebsaGLr0ifBbo8L+NH5eaynnLCNhpegmu0\r\nO8D+KrbM1LtIkcdg1/eFpN5dTc6G2OAc9H9stmHs9nroF6MNhszgdZCz8Q8xkSoD\r\nGdSm8hdPg5GcfLrH27UilGtzdQlWJ1DralLMt3l+SfGJo152c/dc+e6SuT8+EbY2\r\nCeLdH5ImasXNpUgY+tAoEt2ZvhBrUghykBJTJVOjwL1jGLT37ybMtV4FBKo6hpeg\r\ntq/YzEk1ijBAC4pmoNWixDKCdolpVJVz0fijI9mlda3llurcp8eMhxfYJ9soXLHp\r\njvLX02YY6RfPcyy48uWVk4IEt9BvZWEVAAp7hCGA2yXrVSsR37E6sDbLsBcKav9A\r\n6dkGAgfmGkr2WT6O1/EhK/MakmnYO4WD1B+E7PnxtP/wOa+aQBmntQcd7igDiwzG\r\n6h05NYAWcRhqfZ4KWYsq0t0SezMbuHOhwzzi22q8wijC5YZbmhKSh+b3N8XwYKDi\r\nZaw+fSahPbRWaLyR3wn9zh7vKCwqrG3ugrNo6CtyoACAnmxKZ97ROFJIQTe3ndLL\r\nmv7Wy8iCZLhRYUaW/GKrF11AFwBVec9xmvkgU+PIKq2HSjwi9sCF+pFyhVjmq29C\r\nmZEPKUV7ySIpNHXpsXm8kTJJfqjSdb2ECbLfAgMBAAEwDQYJKoZIhvcNAQELBQAD\r\nggEBABvn97e8Nw8KAscf6FX/nZ99rEX+3IrZxTC8fmBgNwAvlbF2A+QZQcFI4G9/\r\n85nHK117+u7XDuwWl4QG3flWlI0hDE59Ud9Bd4AiTQ12VoXlNdYoTg/mXARxVozb\r\nKYqZ+1xRQclZKb2AqW8YiGo18okIKovn9VVRAFYPYx4O3Ve1FjgfsaMlIZLiXUFm\r\nkk+2qWo6kYsdU9FABLo6izx7RFOMbnYNre5FmDrWP1Dga/U7ErK/Dilh8g9b3HrP\r\nwP8OIZhdtFWw21wDTfyqrb9EhC/tsjPVP9u+bqyognHeiMhjbVYRbSvz5o8T7Mhj\r\nbxalCt4/LnMIfMwVyIvye7Uy2GY=\r\n-----END CERTIFICATE-----"},{"id":"rainloop","categories":["social","tools"],"userDocs":"","adminDocs":"","developerDocs":"","issueTracker":"https://github.com/RainLoop/rainloop-webmail/issues","website":"http://www.rainloop.net/","created":"2016-10-20T04:17:37.217555Z","lastModified":"2016-11-18T11:36:04.309739Z","releases":[{"version":"4.26.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":">=5.4.0","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/pierre-alain-b/rainloop-nextcloud/releases/download/v4.26.0/rainloop-4.26.0.tar.gz","created":"2016-10-20T04:28:21.491747Z","licenses":["agpl"],"lastModified":"2016-11-18T11:36:04.619927Z","isNightly":false,"rawPhpVersionSpec":">=5.4","rawPlatformVersionSpec":">=10 <=11","signature":"nTYIVSB6mIwKtXIrKoVGsOGFflpLjed8jFem1VLQNtXQj4bztnNrdc4YaPIn0yzM\nyLpMSqRDNzdYNFuOeDiyKLPJPTA++MotLCNjEe7kxUekek+m+qzgnGBdcT7RQT6R\np9xWGecnVx94d6aA55uiRhgQRyHpdDMMLCOz1be+HvpwHy69DRFZ1+SPmGUt6eW0\nu5yS0vHCu1K22cbrVNXFKjxAOlGcIDm61oQuz7ycl3uAujZO4rZbWt55jilgKGak\new559A5gTp9W+j+TWKIcg6LIZ9zLRlGjcQrWJrsc+OBZQcqiYimSFyO6HhfT9TPS\nPof//I+dSsd+H0SRGGeL8DvSnK+NKZL1q5EX5pziqsv6nZFITpCDwmAN+I8AnXXL\nSNkFi53M8RZTOABpD2x7YPYP1cEvwrRweqV/C/oHcYnpfh7D2DjFeWwXsjeAXrHY\nhgFhPrg+7rf7g6UmJFOCp0dC9sBdyQ3KtJkv7bGqPr854r2cdA7xW0QHWQ2in9qQ\nLhIczc32ECi3ZVVgyF8zyT4Y/3MRS05oX3FHvHyt88mjni6bVaO78F7ZRSha8gHh\nNOAkku7AMXPvUCHaZP2iVCCoAViEso8GeR3O8xh2G42Ai61RLYwx8LB1+23EoJTr\nmfFuRYNSg+qAKCokXNnh+lDlwu4AkaQo3vtKGPXvU7A=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/pierre-alain-b/rainloop-nextcloud/master/screenshots/2016.10.20-screenshot.jpg"}],"translations":{"en":{"name":"RainLoop","summary":"RainLoop Webmail","description":"Simple, modern and fast web-based email client."}},"isFeatured":false,"authors":[{"name":"RainLoop Team","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIIEAzCCAusCAhAXMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYxMDE5MTkzNDEwWhcNMjcwMTI1MTkzNDEwWjATMREwDwYD\nVQQDDAhyYWlubG9vcDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqB\n5jnF9qZ/qjckt0kRjpHCOMtJumW/KiQoMeNP5nGv4ad0DS3KemOapUef8Zn7qCYb\nMnODhK7HBwPifFzI1j8bnT2hP6E0geFLb0MdN59d2NF0n4CCs1+BnepQPJ1kFbPK\n35wQRi0RDeTf/GQ+/owEVCU9a9W1P/VUXk8Z0vMoQxCXEdRqnB63SgsKl7DB9G/C\n4SYrgGor+OHVGl4ntMZhJujiM996DttrNK3iZRGkQ07L+lfUIwQ52XOhQNRdic4p\nB03lw7PpChwPGMv/EEvdR5HpCJQBJniqJbbu3Jh8bMBKTE/8fCzN3vMXICB2g3Bq\nlKkZW6fnJRGsrZ79fsQnl+WBPNSrWRLOxOfe1fyCFV1ljFB4nTH7uF3pC8ZRgJes\nkHIESHz3GJm28hn4+17ESMGHBCbs7L9FK2GY31cobU0VRntLxpSG+d9njbIAgMG1\nS7U+oKVFQhSVpdXNOaUNqhcQ3HkbQTLEP0k53A/lhLQb2+KPd8nntaELjwNyrmZg\nsVMgHj/zdlvrbguZjZFzUzDBFvkuv/5M58lNT/D1C6ufVp/R6eLsYI+nnk1ojAjz\nl7N6U8X5SXpD+Bm7+Kn1PH+bHl7cViCx8oXJXO2RhP+COXckw7BDZKtjItYHNG7M\npFwgYqWpvCu9LN6IN5a/eLqSI76dOOP3iYbaTH+NAgMBAAEwDQYJKoZIhvcNAQEL\nBQADggEBAGB0Vq0l6ndGTgNbZxSEFyBR3u3tiR3pWK81DYjsui7qBoO6P/BaGmf+\nraSwHPaBOwA9XNS8jcGLh5xdqY2p/m0dTS64xNjVL9nweWsG+FwVnPANo8C4nXdm\n9ajJ4cdg54stQK8qn1uh/xPcd23GKfYJazjYSwYmZ3pXXdzlGN9NxkeYJQxJ6B+5\npzAeVGiABI/e5URpxzz2UayRX7EE+vtpe3B84hzkLqsq0N39ZN6KLfaTyEBGLzqE\niLYeXQTV0XSRs8xVt+iyGlj7nPkv2DR0oCqRpWUFWeSBI//niDG5WxS3qg8kacSW\nfDSYhSN+IjrnIkwNtc8V9t7/GeQB5FE=\n-----END CERTIFICATE-----"},{"id":"richdocuments","categories":["integration","office"],"userDocs":"https://nextcloud.com/collaboraonline/","adminDocs":"https://nextcloud.com/collaboraonline/","developerDocs":"","issueTracker":"https://github.com/owncloud/richdocuments/issues","website":"","created":"2016-10-31T08:55:45.631429Z","lastModified":"2016-11-24T12:13:53.905352Z","releases":[{"version":"1.1.3","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=8.2.0 <9.3.0","minIntSize":32,"download":"https://github.com/owncloud/richdocuments/releases/download/1.1.3/richdocuments.tar.gz","created":"2016-10-31T09:03:40.389355Z","licenses":["agpl"],"lastModified":"2016-10-31T09:03:40.439510Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=8.2 <=9.2","signature":"s5giQeiU2zwV5X6FmxWXiG9LtNDeKBlFqK+hfvGNbGZ+nic77Y+AnXHodV4lb3Ko\n0C0ThFLuafaZRdp9rBIN2K/acCfCYKJewGuYErb7FlEl+P9J4OQbb9pva0htZJw6\niuG5eyeTufi5MKB4vuj4+jo9zhepOFAtZMa7o+ZCfJkt8vDBuq5AXxomEiZRtW+n\nf9PPUnq0z7DJVwINhHvvBZJlSLjkpJ6VIHAr+/ElWr8O/mDKq5S5ohbvpDcPqR7b\njnsBckFDLFUz1FX9dA0JCJEKMMfkcfGqZcjH17NdjKAxRW2soN5cEKluu5MkOhz9\nFEPKfshzbrfUIm5MaFGv6w==","translations":{}},{"version":"1.1.14","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/owncloud/richdocuments/releases/download/1.1.14/richdocuments.tar.gz","created":"2016-11-24T12:10:13.337165Z","licenses":["agpl"],"lastModified":"2016-11-24T12:13:53.963638Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"prDGlfRPxqT6LP0BsAFPwGww7P4Bngha2N4u5B6+F02N+RVOjGtTcXKqvM1KjZb1\nCo7qJvgJmjpvIvDmB+rup02i8ObfwP2ct6UdsD7ouzOWJG2sJANXK31bHyvOmQ2h\nvKu5eNcOkf+WFyFKYi51TbsfWn2+1Wge3WWujKAVcEvqtcOOz+uMWNtqzBptEupk\nE1aaRnQfTx488YB8Ubul06LIY0PNCHgGCWPgy817tOVT7JA+V0P0FFonl/PXE0dr\nWgtxRJmvGaNiFzYq+kQmdKMfayZTm3kdVgP0W52t5wp878K0i4s2KPg5lANvjTz7\nDCT+VV2IGIE52o4RpMUGyQ==","translations":{}}],"screenshots":[{"url":"https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-document.png"},{"url":"https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-app.png"},{"url":"https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-presentation.png"},{"url":"https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-spreadsheet.png"}],"translations":{"en":{"name":"Collabora Online","summary":"Edit office documents directly in your browser.","description":"Collabora Online allows you to to work with all kinds of office documents directly in your browser. This application requires Collabora Cloudsuite to be installed on one of your servers, please read the documentation to learn more about that."}},"isFeatured":false,"authors":[{"name":"Collabora Productivity based on work of Frank Karlitschek, Victor Dubiniuk","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIIDCDCCAfACAhAZMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYxMDMxMDg1NDExWhcNMjcwMjA2MDg1NDExWjAYMRYwFAYD\nVQQDEw1yaWNoZG9jdW1lbnRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA1jk29m6JykcJ2Ld0YEpjPMYh6kwxY6GysNJnfkA/th7tPWL3+vBJ9oTYyVnZ\njwAE1Cqwfa9MyBKMZ2IdfIqtT8PeWzuFP7Ib942EdxUpwwh9F3lykeGsj0h4zQwX\nF9OooiS99PfLX+JpkKm15Ujb00iLB6xQmq0+3NeOT1CTD1ziJ1ueOcxBKMwaFp2a\nPuz3F5ywqCvpmxG/OBuOs0LI3/zStXhBNbUMxBrWblr7zaVNJXl/I2JCKj8Wah/H\nXUEEGbW15fAUP1f+90eQSxpEoCZDBHXOQCTJYzySGv+BjU+qlI9/gS0QbrsiyzUf\nO5lyvi8LvUZBzpBw+yg1U75rqQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA9jU3m\nZmD0ywO3MUsG/GLigFtcWi/p7zp2BliR+NpuY2qNFYDcsIb8ZUudmUc/cJRRctzy\nAPaLLj/d+h5RFaxjTVvim1PSe6M7urK/IMSvyUVYCeQRYpG8ZJixKTCOVIBaWHMz\nxTfc51tm9EPlpJpK6JtaWrYYoWGE3k9sINdJ4JkvKkE2CBAqVhX6ZGyEQ0bnEhtk\nRu1DXn+LW7TJ4NZ8VtLWvmW/6Kfmi7dQ1V++Kmn0lO5ntRt5altePbStCHC8bhGp\nmyBOrjhrJgLIwvgH26MYZhdiSkFzoE38nMPZdrUmUDxcPCwucWJqgzDPudguFthj\nWCVZ3TTG/2z3+tWM\n-----END CERTIFICATE-----"},{"id":"ocr","categories":["files","tools"],"userDocs":"https://janis91.github.io/ocr/","adminDocs":"https://github.com/janis91/ocr/wiki","developerDocs":"https://github.com/janis91/ocr/wiki","issueTracker":"https://github.com/janis91/ocr/issues","website":"https://janis91.github.io/ocr/","created":"2016-09-19T12:07:49.220376Z","lastModified":"2016-11-21T11:22:21.024501Z","releases":[{"version":"1.0.0","phpExtensions":[],"databases":[{"id":"pgsql","versionSpec":"*","rawVersionSpec":"*"},{"id":"mysql","versionSpec":"*","rawVersionSpec":"*"},{"id":"sqlite","versionSpec":"*","rawVersionSpec":"*"}],"shellCommands":["ocrmypdf","tesseract"],"phpVersionSpec":">=5.6.0 <8.0.0","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/janis91/ocr/releases/download/v1.0.0/ocr.tar.gz","created":"2016-10-24T06:50:43.283900Z","licenses":["agpl"],"lastModified":"2016-11-21T11:22:21.269108Z","isNightly":false,"rawPhpVersionSpec":">=5.6 <=7","rawPlatformVersionSpec":">=10 <=11","signature":"CBJkCIiUKyf2NuWfz2zJ3grhf8p7wJes7DPV/OxUzhlxIH0Fh7K54+U5A9JOOi6f\nWPhjXG1ylkyIVY1glr/B8svWNsD4jAclpnUi1/9ZW5UPT8LnRBfTbtF9Uoj0OgNs\ntsGQYbpuREoHnjbJWTRe0kq1OsOfX44xuf8PuX43B+lpQPW4iRSSz3ZIhdPcDGq1\n7pyqQM7gdKhBQ6/tOiwd7Enyt5Hi4V6jhwhUOCYeTNiLD2V3yKL+qA9DzpXUfNNw\nLGTjcaMrifibHQIZBZWbPPMmCfMJZ7GO9oR4gWHwkhWqt0yVWAJXAHJBLd5vXC5I\njtRTXRpHO/k6Dtqem8tZCVoDE5MAC7fDZ/0XzoFiXHciP6MenVasVcXo6xJOJc5y\nGsrecNftUEhP/ngxA6lMBVkLmmdpiexVisvsavPi64i34OUA6qOuxjgNVBDwg56i\n2lOEVvHa3nn0UX7ZZoQ/Nu6Mz7J3Hx/VDlttPuWe42eeJAphyDGubT1M62gW8dVB\nD3tJOF7spnK6I3BhVLviou/zs30AIRVBDTU0Orzx78cbInwy6/vyJib2a1olAaHz\nv05SzlQRnBWM4jYBe0mA/2ds9AO6VrXGrT/iLlHemj6JYoGBM185TGewA7OJyX3a\nHSlSDqaremmi+aS3onx3AKhXykDxTRkMVarePwTzzFs=","translations":{}},{"version":"0.8.8","phpExtensions":[],"databases":[{"id":"pgsql","versionSpec":"*","rawVersionSpec":"*"},{"id":"mysql","versionSpec":"*","rawVersionSpec":"*"},{"id":"sqlite","versionSpec":"*","rawVersionSpec":"*"}],"shellCommands":["ocrmypdf","tesseract"],"phpVersionSpec":">=5.6.0 <8.0.0","platformVersionSpec":">=10.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/janis91/ocr/releases/download/v0.8.8-beta/ocr-0.8.8-beta.tar.gz","created":"2016-10-10T18:01:16.076330Z","licenses":["agpl"],"lastModified":"2016-10-10T18:01:16.169733Z","isNightly":false,"rawPhpVersionSpec":">=5.6 <=7","rawPlatformVersionSpec":">=10 <=11","signature":"uEvhHfQCrzb6z+QuOoO8rYXiMsZFkrFWEqDvTyOTSgFKvo7dVoj3EfDfaApgcKEB\nIM/SqjLSO0lNhrp8F2mST3twbvLDprKsfrDWKFE6eiH0yKl2aNB+cHWE27utARaX\n/QZBD114vbWeDnbaBa4b9OwtBXDqKJrnO1LmqSLFP8guAlVTkU1jSPkRTpmwAcAZ\nJA/aiN/D2LSGfiNp/YdeTuzU+gPINIs9dCb6+PPkyam8PCBaXUSSaW+c0lAQHln+\ntb3EXxZ5YXdjPWrpEyHvFLk1N8s/w615QoMxr5fEs1M8D29aGbcL/wu7LXH4X0Yn\noiWwIFbpfrpJQlrIFumxWZR74JXiNr9J7ijnQ7SjxdHCcrLxMdnZ2cwq4iD6PnYm\nnIojhlhPOqUIzsWYCYutLWDQbUQz9eyWbj/+7XL+5KjetUUr/MnCu6xJ27IqFbAX\nHc8KRCO+9I0/qMQ2/rCZXBMeo39MGhhkgkVl5YAKwC1IEN/jlfyVNXZwYlfcHLKj\n6aNQ4zN6gGOerWWZ8qXtOeNJN+qv0nmXUKrZdnZUAhxOdB4G9Ym+JujxJZ9yNIWV\nsiqAI9J+OIzCwv/dzZhaHadONoo/RTF+Fl6Hy56HyBtMehb8w9p8ksVediqf33yt\nFAE/tzKtNK5NiRd+8MZkq/GbocaFUv3C7Y6pLMpTE1c=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc1.png"},{"url":"https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc2.png"},{"url":"https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc3.png"}],"translations":{"en":{"name":"OCR","summary":"Character recoginition for your images and pdf files.","description":"# Description\n\nNextcloud OCR (optical character recoginition) processing for images and PDF with tesseract-ocr and OCRmyPDF brings OCR capability to your Nextcloud 10.\nThe app uses tesseract-ocr, OCRmyPDF and a php internal message queueing service in order to process images (png, jpeg, tiff) and PDF (currently not all PDF-types are supported, for more information see [here](https://github.com/jbarlow83/OCRmyPDF)) asynchronously and save the output file to the same folder in nextcloud, so you are able to search in it.\nThe source data won't get lost. Instead:\n - in case of a PDF a copy will be saved with an extra layer of the processed text, so that you are able to search in it.\n - in case of a image the result of the OCR processing will be saved in a .txt file next to the image (same folder).\n\n**One big feature is the asynchronous ocr processing brought by the internal php message queueing system (Semaphore functions), which supports workers to handle tasks asynchronous from the rest of nextcloud.**\n\n## Prerequisites, Requirements and Dependencies\nThe OCR app has some prerequisites:\n - **[Nextcloud 10](https://nextcloud.com/)** or higher\n - **Linux** server as environment. (tested with Debian 8, Raspbian and Ubuntu 14.04 (Trusty))\n - **[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF)** >v2.x (tested with v4.1.3 (v4 is recommended))\n - **[tesseract-ocr](https://github.com/tesseract-ocr/tesseract)** >v3.02.02 with corresponding language files (e.g. tesseract-ocr-eng)\n\nFor further information see the homepage or the appropriate documentation."},"de":{"name":"OCR","summary":"Schrifterkennung für Bilder (mit Text) und PDF Dateien.","description":"# Beschreibung\n\nOCR (Automatische Texterkennung) für Bilder (mit Text) und PDF Dateien mithilfe von tesseract-ocr und OCRmyPDF ermöglicht Ihnen automatische Schrifterkennung direkt in Ihrer Nextcloud 10.\nDie App nutzt Tesseract-ocr, OCRmyPDF und den internen Message Queueing Service von PHP, um so asynchron (im Hintegrund) Bilder (PNG, JPEG, TIFF) und PDFs (aktuell werden nicht alle Typen unterstützt, näheres [hier](https://github.com/jbarlow83/OCRmyPDF)) zu verarbeiten. Das Ergebnis, welches jetzt durchsuchbar, kopierbar und ähnliches ist, wird anschließend im selben Ordner gespeichert, wie die Ursprungsdatei.\nDie Ursuprungsdatei geht dabei nicht verloren:\n - im Falle einer PDF wird eine Kopie mit einer zusätzlichen Textebene gespeichert, damit sie durchsuchbar und kopierbar wird.\n - im Falle eines Bildes wird das Resultat in einer txt-Datei gespeichert.\n\n**Ein großer Vorteil ist, dass das Ausführen und Verarbeiten asynchron im Hintergrund stattfindet. Dies geschieht mithilfe der PHP internernen Unterstützung einer Message Queue (Semaphore Funktionen). Die Aufgaben werden somit getrennt von der Nextcloud in einem eigenen Arbeits-Prozess (Worker) abgearbeitet.**\n\n## Anforderungen und Abhängigkeiten\nFür die OCR App müssen folgende Anforderungen erfüllt sein:\n - **[Nextcloud 10](https://nextcloud.com/)** oder höher\n - **Linux** server als Betriebssystem. (getestet mit Debian 8, Raspbian und Ubuntu 14.04 (Trusty))\n - **[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF)** >v2.x (getestet mit v4.1.3 (v4 empfohlen))\n - **[tesseract-ocr](https://github.com/tesseract-ocr/tesseract)** >v3.02.02 mit den dazugehörigen Übersetzungs- und Sprachdateien (z. B. tesseract-ocr-deu)\n\nFür weiter Informationen besuchen Sie die Homepage oder lesen Sie die zutreffende Dokumentation."}},"isFeatured":false,"authors":[{"name":"Janis Koehr","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIID/jCCAuYCAhAKMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYwOTE5MTEzNTAxWhcNMjYxMjI2MTEzNTAxWjAOMQwwCgYD\nVQQDDANvY3IwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDpXiwec4f\nXAT//7YBPSb4z6ZsBJSMfBq0VTL/HagjJnQ7BL+WagzWlS69IStNDqlIlenamYRX\n4B40heJIUinzYKjTRbr5UAw6MX29HibZccm/qgrk36o1XTgIsoRhmvSxbXDVIo1k\nbDOJN8gc2Gvswa8X+uOe9pfcDgAdqGxOvFnoKW89GnB01pCNT+xakNErGAFqVLsr\n2AeademAZnbxJ1cB54tQn2Bygb/7DKKY8EmFfIq6/27n9Jbph1FG9HIlWRT4/M2H\nU2pG3cCScWMEBPsW7kpfpnzLk7Q30Oj6k/rEYjJgmNYgg6oVnn0D9uRmhBYBnGyx\nMab1ilsK53lyuzQY0pmU8V5ULqpnNFAK6DVFfofEamDUhBPO+TZXEA5cZmuULRpf\nQQXmGpUQSyV6pS9WirMIqXFp9wmQ4vtjMdhu/6CP7cmtYZdq9uOhWEHbQM0mZUkb\n8hMjeItPx9XITI7Cge1JUOI8ZIwiB3USnQXcMd3v82l++/VgqHB7s5OaKPhygsWI\nM6RCoBcGiuQB5/fEUOg5ACOpGVyJiBda0Mi57AdoxdJmfnr7Bxcf2tAWIJL9Y7T3\nE1+V2BMxJOWwvVz26Cq83F41yXK2hJS+SbfQTqNUR8Cfh50CS9POvgRxNrJK9yvI\nkKle3ITRtGVM1XU0njWjnsdGg3D3O2mmjQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQAbFddMbgfPI1szT57V1FKZrOrdYqQ7qjewlIQOzshGydbMtqS/9XL5hYocJCMt\nY6w+C/i6iEzO2Jx8D/k4rcZMXoVR6y3ZvO0Ke0gzSRsU+5eYj2FK1VV+cNIQW5Iu\nCYYIVa7pVPVHdeQH2Bba680bLV0HMF6b1fI9IwkfdCAinvCYZLjyEXZlmB7YjyA8\nHR7qPCNz4uG2Va7mlUHE3UYUYnlv8JFOV3YdbVL0nxhWwIdzSri5sxFIhdlabpzY\nyA1z/MCBEyTRo80jxFmL+MpwbsdbUJi7Qxlnd56zb6HHDGrLHXZTh9LXgyVbnhWL\nkxomWjIXQh4aMHQL4QF7U4EK\n-----END CERTIFICATE-----"},{"id":"spreedme","categories":["tools"],"userDocs":"https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md","adminDocs":"https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md","developerDocs":"","issueTracker":"https://github.com/strukturag/nextcloud-spreedme/issues","website":"","created":"2016-09-27T08:43:07.835196Z","lastModified":"2016-11-21T16:51:23.703819Z","releases":[{"version":"0.3.4","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://apps.owncloud.com/CONTENT/content-files/174436-spreedme.tar.gz","created":"2016-11-21T16:51:23.689599Z","licenses":["agpl"],"lastModified":"2016-11-21T16:51:23.826509Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"Mhy3hXeGWlIujx1Op39MMRdqHYOo360BCwr4FPWoTNNggH3aS0gWlh48DAfGYK9W\netNiOqIuRyA0NrVlsqR2vDILgFtODJSbKPyHd3PQn3hcGsjogjQ+dkKciLNLinw7\nOhbv6aDdRFLBeRHpX/7wOnWL5W3ko/gyn0Awvi88M9+nC5aARtqncQqPy2SxDGzH\nKlOZHSNDnEQCGMhA8hNWWKdVwNUJHod/wmBWpW5QVNSJq5DqrKZcNwpGM2UUJoql\nEqUMwDXk5uVH5r5k62Tr9kguDWoUEG1OqQSyeMY24AmA64tq/HSlAdZ+CX32bc4E\nZvm+n8poJBrdSVmWEaa4ZfYaLFdOc6Kcuid1B1Sv9kPhD9WD6T1sicdzjDzcorBK\n/MLReCuSb2E8aPTnFWRoAZ4xCUGs1IXzX5fmxI8VdzwR42R6RhGJ/rqMuZRFenZF\nbOks45K5gE1da4QpkYOUQa3GVMNPqPiT3CqjmJ8tjxq7bGpb6v+YoCLACjjPpPZL\n2Y28qLxwHVaINDFUUxD75WWdrlulRbqHwiSw8jolP9qrpXhDuLAqYam9tRwV5K5R\n8uNawnFwWkicBEYkN/WtBTouWzehOPn38tHXov6SyEyD6lkuxUBZrsGQ2ru+t33U\nk0kKCbV0GFw43I+3Ji5DiB4TUVNZYVoPG1B7Qve+UfA=","translations":{}},{"version":"0.3.3","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <11.0.0","minIntSize":32,"download":"https://apps.owncloud.com/CONTENT/content-files/174436-spreedme.tar.gz","created":"2016-10-20T09:09:26.520692Z","licenses":["agpl"],"lastModified":"2016-10-20T09:09:26.666738Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=10","signature":"D62Ck7JUnrYbrfFlX7xXVaYUMZIh2acmykIKapqfemD/tuX5Bvb08GYGTeFG61MA\nQzsqcIylDfGnC1UJbf8yWEX7PbyJD5w/R4WlbFv34njDvM8rBs4HpzSjkqQoykOF\nZpYAjH2ydfKqtZadgoIRm7et5B8X2AeoGg11ec52DId5t1wAEBcDIv824CDBUt8t\n0pVY8Z8n1MUYwU7DCjCbPy23br2+EBODFCHp+cFfMBLg3F0BJI5nY3Q8ku+0tqMR\n0NDxQcscNZ2Ck/wpVDWylfhpS+ICIxSMiyq7urP593mRrK3399SUyaMqDfgl/pxo\nqTzdBxHLaAqcnKZYglbqp+Gxbyj4teqCod8TiSMlp90VaxhC72ACuVQQRWQKuTNI\nZeW3YweWB5d7VErqBNmQR1tGnX5YFFHiKo41fVDQFsrOqHx4zP6AeU3nkl2ol/r/\n3pg553so1MOxMkyLEhGYGMfrdQqVEtajNWHUdj3B73LS+M3jcjBFIdOD+AGXPtDX\njCRymt07c1znhkL+aT8yY5SHMVbKBZj9mExL49hcLjAYYc4U++60uq9MFH5r9g4T\ndph+yT6VVEM/UH2HjvKsHv2wm937sNgG3EXQdh79JU8nCXIz7cVrJ8f5/9r6n1VP\nBbjtfDAPEjmfVCXX2gmgLuZHV+GMhLBS9bTh+61AhhE=","translations":{}},{"version":"0.3.2","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <11.0.0","minIntSize":32,"download":"https://apps.owncloud.com/CONTENT/content-files/174436-spreedme.tar.gz","created":"2016-10-06T08:14:05.212553Z","licenses":["agpl"],"lastModified":"2016-10-06T08:14:05.278533Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=10","signature":"X9zXDyMBPoXPMpZ+XXWK3MLufjY2MG8lJ+93LiW3rv0iq9yd8PafK4IvP9czO6o9\nf/vNilq+1mfl6mjTvL6QF5+sySlzbRGbT3uTBwYXyYL07HVYgl1ZLrwe0kxvxqaW\nxTvPem7+HzwClI3VnWc7ylQfzGrcYIaSIg7nNq1GOHocsgZVNyj/nVW/eQx24MjZ\nuLzZs9SJqYoBGq+mo63vRswhqv5OzGebo+G6dHm0hvRSOw9qsWCDWBugiSRU8zU4\nD9PQ0e8WbyrIrQhBoUvvkuijO3zCySx606S1HUyaHmbJzMc4Fbpwz6ggmi6IRBbT\nFBKB1DWJDwN/7mY4fhS4KhircVnAHDqiBVCTu5i3pSfMPrwFymcmbn9OBuFHAFuZ\nd9PZvWQg4v32w+Q/NNvZVW5lRi+l0w5DEqNREaj79hljk2reZMaB65lvtV9NCYw+\nHmwWqsGqZ1SgGZuhYkOzEIkCfJ2fF/VpyavJ4X6bHP9yYdkt1pxnSSoZ2HC8mkG4\nBnPf28mEXRcY8EJv0rEePqKSBIhAt8yfEW+joH/8nupe1qOdfPvP08ifLad5m76s\nt23UzlSljzq9kVO+d16z2uagKomN9USZaNnJcUDVblfjvCPpdiHLfRPEJnRsDZCm\nNffFWEMcz+TWmwBboZgTRO9v0bPDEuwfCCEW0zy8rT0=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/conference.gif"},{"url":"https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/presentation.png"},{"url":"https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/import.png"},{"url":"https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/users.png"}],"translations":{"en":{"name":"Spreed.ME","summary":"Audio-, video- and text chat for your Nextcloud","description":"Securely communicate with your friends and family using rich audio-, video- and text chat, and much more right from your Nextcloud – in your browser"}},"isFeatured":false,"authors":[{"name":"struktur AG","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\r\nMIIEAzCCAusCAhANMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\r\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\r\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\r\ndXRob3JpdHkwHhcNMTYwOTI2MTYxNzMzWhcNMjcwMTAyMTYxNzMzWjATMREwDwYD\r\nVQQDEwhzcHJlZWRtZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKLx\r\n2dCPBLIgX948BnOdLij0YyI2+FKD6uZOvzxMaoi3rlxNf8MJgraNMzTBWEXtxT5b\r\n7ZISNp89WEXhaQ1dwwCocodd/xow4Ek63m5nUvTZXsm+YSbMgrFbxzsBhYU7KuIE\r\nT/jhKdzYgemzErwwN/gtwkLMfPo3jkgg6c8NPPohYv6k7V4VnsqtJ0JS0kX19FqM\r\nMiNz9XkcncBHy9x0BSxy4+YnwbFcgIx/MtYKlBL8NkPuuJaB/6C1O+IPYhdEdnpX\r\n+RaIue71nSStOYOqT4YDqHAIw7EmqgA1my09mmK+0Pn92GJVEAEN7JGBSQ+F32RI\r\ndB3ivGAOVtUtVvJlepWdbHxj1xqeP+LCjWzHMLQjm0TyH8VqU4Cg/wxwAEFnBATH\r\naOaWwrggzY2d9KBo1mp0k71NArLbBdlHykFU4bgiSDWrXXMz0fZzLQVwGI0Eqcxc\r\nouf6t0kvrK8oKjrnso+FjBoT7lHV/H6ny4ufxIEDAJ/FEBV/gMizt5fDZ+DvmMw4\r\nq+a088/lXoiI/vWPoGfOa77H5BQOt3y70Pmwv2uVYp46dtU8oat+ZvyW9iMmgP1h\r\nJSEHj1WGGGlp45d10l4OghwfTB0OSuPUYwWR+lZnV8sukGvQzC9iRV1DGl/rREMC\r\ncQ5ajRAtO5NPnThvN5/Zuh4n8JoDc0GK4jEZsIivAgMBAAEwDQYJKoZIhvcNAQEL\r\nBQADggEBAGHMRbPV0WTI9r1w6m2iJRrMbZtbBb+mQr8NtOoXQwvSXWT1lXMP2N8u\r\nLQ1a8U5UaUjeg7TnoUWTEOqU05HpwA8GZtdWZqPPQpe691kMNvfqF64g0le2kzOL\r\nhuMP9kpDGzSD8pEKf1ihxvEWNUBmwewrZTC3+b4gM+MJ3BBCfb5SCzMURLirfFST\r\naxCNzc7veb2M98hS73w5ZE6vO+C/wz0GTsxuK0AoLitApT5naQnjvxSvSsjFPEGD\r\nsUNUEU2Decyp0jxLVnrrpz6Y5UupfBR0V8yAv1t5Od/mCKLc5DxHsDWiKOpsob9U\r\nJN+bdzJil2NNftihD4Dm7Ha7OS3O8W0=\r\n-----END CERTIFICATE-----"},{"id":"nextant","categories":["files","tools"],"userDocs":"","adminDocs":"https://github.com/nextcloud/nextant/wiki","developerDocs":"","issueTracker":"https://github.com/nextcloud/nextant/issues","website":"https://github.com/nextcloud/nextant/wiki","created":"2016-09-14T14:34:35.977699Z","lastModified":"2016-11-22T16:02:57.758477Z","releases":[{"version":"0.6.6","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.6/nextant-0.6.6.tar.gz","created":"2016-11-16T15:11:14.344704Z","licenses":["agpl"],"lastModified":"2016-11-16T20:39:59.030384Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"aOZeEeThyZ0V/vXBcn6c+Z0vyCsZcN6nfSJ8oWEea4zXh4g705Si+MFZESqix3M2\nOPCnA/U8eASwdRTAEwQJrW5ECmu1THXSIsrzQzc9kFycvyOGzCgAWtuu0ayzZD2/\nU5aDWlzpLHC1Czg9QJ5UnfZR0AfChWQ402N1YzGqMShdJv6AHXFrVE+uYnIyxuYI\noPJQBUYbQwthVUjpYwFwSxw50YU17gmx5RZ0Y0OPz3i/EiuEUrxopXtfDVYAuCML\npDw37LOTRQ2JqxSU3teALh8LcrwJbTeOP0n4bTeV+vU3jvtiaEoRrwfVrK41F701\nQymGXy1/EFG0kxPGS2dRNPBAXYLZfeoWlROl3D5BWlbsCcXKU1S+22yn0TEdS7x1\nY44x8jRKnBddDE7qkn+QoQYHNNcxOREsFFLmIoyCUpdNOdDX2PvTFUYkIqdnXaJy\noAKv2GkvWPQ0aiiBtA1i4oXuzvHW/M2wOrK7v7DCpNfILrD/sjxpljxcX082nRCd\n9P3iPd2hQ6yOM9fG21LVN74b6wggI81BzFf/xJPd4ZqYLjfeG/yqd0zaiMOzMm1W\nse+kc/a4iB3BoCNX3E942pBBzew4ya8LkCXdCHUUsuelDf1va1ikTh/G7D84ll9/\n2avNqQnUh3hgOnxFCLI/5VrbqxfSTVdO6O/LTuAmwgw=","translations":{}},{"version":"0.6.5","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.5/nextant-0.6.5.tar.gz","created":"2016-11-09T16:58:06.856332Z","licenses":["agpl"],"lastModified":"2016-11-09T16:58:07.139404Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"DVOIbLEVggiLkNkuPW+pXqu8WYT15unUsMoqHSw81NiU6HF0Nuf3XiwnHJJDDqo5\nyX+QyHADO4ZiQpvXhGigvwD2eS1jVLatAztyxE0tEQv5eBU/7R0jQYxI8YCnC/jE\nZDa0qs+TI58EkDek0LBzueVQqrLubKgGU9P+E9H8gbWi1JHvl/2LdY7CplPqaJ+J\nMRokobPntzgx9m4DZC1RsCoXzSON7o2gp2cmunPJiXVHPhMUfIfolGEbNGJ1/xdp\nra7Y7XkPnDx4po98a38UpUh1x/2m5goOV54em35bWbh4ShNDykiE5ttz6tOywlYN\ngxceSsStTKyqscVaOV2Xu6Ive0pY9CInvrSfRnRktIWBYDyWdbK9sJuqs/s69kqn\nKQ/SjzE2smw0zwOUMnSaz0Jzr1vdPFgNu2xDYAVQO5G03V+wQ5AhxuhBz5Xp5Fll\nLaOhymZLCC7lq0DUqkeTeQ2QCfiG23hvG2VUPsIqW7xFe2YzKHZVXi9JuH//Gwym\nDmJmcyZDMvNwNiObx3ZRKsZNH2XwbldFZ9nTpb9AafyoSR/qbwd473NewaDLRTiY\nLrOP5Wx1xx6DOkRmDF2l2iT1bSJ6eoAoWQU2I0aYRH9NAr2Ugd4f2Um4o61EJaL+\nRHT9cERRySEloU/NdgmZEOi+CG0rEu+9LC5G/jGlHE8=","translations":{}},{"version":"0.6.4","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.4/nextant-0.6.4.tar.gz","created":"2016-11-05T18:17:47.622023Z","licenses":["agpl"],"lastModified":"2016-11-05T18:17:47.678445Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"RdkvnhNjw+kAnT6Or3+N9FzAv9DjJ9BAlmgANMwZZcaqo1gZRFewsGD2Rx1KEb9X\numSC28tl2B5/3r/+dprVJmOnYJny/7+kDmI/d+vigKfnaQJOUZ0ya5+f72pFgow7\nth8fw9rX/3+zIBs2IeEN66cis8ioFq97BJDsnDMBDr7wl7CnFJjYe6eviWuiFTnC\n4sBXlYjHoaIRRu561nwAooV+WXmWsparYPVhj2cXdyP/CnWo5HSF5jA51WCsz7Up\n7a0URXdh85xmxEbZtnjUymTW2BIegdxj9Erbfiuy/m3ApgnP+DiEQRcM13J7pXqg\n4cgFOBSzKDZgim599WBW2uNO1ztnDYOzz47GXDfJhcdvKiZZoQpUF9W4LMRtheMz\nxD9YArO3j3d+VOInSYU2Rpvhugwo1LExhwnRdT4+cOZfEeq0VojiM7yBZLDdEjIb\nGdYUJtNWSU0F6cPab2Au8FgZymLrHL9EpGvxuA1oAwtRxHAgMElJG2O6Jp89gGl9\nh/AptypeTvzNEc9/Kg24ueBKqmSUd5a45pZ3gM2pNATJhUK7fzLb/K6cq/kEzZtj\nOPra1ZfP0/20u8VP32Rgw1cFmIjqt8DFzUmOMpMfyoJkdldtTwQtGk+yIvtN1zp6\nT2zDknAKA2N/rZ/0SJl8KxVVNLhQWtYBJ+rFAdapuxI=","translations":{}},{"version":"0.6.3","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.3/nextant-0.6.3.tar.gz","created":"2016-11-03T21:51:27.654342Z","licenses":["agpl"],"lastModified":"2016-11-04T18:25:35.697130Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"Hf5GB4xd+rVXzWvVpGbbF5tqmnI+DGHlNHdLYPImFLS/Z4K5wKeNp80E5sd/RkAi\nhyuEsdWHlGDVMT6s7oeCmH/ClyWqLNSz9VH4KYqyFgD4+usHZp9PrEeEKbvPDPKv\nD3eB7Ats34cWcpf4E1oR5wsPicgmdgIgb2uMXzc/1G9xUBRWzocwCstzjEEAB/VJ\nvJuHvhDTGG294P4gOb82MxKCQ8LZ4i1QXzOf/mLETOubiUbZtJgTReYvpdAo2Wct\nbdfDFw13LYZkCf71r9jIQ3PSPlIpD+0BwAlE1fi0Br9dP2JjIfiKN6CGVaki6O0v\nKR42jtcE9xXiAop0Ym1FYMWJvQUy5PFLMwYDfEd6CvfEFJl+fi+RjXNDNZcTyw00\nHa48sACoGzxwts2JknWMU57mwvi0Z4uwpE0cFp/PRzBsXmSlCzWHjJhu7+2qambE\nAabdP9nH2NvqJHUJyPsxtDSrSWCBY4CoL3wYu36UrIA4NepyudMPNe9fhKTEU0bg\n8DLclw6hYdj5p9Zj3OUuwOZLz6r85KwnooTsUjOYkBXvdUuHWkgONqqZlPMApS4i\nChRQ7ysHAinPyyzsvr0PR9g6J52CSCO/7qwSJy6yqSzuSWrbZUa4FVTcKIwWJJPu\nJ2XzB4rWVG1wWjzMM6MIvFbO2HY9upzh651OdOwsYvk=","translations":{}},{"version":"0.6.2","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.2/nextant-0.6.2.tar.gz","created":"2016-11-01T11:24:58.054892Z","licenses":["agpl"],"lastModified":"2016-11-01T11:24:58.151609Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"h8KgwMh2RGGIp7q/II23VSfE5Ibkha7p/C1kvIfG6QIIc2Zu/Mm3Oekynnysh5ZJ\nZuMTaeWbejbBAmlnxW+AwBWa/s2PoMhv7foVvdtg76l9/qr+9jGvUM7D1LbbBvAy\n/XW6JSrhhBZHOohdU7kwV5Xnwsn/NC/zUe0G4eZ+9fc9uSntkob9wnWvUs2daAeD\nY6Mi7Xt/XAIX65OIVTKfC6ah1SlbXgtJ2i2j4G32I9jRhmbkpt/UJEangn90fHnY\nQXNJ85OyV0aNMafNHoMSL3uLscdvjp0Hy8w4iBeavsRlCs0GiUoG1+YdwTwmC9EM\n4CjbMfRJ0DYK7u697TOmS8MQzk8O7f5THtjeokZlrom2YnV9t6gLvjnxl/+gXPdJ\nmgLixnA8P6tPsff9pquHKQZsvxjv6vVa2DVJc8VpcqJRih7yj/3V7rxesEP7MUnP\nznIOcwzTsKXHuAnvujpCwyQj3QtpQK2XJPQ5WkKgwbTdvriVJfnzPironhcHo1vC\nbuUDOdhL59aySChw2cZGD9lCWaxYR7I1BDDzWKNl9Qg0AZ2auyGUGTv8P2vi5qUB\n0CmnkzkZurR5ju6Nb9/2lcZvda7QJdIdMtm2Wnn+Ak/Z3Y4IehB5PPDP5/MMAhQY\nXE8Jqe0mGtiU/O2346L5IThkS58JlRmep4kYv+II9zE=","translations":{}},{"version":"0.6.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.1/nextant-0.6.1.tar.gz","created":"2016-10-27T21:16:47.066097Z","licenses":["agpl"],"lastModified":"2016-10-27T21:16:47.125641Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"axSX4Kz2P4PbKU676DibjPZsxk8hCIG3lLOmeMXoiBhp3ka4wJ8u5tNwTzgY8/MV\n3mjXe5DNttD66SqmiRNSPKbotYHVFFW3wFK+Dgh/++n/KTomGYUeIwt88Z9ecbuG\nNT6U46jFrfZBYzRHWzbgiJ4c7MCoid9cfmoB7HDuQSyw+E0S2vbLL8+jzMcIzF+5\nTh/sJEterXCyrWSciw/9x98F+4svNbskdEIvrOox3+K6UIhpsojqJR2+bQhG3rsM\nPerOb6J+bzHdLV1ZL/4nWEz1F30T7B08QxY/4pHD68JFQcdtzmHMIhXfCoRvWhN2\nVRnizx3IXBALY4F49Ql6bjsnr6BCp+LroM5RSQ3eupDcqejDJLbjPz8xfOMOwlx7\nz84Xd0MUyqaEkxg1ihsWLbPlYACUZ2aoDkSQUIbfZTTiov7eqTM8UBc/UqVID/LU\nyEW4gjFZzQy6kX76XRAGq1vebKFjCU63asCnVyJhF/YQVTu1hPGbFslkRKnYuh8W\ne4MeaNfbdjcSEX+7oTcPJz6V09pOPvukXL0M1m7lS9PhTisI6oGj8c33GPYp/DSK\n6SGk+ukbt1mwFuFKdTvAMxo1lk96D+pKUv4MX/ralaaoIAmwPTGsSQ04RyL454ae\nU6q8PApwrVyPHYwMBPtXGoQMyb2ZV9rylazYbKCQ8I0=","translations":{}},{"version":"0.6.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/v0.6.0/nextant-0.6.0.tar.gz","created":"2016-10-26T01:46:48.419025Z","licenses":["agpl"],"lastModified":"2016-10-26T01:46:48.521063Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"W2TmbX/NbbfPPjIJLalO0kCDhhQF1pEohH/CxO9bY+yR+a5NKiTbpAhG2McqpTSj\nmgC4J8/thmwGlWGC+VW+KlpXzzjc7wCgMGMKViOpGD3pIy8p8U5MXTqVgjjrEb9g\nKgr9uErXzxJ5oDfkx8Uh1bUeBJTsAAivGJqMlhBYFGxw8BSB09sfOZytNxo4wbwZ\nNAcYP1qvkUQ8CVR0nInSTRfLvAp5+e/8xxyYZwJIWrdNgpoP0CxiwzKP/VSjBk/U\nsPU4R72UQnVZB0InRCeh/KNTwu1YiPkUKm+mNmt2hCfN7Fm6bY2rUMH7oa8KTkqn\nh52FhbaYRSAR1OPdC5RnHiQWlPh71gq+4Xqgp19Eawnl/QiVteVifSjNQZ+Ban8M\nRyw/PxHnzIWg/OAjx81Jx9mXjUIHSeUxMTJTKTp+lEqIAzjku0iHeU5UPGCfE+VM\nwmopZfnlfk2nLkUwjQXLCIcnZD1ME1m0h/H4Ad0Q/qXpBScUp47npkxg2b8zPIhk\n3aXUmNPLgwyXPWvAuaBK/WwpNefUnqWFns8t2Alpzg/EpC2PrZqciCNDcRFEycoa\nX+JsFyD7eYA7Dc9iIf09gXZX+tZ+Jinb+iPDwYrl1u/7IIBoBlUGCgo+cF7/dL9S\nc3vYeWw6MCH8Sv+ckgS2g726BfdN5EjB/8cb071b4lE=","translations":{}},{"version":"0.5.1","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/daita/nextant/releases/download/0.5.1/nextant-0.5.1.tar.gz","created":"2016-10-17T15:23:04.515057Z","licenses":["agpl"],"lastModified":"2016-10-17T15:23:04.576640Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"Pp3rC/9RmAYURneGpGit4HZ2t1qH9A9nwsUnGDgRuJ0akIii7CtJC+n8l1b9k73/\nhxMYnp2JOBu2HWKgFp9a3yeo1xphI5hOUdVQ1UZAWxIQyDI1FZVYDr81l7GCdkqm\n2lMxgviFADSYURCgEnAsj9Nt0NZ6LDcJZiJg3iPAjkDI0U+KnEBjtx/XRFqGUnfp\nCUZ/XLHh/hvoEitSHepTBDCMKkTNjbDEwBYfA2rAoz4zbMR5zKLU+1r1DIUgWSRe\nbk2i8TaTDVL4cbb6MhkGKwkujb+Atikvkpi45o7+fyQMs84c6fjjh/TZJaC+dMyG\n1GCikMPwwtEPjXtnLzynERAxJOd5mP4Ee4sD8ZrnzNUehpyFR88pwWU6r+dmiebb\nnsYlGkhIu2aIO2O4HZ4sUTsO5sfjZ9me7jsafhgJl6iG4IBeHa/L1MsSbhsh6mvH\nYsz4Xsluwr0QcFLmSDDQQYynyogKfJZG2xQsadM0GETWgXE44dVeRvMUsILfB4uZ\nmfKgd23SgaOhYC8m4bg5Hxnkl+xHJnyGZ6jhqg7bhuKwsoLymc18Vmjmb7a45HGV\nXbL5CTmw9yaPOBS3v7v91TnlB+0lhlzbKzZ0xIhY55qsTC76uScbTLwndPqNGaF7\n2koxRbJ3jcmf/Z2MLymdRi2BTZbZkPkxgVrSf9plaR0=","translations":{}},{"version":"0.5.0","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/daita/nextant/releases/download/0.5.0/nextant-0.5.0.tar.gz","created":"2016-10-11T11:47:46.191539Z","licenses":["agpl"],"lastModified":"2016-10-11T11:55:40.393000Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"qzQJSLK8nkrQedwwxUdhxL8bq3aXyppAlWJo+n3GqSvqA8uNTzl3Wsci0LsnBV0E\nDvDNW8K0enhl563F/bywQrDhgKl8jTX/CA5KCxqO9P+tvE80zAfMqiRnQayVcWuY\nSWX6RqfI/kqiWyN1SsFp2EDlas6eb+xfIoiJamlfsN0qzHYOFt5W77wmw2Bn9dB5\n9nwHHyC0z60Pf2pPduc/KuZ/971WrDFaIapL7Gm+z9XoaKSwUT575VtS+RNJkOKy\niBrwnHdc8x/62HPFOXsUYjCt2aEmLzPCQN8Ke5pd3596hm5wbAVzTHuxf2H35tb3\nljfGqAZ5AJX2Xd13d4aHXFdSEILwv6IFq2fx0hO3vkvFEoKF5oQ2t3hi++Mw/h8R\n15OKZCOC1bFH3gTwdshmnHSTSr3LxDeTF60aH16wpXcehQuJHagpb/aG8mPD1w+v\n759/mekqa4LYlqT9TLWTqX3UPeQXYIwcPijG84IvW1BDK1M4Mj2Vqsld4jXwG6CP\nORIL8xoQbA52dQI1Y19JXcU9zxIb6GaHYgpV0wejatsZRfhPv2Yd0CBShF4HY7aw\nnfcm88pqzOKNvjnLZjTFQwuJ0AUUSOsWhgZzYt8tATJ2KDZ+lxz+WAMGXJAC/ciY\ntrrglY7YxwJNLbYp+arE0sowZx+IOVaSZBvmUGHiEBY=","translations":{}},{"version":"0.4.2","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/daita/nextant/releases/download/0.4.2/nextant-0.4.2.tar.gz","created":"2016-10-06T10:31:12.482416Z","licenses":["agpl"],"lastModified":"2016-10-06T10:31:12.551117Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"QHJhYcBMi5HyPofshZ7EdcXsOfRrl7o48Y3BBBq8CkmYtFDbekghdJqjFbwwZa5u\n8FtgMVwssql/RSDNP6M2Zc/MpQ3K9gDm+DyxE5KRmtMEpzHB+oD+1DxP7kAoyW8/\nnBr5iiQSdLCelddMcTwbdxskLFUXIs3cFuLGhMvr8pdQOAgfxte5lolrj4/8EsJ0\n0yUImgIYG4NlgmvCygGApdpVaOcK7XVtv4oH+x43JmX9YZ3Ce0DQPYPUbFTCf4ZS\nS075j1vcnPx2cRFGxc+YpKzYVVYoy7ZdB75Hq+kaei/atxrTyV5+gcCrVPnH1RkX\n6G8rgu5l8FoGJYopi8/9dd8LsGLFx53CHMdEVob3tFR0WeK4TJAGJa403zE6S3hM\nxr86WCedmjuti0uOhSQr5AEVAoXZ/JUWQMMsPAUMuKEYVjKfmve6TlcNMC2oM5XT\nXcOf4OP3pcQq4ViN53o4Pj6NGSci6IzD6xLeAxKZUoTX37ArVKH6RHS5Najc193H\nRhYRnfE7D5YOr1u10HaZCFCVJif2MgMP0/uH2759NoRjXFowrh7Z6dW7JQG5lbHN\ne0jjJH1Y8m8H1peGGcmM0YxFiOVZ0ER7P+AxT4Cbau/cVhaS8vnPF2a2a6YFRiFS\nVH4aqazvvXrtifDr3lfvlyPCrP/570nwvOJgZGk+K/Y=","translations":{}},{"version":"0.10.0-alpha","phpExtensions":[],"databases":[],"shellCommands":[],"phpVersionSpec":"*","platformVersionSpec":">=9.0.0 <12.0.0","minIntSize":32,"download":"https://github.com/nextcloud/nextant/releases/download/0.10.0/nextant-master-0.10.0.tar.gz","created":"2016-11-22T16:02:57.740378Z","licenses":["agpl"],"lastModified":"2016-11-22T16:02:57.900805Z","isNightly":false,"rawPhpVersionSpec":"*","rawPlatformVersionSpec":">=9 <=11","signature":"kDO3xbPpoUdl2qo362tXfJIqKeiKE12M8FkMXbdKiRNzuQyvDOehQq3dErALZDOr\nLG47Tpez/Kn9Fsx1y4dQDx0w9SD3pyoqvjj1O6KkTYN6srpitQcj9EzEItCY+MZd\ngRSc7Px9MzxpFpgwtuThGVlSt5kLMd0vjWFLVcv7k07rZfWEsTwXd24INIDtlr1A\nC7hyLB777nEOGa7oAzx8nr+FJcIbmu9opVZk8AL40FOFDNBgfAy2AS9hGZreUmRV\ndV9Zh7UAR+RsEeO51xcU/CKqC8Jb0jL1gkYyUaQy16oe1HF9bRs1PWuNL5mYwsmy\nZNn0ay/a7mb7hxJbza1F3lbgBtodvbgUm7+So/QvaR29CSAqqoUXeQy6FfSBVWhZ\nYlTxdQfKcBcUPFO14BBk/O5p5iQaG8JCLJ/EZGDPDIVDMn7crGQ4oLZJv80eqTeB\n7ueDmWaD3gQ9j2XKsMk1uLSyY4unt9AaTofBylsKD1SjLhyxofGZym4jc2+M1GnQ\nyLcoEMSexnKuextu4nzrA0y5k3z9tvO07R29lwT1GFp6oGAakMLob/6XrGsm3YQY\nRQXgfXKFw8XmXBumqG8ayEIyvQ/O8nO6r4R1H8a7ooj6oWa3PhPsen+gLE0SpJPZ\nz3e2TLliwC4VtZp69H7u3px5Qn1Fc6RMDTh571edCr8=","translations":{}}],"screenshots":[{"url":"https://raw.githubusercontent.com/nextcloud/nextant/master/screenshots/displayResult.jpg"},{"url":"https://raw.githubusercontent.com/nextcloud/nextant/master/screenshots/admin.jpg"}],"translations":{"en":{"name":"Nextant","summary":"Navigate through your cloud using Solr","description":"\n\t Navigate through your cloud using Solr\n\n\n**Nextant** performs fast and concise _Full-Text Search_ within:\n\n- your own files,\n- shared files,\n- external storage,\n- bookmarks\n\n\n### Recognized file format:\n- plain text,\n- rtf,\n- pdf,\n- html,\n- openoffice,\n- microsoft office,\n- image JPEG and TIFF (will requiert Tesseract installed)\n- pdf with no text layer (will also requiert Tesseract) _[work in progress]_\n\n\n\n## Installation\n\n- [You first need to install a Solr servlet](https://github.com/nextcloud/nextant/wiki)\n- Download the .zip from the appstore, unzip and place this app in **nextcloud/apps/** (or clone the github and build the app yourself)\n- Enable the app in the app list,\n- Edit the settings in the administration page.\n- Extract the current files from your cloud using the **./occ nextant:index** commands\n- Have a look to this [explanation on how Nextant works](https://github.com/nextcloud/nextant/wiki/Extracting,-Live-Update)\n- _(Optional)_ [Installing Tesseract](https://github.com/tesseract-ocr/tesseract/wiki) ([Optical Character Recognition](https://en.wikipedia.org/wiki/Optical_character_recognition) (OCR) Engine) will allow Nextant to extract text from images and pdfs without text layer.\n\n\n\t"}},"isFeatured":false,"authors":[{"name":"Maxence Lange","mail":"","homepage":""}],"ratingRecent":0.5,"ratingOverall":0.5,"ratingNumRecent":0,"ratingNumOverall":0,"certificate":"-----BEGIN CERTIFICATE-----\nMIIEAjCCAuoCAhAFMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD\nVQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI\nMTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB\ndXRob3JpdHkwHhcNMTYwOTE0MTI1NDQwWhcNMjYxMjIxMTI1NDQwWjASMRAwDgYD\nVQQDDAduZXh0YW50MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsbnQ\n+9acjKHfcrUj4yqBpD++GmQ5z2Sp8C8uOz4ZbLyM9bUXEYHo4a4u3CdC49kGUkb3\np5MkEAEzslTWDi1eh5MZgPWpbPgItsDsXY1o55O3jtxNkzSG5/yYcPQcuKtIOm9S\n7DY0K+UQt3nK+RrXEZfARMNrzFbEzpE3b7w901Yl5n+m/B8rhW4pqg8uSfx3u04J\nwduV1fHwoHUB0Ox5HyWib4Pq1XppNh7xdc2Fg93JxshwuCPJyOOzrFTnxC7s1yzQ\nUvaqkjPW5QeQRunQjZ2XtpYH8f8v01W18bNEiHwqtFwuDEyCVx1rvEMgUDVXdPkP\ngZrlB5TzGmz0U3HzYvf6205WuzfHxz7kPj502wP51PoZBKpniggKzmuXkx6BpsZC\nZX45VpDHdiATLwRj1t2bMs3C01nzpIWO5ZwFtkepH3Y+mvwX5lDh/XDsqJC2Yo8o\nWMmniWNW7dspufYOsBUqqYGP7rkailgVT4oYk6D1j6oFZ5SSpfPF5lsyYedDSM6y\nbIGVkSF+sjLK6R9BenBijKceAKsS//WwRYCBPC+JHlsYpXKW12bL+C47Kj2/N6d4\nrYryzV6ofVSF6pwIq0oEjoyfBfNpYavf3xrRkSSmIIlPSnMY7DT1xkGD5retxSm6\n+WIfkWKRZpv2S6PhMHGLspYc4H5Dj8c48rG5Co8CAwEAATANBgkqhkiG9w0BAQsF\nAAOCAQEAOZUwyPaUi+1BOUgQJMWqYRoTVZUyBshTXSC7jSwa97b7qADV9ooA6TYF\nzgsPcE41k7jRkUbnjcY45RGtL3vqsgZbx5TjPa5fGMxlqJ6eYBOY61Q6VIHEVm3u\nxnPEO9dsMoDBijvo5D7KtE+Ccs907Rq70kCsbrdgPHkyb5tDSnCKogN1LiQrg1EP\nmy7Z1C7jG9/h57vx0+QBMDCYnTmqLsvMKqo27uHskzAiB7VXLEdSZ2FtMGHkLUQO\n0bfhnvTZ2VhMmK83t7ovo71An4ycmsolGD/MA0vNI78VrVISrdI8rRh2WntUnCBU\nEJL3BaQAQaASSsvFrcozYxrQG4VzEg==\n-----END CERTIFICATE-----"}] -EOD; - - public static array $expectedResponse = [ - 'data' - => [ - 0 - => [ - 'id' => 'direct_menu', - 'categories' - => [ - 0 => 'customization', - ], - 'userDocs' => '', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/juliushaertl/direct_menu/issues', - 'website' => '', - 'created' => '2016-10-01T09:16:06.030994Z', - 'lastModified' => '2016-10-06T14:01:05.584192Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.9.2', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/juliushaertl/direct_menu/releases/download/v0.9.2/direct_menu.tar.gz', - 'created' => '2016-10-06T14:01:05.578297Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-10-06T14:01:05.643813Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'ERBS9G5bZ3vwCizz2Ht5DehsVJmb63bzF3aYcH7xjbDVMPagOFWdUAiLDwTeZR1n -i4gdZ73J/IjHQQJoOPwtCjgbZgLPFqL5x13CLUO9mb/33dZe/+gqEDc/3AuJ4TlA -XUdLxHRb1bwIlJOwuSr/E24452VG20WUhLXBoM0Zm7WcMxvJWo2zAWnuqnLX3dy9 -cPB4PX+6JU2lUMINj8OYQmM1QnqvjG8YV0cYHbBbnSicOGwXEnni7mojsC8T0cn7 -YEJ2O2iO9hh3fvFEXUzDcL7tDQ5bZqm63Oa991bsAJxFo/RbzeJRh//DcOrd8Ufn -u2SqRhwybS8j4YvfjAL9RPdRfPLwf6X2gx/Y6QFrKHH0QMI/9J/ZFyoUQcqKbsHV -85O+yuWoqVmza71tkp4n9PuMdprCinaVvHbHbNGUf2SIh9BWuEQuVvvnvB+ZW8XY -+Cl+unzk3WgOgT0iY3uEmsQcrLIo4DSKhcNgD1NS13fR/JTSavvmOqBarUMFZfVC -bkR1DTBCyDjdpNBidpa3/26675dz5IT5Zedp4BBBREQzX08cIhJx5mgqDdX3CU09 -uWtzoaLi71/1BWTFAN+Y9VyfZ8/Z3Pg3vKedRJ565mztIj0geL3riEsC5YnPS0+C -+a3B9sDiiOa101EORzX3lrPqL7reEPdCxrIwN+hKFBQ=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://bitgrid.net/~jus/direct_menu_nc.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Direct Menu', - 'summary' => 'Provide easy access to all apps in the header.', - 'description' => 'Provide easy access to all apps in the header.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Julius Härtl', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEBjCCAu4CAhADMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTE0MTI1MDU0WhcNMjYxMjIxMTI1MDU0WjAWMRQwEgYD -VQQDDAtkaXJlY3RfbWVudTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -AMkzWsAkKP/40ktvJMpnr0IJNVoPOR0hvh24igcDskL1WKiD2eiRUenj5LE0Nvn+ -siGmWsAqRVpdiz+Y8ghQqQMzKi43IrRN0AxlCrHWrSqBZT3wIAUcFz4RzEoFxc1N -UZzWma6ljukGnvt4V1ZyT+H/cjqxUkBhh/y9SS0jUen1a1grND6Rw54X46V2dlCu -FH+pLsfPJJGw+QLeTGHn7dqdv18cYMAlWDCzPixVnNiCXHZcUtKMmstU2xU4R2e6 -zimp2rgkE4TNHrafpjH8xGdNi2FG5Dmokob/L5Q2r8jyNaW7UsFfrvLTRj371b3/ -2FhhxoGUvDzaG2An02Exwm52LJfdTVMHAMPZub5poHfy5vAEdZGPQ/m02l8ZK/Y2 -7yT807GlfPMXfdfjCxR6wNtmv7rvBDdrUZmIRNJfpFSdvlH/+MOTWnabyfQv2K4Q -BIwltX6Elh0lh4ntvt1ZVtvFv+PL1Dc7QLV+w19+/LJA0mnsh7GIFYKFlbA65gA0 -c/w+uqDy0+5MxkR9WGPpd79KRA1tKWTis4Ny1lApK5y3zIsVGa3DfBHXcwqkWHbV -wIpyuyyDsFtC1b9LTFONX7iU9cbNk5C5GTM331MdA2kLcD/D5k42GNTBSca7MkPx -Fx/ETSn0Ct167el30symf2AxvXjw+mBYPN71rVTMDwe9AgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAC0fJKnbEhXA8M283jA9GxABxLyTBcQyVVNnz2L/bYYNi81Y9iZv -+U0S3qaIfoqNcV9FTKAutbsKvWyolnI7MRRK6feNuFfoP2jKubM1CnawpyT/RF2Q -e/zxnB1EmeI2X5D2xceJDLB7Fy5W0EGrLixRIdFaSUommWFUm9E2hSIaNlziSBdc -1J/mOQeNYO5zg5ouEt1rzQW4Mhh1I2uNQmGe4ip+Jl/2LAv3FZuu4NrSEcoXH3ro -G2dF9Gtu4GiQ5fuaJknaxlgXHovfqeZwZJX9o4M+Ug81AqiY7XjdiaCPdh0Tthcx -2OmWZo7UBREWenjKyFZZ/iKoqH5sdenBtpo= ------END CERTIFICATE-----', - ], - 1 - => [ - 'id' => 'apporder', - 'categories' - => [ - 0 => 'customization', - ], - 'userDocs' => '', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/juliushaertl/apporder/issues', - 'website' => '', - 'created' => '2016-10-01T09:16:47.111889Z', - 'lastModified' => '2016-10-12T19:50:16.038821Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.3.3', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/juliushaertl/apporder/releases/download/0.3.3/apporder.tar.gz', - 'created' => '2016-10-12T19:14:10.802359Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-10-12T19:50:16.104357Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'nhlT9lhrmBxIsqh/e3RLm2NDw/U8ZvvoMyYQTLMM3H19DQmVcPYPYC9QWVTsowUzXblVaOXVGylbpKma9yOlOAqJtF3qyXecLl4+tA/Awb6BBhKPgHFdcLDAL5yy1K7/uei3SgEojMlJoI9vEK5I1C5YTh43yNH+//Im6MAuNYFUTlMXK426zdOe6ogpCI5GlYdXopqYANxcpT+WXWET6DDSM5Ev+MYwMcSAY4r8+tvARRU4ZAenRgjkBT6R5z6cD76emRax14tbV6vkjjwRcO+dQtM0tFPbd+5fsNInbauv50VzIMgjA6RnKTOI17gRsSdGlsV4vZn2aIxEPWauu6T/UohMvAE9HMn13vtbpPBSFwJAktj6yHASYGzupNQLprA0+OdyALoLZPpQAKNEXA42a4EVISBKu0EmduHJlUPeqhnIGkkGgVNWS8AWqzP2nFoPdXBgUWateiMcBTHxgEKDac5YmNc9lsXpzf1OxBaXHBhGYKuXPwIfyH3jTWb1OdwixJEuRe9dl63T9qOTRre8QWns/bMqKLibGfMtFhVB21ARJayBuX70eVvabG/2N7Y5t1zUlFygIKu51tvo3AVCRDdRrFWDvkAjxzIz5FIdALVZ+DReFYu/r4WF/w3V9rInFuEDSwb/OH4r8sQycs07tSlMyA74Y3FpjKTBxso=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://bitgrid.net/~jus/apporder-nc.gif', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'AppOrder', - 'summary' => 'Sort apps in the menu with drag and drop', - 'description' => ' -Enable sorting for icons inside the app menu. The order will be saved for each -user individually. Administrators can define a custom default order. -AppOrder works with the default owncloud menu as well as with the direct_menu -app. - -## Set a default order for all new users - -Go to the Admin settings > Additional settings and drag the icons under App order. - -## Use first app as default app - -You can easily let Nextcloud redirect your user to the first app in their -personal order by changing the following parameter in your config/config.php: - -\'defaultapp\' => \'apporder\', - -Users will now get redirected to the first app of the default order or to the -first app of the user order. - ', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Julius Härtl', - 'mail' => 'jus@bitgrid.net', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAzCCAusCAhAEMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTE0MTI1MjQ4WhcNMjYxMjIxMTI1MjQ4WjATMREwDwYD -VQQDDAhhcHBvcmRlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKVK -Kn5jivCu+eRfe5BECjDOzNaGHlpiegb49Hf4nh0W7DqcoLHip5c1O2BcEYdH6rkw -20WclvjoQpgavG5aFXzXzur6eKTT5TpgY5oZTLoWjbx4e+fKdhyDPTpqNZzs1pxz -sZLDL/ElpbSErE0s+QK/pzP11WNPylAkI9AKSyDMO3Mbllg8I8Bt+bT7LJKYOO/T -Lhv9m0anLZ4HrdfimhVIoMiu3RpyRigk8titXZA94+F8Fbf7ZQ9f14Y/v3+rfJFQ -ii9cDoox5uUrjplH2LrMr5BodfCfydLu4uVpPWYkdccgvcZ1sugjvNXyCQgdzQDK -pOpiwVkkiQFaQJYbGSDblFWPl/cLtA/P/qS7s8tWyTQuc1rYlEpCHG/fG8ZFkSVK -9eCMGxK908VB4IU2DHZHOHi7JvtOz8X/Ak6pIIFdARoW3rfKlrz6DD4T9jEgYq0n -Re7YwCKEIU3liZJ+qG6LCa+rMlp/7sCzAmqBhaaaJyX4nnZCa2Q2cNZpItEAdwVc -qxLYL1FiNFMSeeYhzJJoq5iMC3vp2LScUJJNoXZj9zv+uqTNGHr+bimchR2rHUBo -PzDLFJmat03KdWcMYxcK5mxJNGHpgyqM7gySlbppY/cgAospE8/ygU2FlFWIC9N0 -eDaY+T8QA1msnzsfMhYuOI8CRYigan1agGOMDgGxAgMBAAEwDQYJKoZIhvcNAQEL -BQADggEBAGsECd+meXHg1rr8Wb6qrkDz/uxkY1J+pa5WxnkVcB6QrF3+HDtLMvYm -TTS02ffLLyATNTOALZFSy4fh4At4SrNzl8dUaapgqk1T8f+y1FhfpZrEBsarrq+2 -CSKtBro2jcnxzI3BvHdQcx4RAGo8sUzaqKBmsy+JmAqpCSk8f1zHR94x4Akp7n44 -8Ha7u1GcHMPzSeScRMGJX/x06B45cLVGHH5GF2Bu/8JaCSEAsgETCMkc/XFMYrRd -Tu+WGOL2Ee5U4k4XFdzeSLODWby08iU+Gx3bXTR6WIvXCYeIVsCPKK/luvfGkiSR -CpW1GUIA1cyulT4uyHf9g6BMdYVOsFQ= ------END CERTIFICATE-----', - ], - 2 - => [ - 'id' => 'twofactor_totp', - 'categories' - => [ - 0 => 'tools', - ], - 'userDocs' => '', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => '', - 'website' => '', - 'created' => '2016-10-08T14:13:54.356716Z', - 'lastModified' => '2016-10-12T14:38:56.186269Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.4.1', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '>=5.4.0 <7.1.0', - 'platformVersionSpec' => '>=10.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/ChristophWurst/twofactor_totp/releases/download/0.4.1/twofactor_totp.tar.gz', - 'created' => '2016-10-12T14:38:56.174612Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-10-12T14:38:56.248223Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '>=5.4 <=7.0', - 'rawPlatformVersionSpec' => '>=10 <=11', - 'signature' => 'bnwWxmHEn8xkoWbtwhC1kIrJ0dQfAI3PUtU62k+Tru/BHt1G2aVxqO8bCdghojZ7 -zdFMlIJw4kekYFsVfLk8jzjUTZKVbNVKCdkHrVTQ0bUUryMAMLqGQ3PSRI5NX6D5 -FpkvwO1coYwU0XVWF8KAS0meX0ztSkT3Mv96LLrxr8F8SrB/MGmKIE4WTjt1fAIa -ZLAVEUo/3sNFTGLYBtL3wjctrkZvJltP8abeRfls9FkRHu+rN7R3uLFzk42uZn3X -Wpt5BBmlYm5ORbnJ2ApsxEkMNK+rOy8GIePaz5277ozTNrOnO04id1FXnS9mIsKD -20nRzjekZH+nneQYoCTfnEFg2QXpW+a+zINbqCD5hivEU8utdpDAHFpNjIJdjXcS -8MiCA/yvtwRnfqJ5Fy9BxJ6Gt05/GPUqT8DS7P1I1N+qxhsvFEdxhrm2yIOhif8o -h7ro5ls+d3OQ8i3i4vdZm821Ytxdu/DQBHiVoOBarvFWwWAv2zd2VAvpTmk6J5yv -3y+csRqpEJYd9fcVMPsTu7WBRRrpBsAqdAHJcZEwak2kz1kdOgSf8FIzP1z6Q71d -Ml2RKcPeutMHHSLiGIN/h7fM5aSs49wGgGZmfz28fHVd7/U0HFSMYmkT/GMq5tMP -Iyc+QZAN4qbX8G0k/QSTkK/L4lOT2hQiQqiSqmWItMk=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Two Factor TOTP Provider', - 'summary' => 'A Two-Factor-Auth Provider for TOTP (e.g. Google Authenticator)', - 'description' => 'A Two-Factor-Auth Provider for TOTP (e.g. Google Authenticator)', - ], - ], - 'isFeatured' => true, - 'authors' - => [ - 0 - => [ - 'name' => 'Christoph Wurst', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIECTCCAvECAhASMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDEyMDkzNDMxWhcNMjcwMTE4MDkzNDMxWjAZMRcwFQYD -VQQDDA50d29mYWN0b3JfdG90cDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBALC1K94104L/nOtmTygx7QNjUcnHs3yrn71mw4pMxTlonXOnMTpwxsfL1Hhu -/5GMSgupTbQPlevSl6J86UMs455/sPShd6ifmAuhb8VFaAsjpizjs0RMaUg1sjmF -uV18PD9FXLourx51V/c4MG5kpavlV+bLUrVMAjbsJY2+k30tCC/XkP5u8jUWmM/T -5REChn7/obPgaeddhuJoILYhKEW3VNrR8Fm9SYiviB3FLhM7URDZ97IBnXYqbvbT -Znvq+E74Zc7HgYwQwrjU/AqQAInhNpAR4ZM6CkWWWWaL96O1q3lCfKJNaxqC0Kg/ -kGn/pxYkl9062jtMUz60s9OPDyuisfyl68UyM68Ozyz4SMRLmDVbewOqQAwmAbtz -8p9AQrX3Pr9tXhARR4pDSsQz1z+8ExEd6EKbhMyiTtHtZQ1Vm9qfoR52snpznb5N -e4TcT2qHAkOWV9+a9ESXmQz2bNjgThxEl5edTVY9m4t248lK5aBTGq5ZKGULNHSQ -GGpr/ftMFpII45tSvadexUvzcR/BHt3QwBAlPmA4rWtjmOMuJGDGk+mKw4pUgtT8 -KvUMPQpnrbXSjKctxb3V5Ppg0UGntlSG71aVdxY1raLvKSmYeoMxUTnNeS6UYAF6 -I3FiuPnrjVFsZa2gwZfG8NmUPVPdv1O/IvLbToXvyieo8MbZAgMBAAEwDQYJKoZI -hvcNAQELBQADggEBAEb6ajdng0bnNRuqL/GbmDC2hyy3exqPoZB/P5u0nZZzDZ18 -LFgiWr8DOYvS+9i6kdwWscMwNJsLEUQ2rdrAi+fGr6dlazn3sCCXrskLURKn5qCU -fIFZbr2bGjSg93JGnvNorfsdJkwpFW2Z9gOwMwa9tAzSkR9CsSdOeYrmdtBdodAR -dIu2MkhxAZk9FZfnFkjTaAXcBHafJce7H/IEjHDEoIkFp5KnAQLHsJb4n8JeXmi9 -VMgQ6yUWNuzOQMZpMIV7RMOUZHvxiX/ZWUFzXNYX0GYub6p4O2uh3LJE+xXyDf77 -RBO7PLY3m4TXCeKesxZlkoGke+lnq7B8tkADdPI= ------END CERTIFICATE-----', - ], - 3 - => [ - 'id' => 'contacts', - 'categories' - => [ - 0 => 'office', - 1 => 'organization', - 2 => 'social', - ], - 'userDocs' => 'https://docs.nextcloud.com/server/11/user_manual/pim/contacts.html', - 'adminDocs' => 'https://docs.nextcloud.com/server/11/admin_manual/configuration_server/occ_command.html?highlight=occ%20commands#dav-label', - 'developerDocs' => 'https://github.com/nextcloud/contacts#building-the-app', - 'issueTracker' => 'https://github.com/nextcloud/contacts/issues', - 'website' => 'https://github.com/nextcloud/contacts#readme', - 'created' => '2016-10-30T14:00:58.922766Z', - 'lastModified' => '2016-11-22T22:08:01.904319Z', - 'releases' - => [ - 0 - => [ - 'version' => '1.5.0', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/nextcloud/contacts/releases/download/v1.5.0/contacts.tar.gz', - 'created' => '2016-11-22T22:08:01.861942Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-22T22:08:02.306939Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'ZqqhqtbHcNB+rzGCQ7FDIjjvHjit+dhAE1UhFgiXApkx3tmPP4nJOBAGNjHe+2Ao -VcTIX2SrWEfieRrA4Gp+0k7pUPWag1Z0T1OVOwO4cmS1AVFyGIOE1bRvDhMfsWTU -4CI4oXaKBFAY6mtnf7VJ7EeIdNqhImkohyWDQ88NiPRLM1XNkJJk6AvZBcT0fvCv -o145X4dLpbixSXsN99QFNJ/oXvK+9tBGwTd5i/WnNFY90vcNRLia8aRo7SA0YJRx -Lnxnj2HMqwTTDQEKE+1elYKWsqQ2DeqwScP97UIKe5bZXnrwOi9kH9PDmR4abtzd -lHL8E1Wgw25ePDeHG7APrx0tVOJy1bP+g8vcarpGynWZoizDkBvYZD+xtxizpBXC -JsDOSzczApptY6dnOtv0Vat8oh/Z/F99gBUahEu4WZ16ZgR1nj40PDK1Snl18Cgk -Me1EZcde8SLEpTbCWYIfIw/O9Fkp5cWD/dAqoiO6g+gNxSZ/gGp57qoGfFxn7d/x -H3aH8GljatAFjrwItw1JzR0THt0ukkOK+bw/pfCslk10sjHMitmz/GXa4qMS91DZ -BKLUd0dSfQUQzkfwcojImbzJRvca4/DYe3mfG7+RCH0tDL6t72dKL9joB++u5R1u -VZPgkToexlXcKWpiDB8H2/SEShKr4udAOjR5de9CYWM=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/screenshots/master/apps/Contacts/contacts.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Contacts', - 'summary' => 'The new and improved app for your Contacts.', - 'description' => 'The new and improved app for your Contacts.', - ], - ], - 'isFeatured' => true, - 'authors' - => [ - 0 - => [ - 'name' => 'Alexander Weidinger', - 'mail' => '', - 'homepage' => '', - ], - 1 - => [ - 'name' => 'Jan-Christoph Borchardt', - 'mail' => '', - 'homepage' => '', - ], - 2 - => [ - 'name' => 'Hendrik Leppelsack', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAzCCAusCAhATMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDEyMjAzNzIyWhcNMjcwMTE4MjAzNzIyWjATMREwDwYD -VQQDDAhjb250YWN0czCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANzx -/zJF+5/s4lOJLWIlfKQgTy+UpvIpiUXCgrsHsDZTx+hjQAhIWukH88a+7NVAL7Ys -kQNC0Tlm755FJi/T6EdR7edOwIRdo2ZwakOWLZXd209+6cCd2UloHL0bgnbWepTl -R/4YgbLg/G+FVKCfkEiYc3PuDZ3EVrcwQFcg7h74X9ne6CHH0Z1WQLydxJuVEb2n -X9I+nIRpPHcVostkSiUmzHR7C5TjTIo2PzzgnCU6GC0iBa6z6dDYfz24QEp/b8UA -ZaLhjkyKghVGMnoF/s9KPgH4NM8pvvtadQN8pxlOPju4wbmKPUrsXo4ujurDXbbc -YkzNt8ojobGwdTXoyDogAsGZLQd2FQksWpRvY+I3zVPokBfPMdUPLllG5VcV0VA5 -DRK+h2ms+XmspdBvGonjF+XdbFm9hEmDoFmoi9aU6C6AdofjmG/e9+pw/20dXUWk -mMorWwXQ5yLmIn5LnpRXrOuK7CS28VRhBYuVNMlsyKhzU0rophbsD9OFXxYLjr6s -7UPNwZ5h+kjXZDBKD89QctBSViT8RhLe8nulRIm0iJn1sb9hca/CF63KmsFzENfK -QeM6MO0H34PB84iNyz5AX1OIy+1wHD4Wrzt9O/i2LkWK6tBhL69aZiBqdLXWKffj -ARDCxxIfews51EZFyHzwsw65I97y46aBKxY382q7AgMBAAEwDQYJKoZIhvcNAQEL -BQADggEBACLypX0spxAVAwQIS9dlC9bh1X/XdW2nAvSju2taUTBzbp074SnW6niI -bnY4ihYs4yOuGvzXxnp/OlvWH7qhOIchJUq/XPcEFMa7P03XjVpcNnD3k0zQWlZb -tGonX9EUOeLZKdqI4fkrCkMLScfjgJzoHGYQrm8vlIg0IVuRLCKd5+x4bS7KagbG -iuPit2pjkw3nWz0JRHneRXz/BNoAWBnJiV7JMF2xwBAHN4ghTM8NSJzrGTurmpMI -Gld7yCP47xNPaAZEC66odcClvNtJ2Clgp8739jD6uJJCqcKDejeef0VU1PG7AXId -52bVrGMxJwOuL1393vKxGH0PHDzcB1M= ------END CERTIFICATE-----', - ], - 4 - => [ - 'id' => 'mail', - 'categories' - => [ - 0 => 'tools', - ], - 'userDocs' => '', - 'adminDocs' => 'https://github.com/nextcloud/mail#readme', - 'developerDocs' => '', - 'issueTracker' => '', - 'website' => '', - 'created' => '2016-10-19T19:41:41.710285Z', - 'lastModified' => '2016-10-19T19:57:33.689238Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.6.0', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '>=5.4.0 <7.1.0', - 'platformVersionSpec' => '>=10.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/nextcloud/mail/releases/download/v0.6.0/mail.tar.gz', - 'created' => '2016-10-19T19:57:33.676730Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-10-19T19:57:33.834580Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '>=5.4 <=7.0', - 'rawPlatformVersionSpec' => '>=10 <=11', - 'signature' => 'VbMsvDpt+gSPeFM8LrZXEK10rk8kkLlgCcblgqNdCSeGZeVpwDAYv3CccVSLa0+l -lTSqQ0VIoH+OIU6vIQNBKHmSCzTplk7OrY0+L5FajXx8LnBaOh892GfGSlEt1neN -KyM0i0uOjO/xpCP/NoUlgkz6hnmYY5XEdN6DTsJtJ/XZhDQ45IYuIkMkHE/eFehS -0JnOagIz+PSipeBY2Ry+tV8YbRa7bC1JAvZzlod0dyI015AHZESeitRUY+MwMWkt -N/me7g7/Kev0wggIQQZm9aYcw63GMk/1VHUPB7Y0ESW9tx2nR5+KwTDn/Jy4DGf1 -rg8h0t5I+aPhHOBLrpczH0qaZWY2lsVZWq8KWjJI9aR9P0v2f2aXixXzD/Cuz1cK -hvhKWkOSla4D+/FxeyHGjQvdXMG8gXm0ZmTimKChCoVuCbncDd8pzkdyNoGXcvuk -sP8OrkQFooL4E7S4BWfdSiN/a8jUITJQkuXp/OVrVGeCupLWJh7qegUw6DvoqyGy -D4c6b+qYn68kx3CLaPPiz+tFAZQZQdj7+Kx/lohso8yTnVSiGYrMj4IvvCbpsQjg -WF3WSqF/K/tTnPYTWb9NUPSihTbVNv6AXOfTsPEp/ba2YSS5DjvjVjkr5vhR9eg1 -ikQ3Cw6lW3vaA4LVCC+hFkMRnI4N0bo5qQavP3PnZPc=', - 'translations' - => [ - 'en' - => [ - 'changelog' => '### Added -- Alias support - [#1523](https://github.com/owncloud/mail/pull/1523) @tahaalibra -- New incoming messages are prefetched - [#1631](https://github.com/owncloud/mail/pull/1631) @ChristophWurst -- Custom app folder support - [#1627](https://github.com/owncloud/mail/pull/1627) @juliushaertl -- Improved search - [#1609](https://github.com/owncloud/mail/pull/1609) @ChristophWurst -- Scroll to refresh - [#1595](https://github.com/owncloud/mail/pull/1593) @ChristophWurst -- Shortcuts to star and mark messages as unread - [#1590](https://github.com/owncloud/mail/pull/1590) @ChristophWurst -- Shortcuts to select previous/next messsage - [#1557](https://github.com/owncloud/mail/pull/1557) @ChristophWurst - -## Changed -- Minimum server is Nextcloud 10/ownCloud 9.1 - [#84](https://github.com/nextcloud/mail/pull/84) @ChristophWurst -- Use session storage instead of local storage for client-side cache - [#1612](https://github.com/owncloud/mail/pull/1612) @ChristophWurst -- When deleting the current message, the next one is selected immediatelly - [#1585](https://github.com/owncloud/mail/pull/1585) @ChristophWurst - -## Fixed -- Client error while composing a new message - [#1609](https://github.com/owncloud/mail/pull/1609) @ChristophWurst -- Delay app start until page has finished loading - [#1634](https://github.com/owncloud/mail/pull/1634) @ChristophWurst -- Auto-redirection of HTML mail links - [#1603](https://github.com/owncloud/mail/pull/1603) @ChristophWurst -- Update folder counters when reading/deleting messages - [#1585](https://github.com/owncloud/mail/pull/1585)', - ], - ], - ], - ], - 'screenshots' - => [ - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Mail', - 'summary' => 'Easy to use email client which connects to your mail server via IMAP and SMTP.', - 'description' => 'Easy to use email client which connects to your mail server via IMAP and SMTP.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Christoph Wurst, Thomas Müller, Jan-Christoph Borchardt, Steffen Lindner & many more …', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIID/zCCAucCAhAVMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDE5MTkzMDM0WhcNMjcwMTI1MTkzMDM0WjAPMQ0wCwYD -VQQDDARtYWlsMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp++RuliQ -lBeeiPtP0ecBn00OaU1UCpft/NVI5pnSiT9nU4l2kc5IvKjA8UxDB3gWfYTOeBFh -tUHQ2P6UKCmHZT9sApHhqLu2n0V+YhlFIViuaxndSID/M414cl56xOYQusV3Pcae -o2dOSeRRzLab3tEaVHlkBSFkGmAwPZItsmTklvV3h1sUysDicYgfXPCkf7K+JgWA -BP7vsWC8B7MDRhcB3enYv5tTcpsyvtGX7bb1oTIWVypcmKsGYfTX12VNBxKzNBIG -8pwdb8Xo0o14TytWsWN7mSHf1XbwfwYMjDWOlMqiRc+mcoKMBH41TfM/CXslSivI -syvxasEaFdlj8lmKPENdzw1OfYRs43usIf4szwyt4rb8ocXfDipnY3P2hccN6YcZ -l8y8Vsr69ASluDj2A2Pl5vH6xp6tNybZRnN5G6sghhaYaLNDU/TdMyYzz4AY33Ra -HSaMypfcXjd76Aj8jZvcwk1BH+ZsvFqNK7ZKCb7WVcMH8KRcU1sxZ4rp9vviM2fL -L7EVtznm3bSI9jjHXbiwq7RvNRRy+F6YRpAdWGwTU8uUkDabPFi41FikYyzNWauK -JhlDJXl514XjKyMVBjAZYVr5gZZkO1J7C4XzLFbC5UzYNSzW5Iwx/1j5OeYJRxh6 -5rhiUwR+COT1wdVsl6khMC8MfBR4unSd338CAwEAATANBgkqhkiG9w0BAQsFAAOC -AQEATBvpqz75PUOFPy7Tsj9bJPaKOlvBSklHH7s43fDDmQbJwswXarZi3gNdKf8D -yO/ACZvO8ANWAWL/WahkOyQtKOYzffaABGcEIP7636jzBdKtgwSGzW3fMwDghG10 -qBr2dE6ruOEdSpuZxgMgh2EulgknZUXaHAMI2HjjtAMOBScLQVjOgUqiOHmICrXy -ZETmzhx0BXDt5enJYs8R2KMYJNIme1easQRYmWKliXogNY09W7ifT9FHtVW1HX+K -xRS4JXbapjolkxyGSpP+iYSgItVnYzl6o9KZResR4yDsBv7G/8fpV4GQU9IS3zLD -PiZOosVHWJdpUKCw9V4P1prGTQ== ------END CERTIFICATE-----', - ], - 5 - => [ - 'id' => 'audioplayer', - 'categories' - => [ - 0 => 'multimedia', - ], - 'userDocs' => 'https://github.com/rello/audioplayer/wiki#user-documentation', - 'adminDocs' => 'https://github.com/rello/audioplayer/wiki#admin-documentation', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/rello/audioplayer/issues', - 'website' => 'https://github.com/rello/audioplayer', - 'created' => '2016-09-16T05:44:24.857567Z', - 'lastModified' => '2016-11-17T22:34:34.637028Z', - 'releases' - => [ - 0 - => [ - 'version' => '1.3.1', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '>=5.4.0', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/Rello/audioplayer/releases/download/1.3.1/audioplayer-1.3.1.tar.gz', - 'created' => '2016-11-17T22:34:34.215350Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-17T22:34:34.867778Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '>=5.4', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'p6Zz0IEFrxvw6y/3jHgGWWCxR6qpMzvU2HKfxcIVsK6sJnoRUhWLeAXwZ432fH2a S2llj+IGS9OvW+5VQElrXgPtEjDK1BT00DRJnp5RFCRlUv0LNoedJMzx6B6AHqPP JBufk3cG1O/CO0M0L1ITGSmSOzfKvWTRo3lxVGF792NyBaP/SyZCkH1N1TzBQzUi Ywl3+HiglPcXbHjtJm/arnKorbJWVKoaN93xFuaBapd2ozQSpi0fE0uGRsici+U7 HNa1M5WFE1rzUJoufE0E9246At07rFY1e+TdNEq8IlLgCXg5vGCKkEyuWpWno6aX LfRaIiT9x39UTAwNvuDKS0c+n4uWDYPsGfKhDx9N7CXpUrthfXVEWRzZEXG7as10 6ANvrRPJemSZH8FUSrdJhD7k12qa9R825y7mIG68Li8P71V92EOxFfo9tNXqXwBt VuDGxBqByFVPqSCj5I8hrzJzQl2Xt40g8+8ZcSF96RMg/pM+bwRMTv+mz0V+vQQ4 DWjqnWVPalaJ1PPD5/QFFErtXuNRbyxKZ6BMWxfJlLM9Kz66P75K+8fYaSwz+2KG NxY7I3svzS2K9LGH3fBLUy1t3Hl+c3zGFq/ll1MJrf9NswV4yxt2WmISfM/KDww8 yELkGs2cRzbw2tCvnmYlJJhIqRLkyFAbDzWRH/XdVx4=', - 'translations' - => [ - 'en' - => [ - 'changelog' => '2016-11-17 -- fix: one-click-play for wav not working -- fix: wrong sql statement for PostgreSQL [#90](https://github.com/rello/audioplayer/issues/90)', - ], - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://github.com/rello/screenshots/raw/master/audioplayer_main.png', - ], - 1 - => [ - 'url' => 'https://github.com/rello/screenshots/raw/master/audioplayer_lists.png', - ], - 2 - => [ - 'url' => 'https://github.com/rello/screenshots/raw/master/audioplayer_share.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Audio Player', - 'summary' => 'Audio Player for ownCloud and Nextcloud', - 'description' => 'Audio Player for MP3, MP4, Ogg, and Wave with a lot of flexibility for all your needs.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Marcel Scherello', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEBjCCAu4CAhAIMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTE1MjExMjA4WhcNMjYxMjIyMjExMjA4WjAWMRQwEgYD -VQQDDAthdWRpb3BsYXllcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -ALyC+iLscLs62NeNmUXEBmg+xMuUtDmZKr+xzJWtl6SSNRz+8K1JygvUIXFJ3RIL -CYA3xyq8/wyZH1gNrLKyz5eTeYawG+eT3ges/FT6MWGUbZoRrBrikVcLC94QzxTH -xOl8Dn+SCV/2bhcvPTQdhK+dqtvGilOtjHa40iMrk9gSdlKVys5CK/xdlEp8uiMa -kz1WENn8MVCCJV58bAUbaCupDWXR9CCoSsw8XinNsCenZ2B2XlnmbM44280w0ojs -72rfQRgj3yDG+ZUUyUOuxIuodu8liXYciLf0ph6t/f/qoSmctbBdsR5Fl1Upj1Ac -qeHb5Yf/B3Vi6Mn3XfDx0H2EHk1v9Dhzxay+v9BHUzyIX2iH/q+7TE0/Jzo5AwBW -vFKWXvG7wXaALcHYZf5v/M93IE0iCHsv2EsZKQPBnzXVGmp4DwFSP4po1B7hcog1 -gAMaellAzzvUAizgCovN6Qct3qDEANYniPlvtnlcaQGonajW4N019kFQRHLIzPFR -jab5iUMMwSnT8FhZO2ZOWuWhJven+gXjxC8mfMVgBfZnAVgydNfx9rN+KzTc88ke -obUdZ0OOeBzA7pIxGEFg9V6KTEEWZ+qH048vxXz4HI9B1I+2wQLBrZl8CvweEZ5U -5ID8XrrE/UaNZ1CvLKtCgB24gj/m1Elkh7wA3gEcEo2JAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBACtgUp+FCmjWIkQUuWSdzKWdO+IH4v9wBIrF9mo0OLIakFyDYyM5 -LlkYZXbplGXd4cfn3ruIqJNzlIb4xa5CU0bM4TMbD4oOSlLMKM/EamKPHI3bzr++ -zi7mQDFxmAE6FWSMBgKKUb4tqLc5oBap8e12tPEZl/UR6d9iUB2ltvrm3T3vrjjl -2Worm0eYBNqnagXmX5+wS11AQqeJemGqRy5e1yXRlTgB0IJhH0dCsFNwifEigutp -FNvGFVBn4r5qCiChEoq+rCXHRjPi/eCfbW21XeLFDiLxapcZyc85JIcA7znUYoFe -P7Y/ekMscwWhLbF91OaQlcWpRtEMyde/DaI= ------END CERTIFICATE-----', - ], - 6 - => [ - 'id' => 'calendar', - 'categories' - => [ - 0 => 'organization', - ], - 'userDocs' => 'https://docs.nextcloud.com/server/10/user_manual/pim/calendar.html', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/nextcloud/calendar/issues', - 'website' => 'https://github.com/nextcloud/calendar/', - 'created' => '2016-10-01T12:40:39.060903Z', - 'lastModified' => '2016-11-22T20:31:13.029921Z', - 'releases' - => [ - 0 - => [ - 'version' => '1.4.1', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/nextcloud/calendar/releases/download/v1.4.1/calendar.tar.gz', - 'created' => '2016-11-22T20:31:13.020268Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-22T20:31:13.087340Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'nThwe9CJBCan9nuDLdhfBiQyPhmum6Aa0UcYsIDdhGMw+C2acf81KhEmBJuTTWxo -WGby6WcrcJJmeuCW+ePU91ju7Pd76RirprhVXIEceIDzSCxin+K0oZCZ1IGVIJjP -IkVehTsLuCeTBbjvz1b3k5QFyhUhvd32Xt7k5d7VARyI4OqnqYYNBtH9vvgeRrFw -AxsQr4o4axof6i3iykLg6WfWarYArY4dIuu5DkPuGPWf2bbgjwWEra4sQejhOs7G -sk1xcsfYv2NpArIbpw/wnATdjiax+Gjz1URMD3NgL5ky0ecuZmNvN25QErg3nlVr -hh1FBfA5pvCJbkJ6nr5bU4bKaffwDX1nr5h77FS5zzn0Pyd7ZIExmVmNtaeJfnfV -5vnclapzXMNU+R6t/ATJQd1srvSJhyljQapzsqnrjNMEUojOEvqqygJp0KwNVPqs -3g9XGSoOnO+WULWBeISW7UVOg8BOF8pwvHIU2++bSzOdpypW0Eq6p2DPWO6qL/H1 -eFLKrUg3EvnTjvknbBxMB55h9jNJr0SAlkrmyEVm6+CE3BwRWpKB+cJMBuGiwPwv -r/ASRiJrkDThbNWAUtX70rUmCqDV6/MujLXViqOc/Q2OHvcXd1oGDccJSQT92/1z -7nonnedyYQIDqUt7u68WL8JRxp7pFsEqKLVuWSgxW3c=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/1.png', - ], - 1 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/2.png', - ], - 2 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/3.png', - ], - 3 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/calendar/master/screenshots/4.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Calendar', - 'summary' => 'Calendar GUI for Nextcloud\'s CalDAV server', - 'description' => 'The Nextcloud calendar app is a user interface for Nextcloud\'s CalDAV server. - -It integrates with other apps, allows you to manage calendars and events, display external calendars and invite attendees to your events', - ], - ], - 'isFeatured' => true, - 'authors' - => [ - 0 - => [ - 'name' => 'Georg Ehrke', - 'mail' => '', - 'homepage' => 'https://georg.coffee', - ], - 1 - => [ - 'name' => 'Raghu Nayyar', - 'mail' => '', - 'homepage' => 'http://raghunayyar.com', - ], - 2 - => [ - 'name' => 'Thomas Citharel', - 'mail' => '', - 'homepage' => 'https://tcit.fr', - ], - ], - 'ratingRecent' => 0.94444444444444398, - 'ratingOverall' => 0.94444444444444398, - 'ratingNumRecent' => 9, - 'ratingNumOverall' => 9, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAzCCAusCAhARMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDAzMTMyNjQwWhcNMjcwMTA5MTMyNjQwWjATMREwDwYD -VQQDEwhjYWxlbmRhcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMm6 -FTeqgzCXprkU83VM4/DrZWn3kqtfaR/edkC4gYT3ug7RHa/Uv1C/S++vr7pvgpnk -YzQoavl/0Qlh5sKEYX+0ud/LQDoiidwBRDckFUQ1bRfVLxAD9UAVvDRHxDqJMOx2 -gZArbeQ3ztdSHZp4ThzBFWq2FILsJD86weG7LwHjzhW6SWgLb/YTLbuuW6tRCDVV -btB0I/a0vCwj2u91Chw3u6pWWjPakc9DQrIDH4HCIBKQ4zVrYDxAmJDRFGDvVVWx -uIAeux8sd8drqSMqAhX+XMcZPRD71NQTWbCupSwWO8kgjmZnBpIiBNpzvMQzJf3A -QloZtjZ2RDXAQG88eTeT8pp8yEOCEoDLpGdraKxJrh/z2Dsk30JP3lOiNYJ9vBaB -C8NJbJ3oAlG7THwUaFF9fsdAKoTwzs5Xms04TI7W/v4Z/GClOzMymnR1T4sR72Oy -3WaMNHv/1QGffvQn2/TtZt23Ou3P083xWx2vn5FgTcac8+x85vRgWsVCA4hq9v6m -AlktB0+UWDEXpDTKD9BdFNWM8Ig9jQf7EJrvTLNnS7FIJZMB4GK8lpvPxyvACWnh -R2hQOe987Zvl3B1JZNO5RvtSeYld9Y9UfMgW1aPRweDNjSuZYAKlugx1ZoyI5HyA -QjfzAwicIMwZsCJDV/P5ZO8FE+23rdWaoJczpBqDAgMBAAEwDQYJKoZIhvcNAQEL -BQADggEBAHQXwvj8q5khWR/ilg3JGYpmMNBYHE9OeDaOcNArkKaGMd478SDPOXeu -yW7hCvNEpiTk5g0h3g3yleZFws0xH8fPsQgZANgvQXb3RCcD61NL77d0cMTr7Xzr -N3Lq/ML1YLc/WwL4uV1XvpMQMwALFL1p63BU2c0ysO31zbLOjMKAJi0hHFDYz5ZQ -D3xxtc17ll3B5IqrMnMHRqmOQ39Sbe56Y7T4agaIz/sUWpseo85D5kt7UAIOR+Mr -Q0Bl/QinETk72afGR46Qvc7tC1t9JjQQD3AUbEGuJdGvXjJJ9GREYu01XoODmPdT -jXXOI8XIOK6kxXhPHUc3iWu9b4KqGm0= ------END CERTIFICATE-----', - ], - 8 - => [ - 'id' => 'ownpad', - 'categories' - => [ - 0 => 'tools', - ], - 'userDocs' => 'https://github.com/otetard/ownpad/blob/master/README.md#mimetype-detection', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/otetard/ownpad/issues', - 'website' => '', - 'created' => '2016-09-29T15:58:52.814912Z', - 'lastModified' => '2016-11-19T17:37:52.278497Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.5.6', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/otetard/ownpad/releases/download/v0.5.6/ownpad.tar.gz', - 'created' => '2016-11-19T17:37:52.234684Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-19T17:37:52.423930Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'dh+Txg1iVfqXr8+cxplNQuBZGErSnXUo0ewGwnybNMJqp8/EjEo72+zPpW3dVnhY -67YCvhrm2bo+VRdFFymEfymzSJu9nWVFkGJhEwvTxPyIdAtuD5YAVrzmnR6L+H7m -7Q1nXE63ICPCAQpHkxIfIXLh25OhWeyofBB8AVsjDUNn58FEYJ8fFkr6dCgPriZS -sM2J+xtZMDYufy+xFMsVf/Q3WopjFuBjMC3qOecW76ZTwtREaswOC2RtpzUku2r1 -sogrfFlFer3Ii9/CWgOktnLfjB1DzbTwdEkM2xNVBRJgdMXt2VLA9FsxFFkjmr5A -l7x9cNLWA8RLpOIpIMBbaef75u5HgRBvSvq114UsA9GCu/EYbIgD8YxEt7xuKd4t -enksJB5gJ2IQNdHrPbsil59AsJ/dismDN6ktYgWQEk5dINzvm9EAvucueW0Gt+Jr -qEC5WBgJucsFxSvkHh52v43M8jgPYBfHWEL/M/+377z3+mbuIh+BcQ+vcDdiqxTF -o3n0+gw3QYIhLEe75sUhxG6ynVUdW25AKKju1kVj3KJnZTBH1R8t8/zy4DnJG8d4 -uRGqyU4BXpZjEC3nVlsC7vCncWWhxl0WZQ/MWKqsfjVAU4I88E518D6NioqMnPEJ -iCZ2x+69UCDEQyfCSKajgAYT17r3OhZei8F9KSCH8Vw=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Ownpad', - 'summary' => ' - Create and open Etherpad and Ethercalc documents. - ', - 'description' => ' - Ownpad is an ownCloud application that allows to create and open - Etherpad and Ethercalc documents. - - This application requires to have access to an instance of - Etherpad and/or Ethercalc to work properly. - ', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Olivier Tétard', - 'mail' => 'olivier.tetard@miskin.fr', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEATCCAukCAhAPMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTI5MTU1NDA3WhcNMjcwMTA1MTU1NDA3WjARMQ8wDQYD -VQQDDAZvd25wYWQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6CY7I -HRJTaqDu376vt+kruX+QOL864joScxRuh3IOVcQktCvxasuA0EtrX7TCAQrV1tBK -fkqJxU9uOV54RTgyh30yH/ZtnF2bYQwViGM06Snc0riqWydFrN5fxK52dpZWs63o -UFCNhHxrX4aUGyfXu5nQMISLm4QHoZ3LDLofk1ZsiK62fM/Jz8N2PM8qeHzf1ATo -SKcAOd3UeaS9C8bv2DuiZM7unkSO/tjrBzkMiq8ds9sIzBBsyk6BRh2HQjHPOtmO -ed+pS9mIZmc2xhssXoHL4IfZwTqwhktpsaTl7v0ROw2dwDATz/QoKMkUpboQ5lkz -wgLQhoIZw6uAZ1R/Qjze59I3iU8zIo9quDarHBotZNXboYCmg9FRfE4mHtegVaa8 -v1a1JvFQ5gvsWEsKSV6Bzb65GTp4KG4q7YnUrzh6HJyDCGLvLlWm5OWsFj6sNzXX -wLOv6JLORMbF4ZIo2iybb3x7gdfCu9JxMZ4JtOUC8KSJ6+ub15C1Aia3lN68dNts -Y6KwUF1Ted0o4OQPAulq5pUc+g6dTYmIKsavIiPKhMtl86AbUK50vRTeuGdFsT7X -av73IanPdFI9bKth+tajgvB6dxcVnvBXbrsLUyEcsxsxtBJvQcMYS4aZ6ZJYLTep -7AdK0Zt1iMdXB8+4PCps4rcG6bYB/uJeEAVm7QIDAQABMA0GCSqGSIb3DQEBCwUA -A4IBAQCM10O+sCYhIExnx01vGzKlnRS7MSQNx8ZMmbR5Elfz4AVJAEJ96ytS2DXH -2c+hcD0wAenXQEFk920AEqFQBT8DP34p0FmF83aMHW08ovzFiu4MdlhcqrLnko0h -cZTXHVyS/8JZh+o6SVm8R0/BBLF1MQQ5TqRkJehbmk6gL0+MSYxehUDKWTjJITkR -ifneTw/Ba1d0AXBOq0c0HFyGxMPIlWe4qn5LtxH5t0wyVGeSj4jyv4nvd3ZGuAgY -EUa2uYht/z475k4+vf0YhV98iQH07GnmlfD2TDZgmOCQGKlNfJh1v88OZyLLa3dz -gRHzGwKbAiJ8T8bbpZ3e2ozXxADr ------END CERTIFICATE-----', - ], - 9 - => [ - 'id' => 'announcementcenter', - 'categories' - => [ - 0 => 'organization', - ], - 'userDocs' => '', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/nextcloud/announcementcenter/issues', - 'website' => 'https://github.com/nextcloud/announcementcenter', - 'created' => '2016-09-14T10:38:53.939634Z', - 'lastModified' => '2016-11-24T11:21:50.324839Z', - 'releases' - => [ - 0 - => [ - 'version' => '2.0.0', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=10.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/nextcloud/announcementcenter/releases/download/v2.0.0/announcementcenter-2.0.0.tar.gz', - 'created' => '2016-10-06T12:41:56.195206Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-10-06T12:41:56.263124Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=10 <=11', - 'signature' => 'NVWYz73KtuoZ7ti2sluztJO5aFUc7PzhlDcg0VWyAQd1H7sk5wjw7i0bhrjw8O7M -Lsrb+PegnsL9eMlYM2WrRom+RF1PDP482xymZf1T8vh8qcTCm3TK89xSuiSm8yoA -iWUb/Uv/ODj74wVDWqWxAFKaAG/FestCB3InOOZQqQZLzlAV0U9ziYDGNzBjFqof -9rLNxJ2IOqZOA7hhMIKhSrpA0KkSfNhBsVf8CWClYnVkZQiq0LoYkHkHIlXmXUr3 -OfQFKEjtsx+bNLa6CkAaocHGHJXAofX3GQZ9cjBsjZqiTfbXfcVk0kRfz7pwL92L -I1McfJYvgMxDQG5bjRpNJw==', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://github.com/nextcloud/announcementcenter/raw/stable10/docs/AnnouncementCenterFrontpage.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Announcement Center', - 'summary' => 'An announcement center for Nextcloud', - 'description' => 'An announcement center for Nextcloud', - ], - ], - 'isFeatured' => true, - 'authors' - => [ - 0 - => [ - 'name' => 'Joas Schilling', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.75, - 'ratingOverall' => 0.75, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIDDTCCAfUCAhABMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwODIzMDkyNTQ0WhcNMjYxMTI5MDkyNTQ0WjAdMRswGQYD -VQQDDBJhbm5vdW5jZW1lbnRjZW50ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDPx4Hp1HdBo5v7bDEiVcv2UrVjNW+fPPKS/5rwbagtPcE/1v3WDcwX -vFwaXk8qCn2UpPSQ2b1rTuTDm51G1ZmEZhNiio+rBfEe9F+3tLsq9lElqIPKhkAq -EUVI6dcN+jSqvLmLhuwloEoQQSYaLrX75mY3lGqTb83h1l2Pk/brVixuVf4vJW31 -TgeieuGKnC+keLzKOrvTHffJakU8ktwB2Nuu1o+jN5a7u1bxKkP3LjEWPjq236hk -AoOcW/wi1dUEyUKUZsZQeJyvTJh1UXdLHKwYywtUu1/VLZ1IUtNyPBfiQ8ukPp3T -TnSSmG3ZnvsfM6DmAvLZ8bBQkMBzEcTLAgMBAAEwDQYJKoZIhvcNAQELBQADggEB -AAB3i2NgiZ4rpNag7cXYdaFxAxdDWnke1+LX2V2R3hzGmx73/W6cKLpo3JBn9+zT -1aEjlqkt0yHu4aAPVYQzOa5zIV8mjP84p3ODSyV9J8lfjFNXT7wdA8+9PVx3lVki -2ONoCNBh1kOxnxI4+BsMlQfF00ZbBSuGcMm3Ep3lTFWXzuUn3MQITzPwkL5LkW6a -sli/yAYQRTVDsXD8A3ACYT7BG31jGxyXtIHzqCci0MhZFdKKayMYkwfjZchIUtGN -JJoU8LQoHwGRtp3wutk0GlFzpEQEvSsn/Lsvvot5IfIe46tnzA6MVj5s64s5G8+Q -phhXFlzXqO/VxquPdbfYjvU= ------END CERTIFICATE-----', - ], - 11 - => [ - 'id' => 'rainloop', - 'categories' - => [ - 0 => 'social', - 1 => 'tools', - ], - 'userDocs' => '', - 'adminDocs' => '', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/RainLoop/rainloop-webmail/issues', - 'website' => 'http://www.rainloop.net/', - 'created' => '2016-10-20T04:17:37.217555Z', - 'lastModified' => '2016-11-18T11:36:04.309739Z', - 'releases' - => [ - 0 - => [ - 'version' => '4.26.0', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '>=5.4.0', - 'platformVersionSpec' => '>=10.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/pierre-alain-b/rainloop-nextcloud/releases/download/v4.26.0/rainloop-4.26.0.tar.gz', - 'created' => '2016-10-20T04:28:21.491747Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-18T11:36:04.619927Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '>=5.4', - 'rawPlatformVersionSpec' => '>=10 <=11', - 'signature' => 'nTYIVSB6mIwKtXIrKoVGsOGFflpLjed8jFem1VLQNtXQj4bztnNrdc4YaPIn0yzM -yLpMSqRDNzdYNFuOeDiyKLPJPTA++MotLCNjEe7kxUekek+m+qzgnGBdcT7RQT6R -p9xWGecnVx94d6aA55uiRhgQRyHpdDMMLCOz1be+HvpwHy69DRFZ1+SPmGUt6eW0 -u5yS0vHCu1K22cbrVNXFKjxAOlGcIDm61oQuz7ycl3uAujZO4rZbWt55jilgKGak -ew559A5gTp9W+j+TWKIcg6LIZ9zLRlGjcQrWJrsc+OBZQcqiYimSFyO6HhfT9TPS -Pof//I+dSsd+H0SRGGeL8DvSnK+NKZL1q5EX5pziqsv6nZFITpCDwmAN+I8AnXXL -SNkFi53M8RZTOABpD2x7YPYP1cEvwrRweqV/C/oHcYnpfh7D2DjFeWwXsjeAXrHY -hgFhPrg+7rf7g6UmJFOCp0dC9sBdyQ3KtJkv7bGqPr854r2cdA7xW0QHWQ2in9qQ -LhIczc32ECi3ZVVgyF8zyT4Y/3MRS05oX3FHvHyt88mjni6bVaO78F7ZRSha8gHh -NOAkku7AMXPvUCHaZP2iVCCoAViEso8GeR3O8xh2G42Ai61RLYwx8LB1+23EoJTr -mfFuRYNSg+qAKCokXNnh+lDlwu4AkaQo3vtKGPXvU7A=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/pierre-alain-b/rainloop-nextcloud/master/screenshots/2016.10.20-screenshot.jpg', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'RainLoop', - 'summary' => 'RainLoop Webmail', - 'description' => 'Simple, modern and fast web-based email client.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'RainLoop Team', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAzCCAusCAhAXMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDE5MTkzNDEwWhcNMjcwMTI1MTkzNDEwWjATMREwDwYD -VQQDDAhyYWlubG9vcDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqB -5jnF9qZ/qjckt0kRjpHCOMtJumW/KiQoMeNP5nGv4ad0DS3KemOapUef8Zn7qCYb -MnODhK7HBwPifFzI1j8bnT2hP6E0geFLb0MdN59d2NF0n4CCs1+BnepQPJ1kFbPK -35wQRi0RDeTf/GQ+/owEVCU9a9W1P/VUXk8Z0vMoQxCXEdRqnB63SgsKl7DB9G/C -4SYrgGor+OHVGl4ntMZhJujiM996DttrNK3iZRGkQ07L+lfUIwQ52XOhQNRdic4p -B03lw7PpChwPGMv/EEvdR5HpCJQBJniqJbbu3Jh8bMBKTE/8fCzN3vMXICB2g3Bq -lKkZW6fnJRGsrZ79fsQnl+WBPNSrWRLOxOfe1fyCFV1ljFB4nTH7uF3pC8ZRgJes -kHIESHz3GJm28hn4+17ESMGHBCbs7L9FK2GY31cobU0VRntLxpSG+d9njbIAgMG1 -S7U+oKVFQhSVpdXNOaUNqhcQ3HkbQTLEP0k53A/lhLQb2+KPd8nntaELjwNyrmZg -sVMgHj/zdlvrbguZjZFzUzDBFvkuv/5M58lNT/D1C6ufVp/R6eLsYI+nnk1ojAjz -l7N6U8X5SXpD+Bm7+Kn1PH+bHl7cViCx8oXJXO2RhP+COXckw7BDZKtjItYHNG7M -pFwgYqWpvCu9LN6IN5a/eLqSI76dOOP3iYbaTH+NAgMBAAEwDQYJKoZIhvcNAQEL -BQADggEBAGB0Vq0l6ndGTgNbZxSEFyBR3u3tiR3pWK81DYjsui7qBoO6P/BaGmf+ -raSwHPaBOwA9XNS8jcGLh5xdqY2p/m0dTS64xNjVL9nweWsG+FwVnPANo8C4nXdm -9ajJ4cdg54stQK8qn1uh/xPcd23GKfYJazjYSwYmZ3pXXdzlGN9NxkeYJQxJ6B+5 -pzAeVGiABI/e5URpxzz2UayRX7EE+vtpe3B84hzkLqsq0N39ZN6KLfaTyEBGLzqE -iLYeXQTV0XSRs8xVt+iyGlj7nPkv2DR0oCqRpWUFWeSBI//niDG5WxS3qg8kacSW -fDSYhSN+IjrnIkwNtc8V9t7/GeQB5FE= ------END CERTIFICATE-----', - ], - 12 - => [ - 'id' => 'richdocuments', - 'categories' - => [ - 0 => 'integration', - 1 => 'office', - ], - 'userDocs' => 'https://nextcloud.com/collaboraonline/', - 'adminDocs' => 'https://nextcloud.com/collaboraonline/', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/owncloud/richdocuments/issues', - 'website' => '', - 'created' => '2016-10-31T08:55:45.631429Z', - 'lastModified' => '2016-11-24T12:13:53.905352Z', - 'releases' - => [ - 0 - => [ - 'version' => '1.1.14', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/owncloud/richdocuments/releases/download/1.1.14/richdocuments.tar.gz', - 'created' => '2016-11-24T12:10:13.337165Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-24T12:13:53.963638Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'prDGlfRPxqT6LP0BsAFPwGww7P4Bngha2N4u5B6+F02N+RVOjGtTcXKqvM1KjZb1 -Co7qJvgJmjpvIvDmB+rup02i8ObfwP2ct6UdsD7ouzOWJG2sJANXK31bHyvOmQ2h -vKu5eNcOkf+WFyFKYi51TbsfWn2+1Wge3WWujKAVcEvqtcOOz+uMWNtqzBptEupk -E1aaRnQfTx488YB8Ubul06LIY0PNCHgGCWPgy817tOVT7JA+V0P0FFonl/PXE0dr -WgtxRJmvGaNiFzYq+kQmdKMfayZTm3kdVgP0W52t5wp878K0i4s2KPg5lANvjTz7 -DCT+VV2IGIE52o4RpMUGyQ==', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-document.png', - ], - 1 - => [ - 'url' => 'https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-app.png', - ], - 2 - => [ - 'url' => 'https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-presentation.png', - ], - 3 - => [ - 'url' => 'https://nextcloud.com/wp-content/themes/next/assets/img/features/collabora-spreadsheet.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Collabora Online', - 'summary' => 'Edit office documents directly in your browser.', - 'description' => 'Collabora Online allows you to to work with all kinds of office documents directly in your browser. This application requires Collabora Cloudsuite to be installed on one of your servers, please read the documentation to learn more about that.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Collabora Productivity based on work of Frank Karlitschek, Victor Dubiniuk', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIDCDCCAfACAhAZMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDMxMDg1NDExWhcNMjcwMjA2MDg1NDExWjAYMRYwFAYD -VQQDEw1yaWNoZG9jdW1lbnRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEA1jk29m6JykcJ2Ld0YEpjPMYh6kwxY6GysNJnfkA/th7tPWL3+vBJ9oTYyVnZ -jwAE1Cqwfa9MyBKMZ2IdfIqtT8PeWzuFP7Ib942EdxUpwwh9F3lykeGsj0h4zQwX -F9OooiS99PfLX+JpkKm15Ujb00iLB6xQmq0+3NeOT1CTD1ziJ1ueOcxBKMwaFp2a -Puz3F5ywqCvpmxG/OBuOs0LI3/zStXhBNbUMxBrWblr7zaVNJXl/I2JCKj8Wah/H -XUEEGbW15fAUP1f+90eQSxpEoCZDBHXOQCTJYzySGv+BjU+qlI9/gS0QbrsiyzUf -O5lyvi8LvUZBzpBw+yg1U75rqQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA9jU3m -ZmD0ywO3MUsG/GLigFtcWi/p7zp2BliR+NpuY2qNFYDcsIb8ZUudmUc/cJRRctzy -APaLLj/d+h5RFaxjTVvim1PSe6M7urK/IMSvyUVYCeQRYpG8ZJixKTCOVIBaWHMz -xTfc51tm9EPlpJpK6JtaWrYYoWGE3k9sINdJ4JkvKkE2CBAqVhX6ZGyEQ0bnEhtk -Ru1DXn+LW7TJ4NZ8VtLWvmW/6Kfmi7dQ1V++Kmn0lO5ntRt5altePbStCHC8bhGp -myBOrjhrJgLIwvgH26MYZhdiSkFzoE38nMPZdrUmUDxcPCwucWJqgzDPudguFthj -WCVZ3TTG/2z3+tWM ------END CERTIFICATE-----', - ], - 13 - => [ - 'id' => 'ocr', - 'categories' - => [ - 0 => 'files', - 1 => 'tools', - ], - 'userDocs' => 'https://janis91.github.io/ocr/', - 'adminDocs' => 'https://github.com/janis91/ocr/wiki', - 'developerDocs' => 'https://github.com/janis91/ocr/wiki', - 'issueTracker' => 'https://github.com/janis91/ocr/issues', - 'website' => 'https://janis91.github.io/ocr/', - 'created' => '2016-09-19T12:07:49.220376Z', - 'lastModified' => '2016-11-21T11:22:21.024501Z', - 'releases' - => [ - 0 - => [ - 'version' => '1.0.0', - 'phpExtensions' - => [ - ], - 'databases' - => [ - 0 - => [ - 'id' => 'pgsql', - 'versionSpec' => '*', - 'rawVersionSpec' => '*', - ], - 1 - => [ - 'id' => 'mysql', - 'versionSpec' => '*', - 'rawVersionSpec' => '*', - ], - 2 - => [ - 'id' => 'sqlite', - 'versionSpec' => '*', - 'rawVersionSpec' => '*', - ], - ], - 'shellCommands' - => [ - 0 => 'ocrmypdf', - 1 => 'tesseract', - ], - 'phpVersionSpec' => '>=5.6.0 <8.0.0', - 'platformVersionSpec' => '>=10.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/janis91/ocr/releases/download/v1.0.0/ocr.tar.gz', - 'created' => '2016-10-24T06:50:43.283900Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-21T11:22:21.269108Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '>=5.6 <=7', - 'rawPlatformVersionSpec' => '>=10 <=11', - 'signature' => 'CBJkCIiUKyf2NuWfz2zJ3grhf8p7wJes7DPV/OxUzhlxIH0Fh7K54+U5A9JOOi6f -WPhjXG1ylkyIVY1glr/B8svWNsD4jAclpnUi1/9ZW5UPT8LnRBfTbtF9Uoj0OgNs -tsGQYbpuREoHnjbJWTRe0kq1OsOfX44xuf8PuX43B+lpQPW4iRSSz3ZIhdPcDGq1 -7pyqQM7gdKhBQ6/tOiwd7Enyt5Hi4V6jhwhUOCYeTNiLD2V3yKL+qA9DzpXUfNNw -LGTjcaMrifibHQIZBZWbPPMmCfMJZ7GO9oR4gWHwkhWqt0yVWAJXAHJBLd5vXC5I -jtRTXRpHO/k6Dtqem8tZCVoDE5MAC7fDZ/0XzoFiXHciP6MenVasVcXo6xJOJc5y -GsrecNftUEhP/ngxA6lMBVkLmmdpiexVisvsavPi64i34OUA6qOuxjgNVBDwg56i -2lOEVvHa3nn0UX7ZZoQ/Nu6Mz7J3Hx/VDlttPuWe42eeJAphyDGubT1M62gW8dVB -D3tJOF7spnK6I3BhVLviou/zs30AIRVBDTU0Orzx78cbInwy6/vyJib2a1olAaHz -v05SzlQRnBWM4jYBe0mA/2ds9AO6VrXGrT/iLlHemj6JYoGBM185TGewA7OJyX3a -HSlSDqaremmi+aS3onx3AKhXykDxTRkMVarePwTzzFs=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc1.png', - ], - 1 - => [ - 'url' => 'https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc2.png', - ], - 2 - => [ - 'url' => 'https://raw.githubusercontent.com/janis91/ocr/master/screenshots/sc3.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'OCR', - 'summary' => 'Character recoginition for your images and pdf files.', - 'description' => '# Description - -Nextcloud OCR (optical character recoginition) processing for images and PDF with tesseract-ocr and OCRmyPDF brings OCR capability to your Nextcloud 10. -The app uses tesseract-ocr, OCRmyPDF and a php internal message queueing service in order to process images (png, jpeg, tiff) and PDF (currently not all PDF-types are supported, for more information see [here](https://github.com/jbarlow83/OCRmyPDF)) asynchronously and save the output file to the same folder in nextcloud, so you are able to search in it. -The source data won't get lost. Instead: - - in case of a PDF a copy will be saved with an extra layer of the processed text, so that you are able to search in it. - - in case of a image the result of the OCR processing will be saved in a .txt file next to the image (same folder). - -**One big feature is the asynchronous ocr processing brought by the internal php message queueing system (Semaphore functions), which supports workers to handle tasks asynchronous from the rest of nextcloud.** - -## Prerequisites, Requirements and Dependencies -The OCR app has some prerequisites: - - **[Nextcloud 10](https://nextcloud.com/)** or higher - - **Linux** server as environment. (tested with Debian 8, Raspbian and Ubuntu 14.04 (Trusty)) - - **[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF)** >v2.x (tested with v4.1.3 (v4 is recommended)) - - **[tesseract-ocr](https://github.com/tesseract-ocr/tesseract)** >v3.02.02 with corresponding language files (e.g. tesseract-ocr-eng) - -For further information see the homepage or the appropriate documentation.', - ], - 'de' - => [ - 'name' => 'OCR', - 'summary' => 'Schrifterkennung für Bilder (mit Text) und PDF Dateien.', - 'description' => '# Beschreibung - -OCR (Automatische Texterkennung) für Bilder (mit Text) und PDF Dateien mithilfe von tesseract-ocr und OCRmyPDF ermöglicht Ihnen automatische Schrifterkennung direkt in Ihrer Nextcloud 10. -Die App nutzt Tesseract-ocr, OCRmyPDF und den internen Message Queueing Service von PHP, um so asynchron (im Hintegrund) Bilder (PNG, JPEG, TIFF) und PDFs (aktuell werden nicht alle Typen unterstützt, näheres [hier](https://github.com/jbarlow83/OCRmyPDF)) zu verarbeiten. Das Ergebnis, welches jetzt durchsuchbar, kopierbar und ähnliches ist, wird anschließend im selben Ordner gespeichert, wie die Ursprungsdatei. -Die Ursuprungsdatei geht dabei nicht verloren: - - im Falle einer PDF wird eine Kopie mit einer zusätzlichen Textebene gespeichert, damit sie durchsuchbar und kopierbar wird. - - im Falle eines Bildes wird das Resultat in einer txt-Datei gespeichert. - -**Ein großer Vorteil ist, dass das Ausführen und Verarbeiten asynchron im Hintergrund stattfindet. Dies geschieht mithilfe der PHP internernen Unterstützung einer Message Queue (Semaphore Funktionen). Die Aufgaben werden somit getrennt von der Nextcloud in einem eigenen Arbeits-Prozess (Worker) abgearbeitet.** - -## Anforderungen und Abhängigkeiten -Für die OCR App müssen folgende Anforderungen erfüllt sein: - - **[Nextcloud 10](https://nextcloud.com/)** oder höher - - **Linux** server als Betriebssystem. (getestet mit Debian 8, Raspbian und Ubuntu 14.04 (Trusty)) - - **[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF)** >v2.x (getestet mit v4.1.3 (v4 empfohlen)) - - **[tesseract-ocr](https://github.com/tesseract-ocr/tesseract)** >v3.02.02 mit den dazugehörigen Übersetzungs- und Sprachdateien (z. B. tesseract-ocr-deu) - -Für weiter Informationen besuchen Sie die Homepage oder lesen Sie die zutreffende Dokumentation.', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Janis Koehr', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIID/jCCAuYCAhAKMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTE5MTEzNTAxWhcNMjYxMjI2MTEzNTAxWjAOMQwwCgYD -VQQDDANvY3IwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDpXiwec4f -XAT//7YBPSb4z6ZsBJSMfBq0VTL/HagjJnQ7BL+WagzWlS69IStNDqlIlenamYRX -4B40heJIUinzYKjTRbr5UAw6MX29HibZccm/qgrk36o1XTgIsoRhmvSxbXDVIo1k -bDOJN8gc2Gvswa8X+uOe9pfcDgAdqGxOvFnoKW89GnB01pCNT+xakNErGAFqVLsr -2AeademAZnbxJ1cB54tQn2Bygb/7DKKY8EmFfIq6/27n9Jbph1FG9HIlWRT4/M2H -U2pG3cCScWMEBPsW7kpfpnzLk7Q30Oj6k/rEYjJgmNYgg6oVnn0D9uRmhBYBnGyx -Mab1ilsK53lyuzQY0pmU8V5ULqpnNFAK6DVFfofEamDUhBPO+TZXEA5cZmuULRpf -QQXmGpUQSyV6pS9WirMIqXFp9wmQ4vtjMdhu/6CP7cmtYZdq9uOhWEHbQM0mZUkb -8hMjeItPx9XITI7Cge1JUOI8ZIwiB3USnQXcMd3v82l++/VgqHB7s5OaKPhygsWI -M6RCoBcGiuQB5/fEUOg5ACOpGVyJiBda0Mi57AdoxdJmfnr7Bxcf2tAWIJL9Y7T3 -E1+V2BMxJOWwvVz26Cq83F41yXK2hJS+SbfQTqNUR8Cfh50CS9POvgRxNrJK9yvI -kKle3ITRtGVM1XU0njWjnsdGg3D3O2mmjQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB -AQAbFddMbgfPI1szT57V1FKZrOrdYqQ7qjewlIQOzshGydbMtqS/9XL5hYocJCMt -Y6w+C/i6iEzO2Jx8D/k4rcZMXoVR6y3ZvO0Ke0gzSRsU+5eYj2FK1VV+cNIQW5Iu -CYYIVa7pVPVHdeQH2Bba680bLV0HMF6b1fI9IwkfdCAinvCYZLjyEXZlmB7YjyA8 -HR7qPCNz4uG2Va7mlUHE3UYUYnlv8JFOV3YdbVL0nxhWwIdzSri5sxFIhdlabpzY -yA1z/MCBEyTRo80jxFmL+MpwbsdbUJi7Qxlnd56zb6HHDGrLHXZTh9LXgyVbnhWL -kxomWjIXQh4aMHQL4QF7U4EK ------END CERTIFICATE-----', - ], - 14 - => [ - 'id' => 'spreedme', - 'categories' - => [ - 0 => 'tools', - ], - 'userDocs' => 'https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md', - 'adminDocs' => 'https://github.com/strukturag/nextcloud-spreedme/blob/master/README.md', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/strukturag/nextcloud-spreedme/issues', - 'website' => '', - 'created' => '2016-09-27T08:43:07.835196Z', - 'lastModified' => '2016-11-21T16:51:23.703819Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.3.4', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://apps.owncloud.com/CONTENT/content-files/174436-spreedme.tar.gz', - 'created' => '2016-11-21T16:51:23.689599Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-21T16:51:23.826509Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'Mhy3hXeGWlIujx1Op39MMRdqHYOo360BCwr4FPWoTNNggH3aS0gWlh48DAfGYK9W -etNiOqIuRyA0NrVlsqR2vDILgFtODJSbKPyHd3PQn3hcGsjogjQ+dkKciLNLinw7 -Ohbv6aDdRFLBeRHpX/7wOnWL5W3ko/gyn0Awvi88M9+nC5aARtqncQqPy2SxDGzH -KlOZHSNDnEQCGMhA8hNWWKdVwNUJHod/wmBWpW5QVNSJq5DqrKZcNwpGM2UUJoql -EqUMwDXk5uVH5r5k62Tr9kguDWoUEG1OqQSyeMY24AmA64tq/HSlAdZ+CX32bc4E -Zvm+n8poJBrdSVmWEaa4ZfYaLFdOc6Kcuid1B1Sv9kPhD9WD6T1sicdzjDzcorBK -/MLReCuSb2E8aPTnFWRoAZ4xCUGs1IXzX5fmxI8VdzwR42R6RhGJ/rqMuZRFenZF -bOks45K5gE1da4QpkYOUQa3GVMNPqPiT3CqjmJ8tjxq7bGpb6v+YoCLACjjPpPZL -2Y28qLxwHVaINDFUUxD75WWdrlulRbqHwiSw8jolP9qrpXhDuLAqYam9tRwV5K5R -8uNawnFwWkicBEYkN/WtBTouWzehOPn38tHXov6SyEyD6lkuxUBZrsGQ2ru+t33U -k0kKCbV0GFw43I+3Ji5DiB4TUVNZYVoPG1B7Qve+UfA=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/conference.gif', - ], - 1 - => [ - 'url' => 'https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/presentation.png', - ], - 2 - => [ - 'url' => 'https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/import.png', - ], - 3 - => [ - 'url' => 'https://raw.githubusercontent.com/strukturag/nextcloud-spreedme/master/screenshots/appstore/users.png', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Spreed.ME', - 'summary' => 'Audio-, video- and text chat for your Nextcloud', - 'description' => 'Securely communicate with your friends and family using rich audio-, video- and text chat, and much more right from your Nextcloud – in your browser', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'struktur AG', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAzCCAusCAhANMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTI2MTYxNzMzWhcNMjcwMTAyMTYxNzMzWjATMREwDwYD -VQQDEwhzcHJlZWRtZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKLx -2dCPBLIgX948BnOdLij0YyI2+FKD6uZOvzxMaoi3rlxNf8MJgraNMzTBWEXtxT5b -7ZISNp89WEXhaQ1dwwCocodd/xow4Ek63m5nUvTZXsm+YSbMgrFbxzsBhYU7KuIE -T/jhKdzYgemzErwwN/gtwkLMfPo3jkgg6c8NPPohYv6k7V4VnsqtJ0JS0kX19FqM -MiNz9XkcncBHy9x0BSxy4+YnwbFcgIx/MtYKlBL8NkPuuJaB/6C1O+IPYhdEdnpX -+RaIue71nSStOYOqT4YDqHAIw7EmqgA1my09mmK+0Pn92GJVEAEN7JGBSQ+F32RI -dB3ivGAOVtUtVvJlepWdbHxj1xqeP+LCjWzHMLQjm0TyH8VqU4Cg/wxwAEFnBATH -aOaWwrggzY2d9KBo1mp0k71NArLbBdlHykFU4bgiSDWrXXMz0fZzLQVwGI0Eqcxc -ouf6t0kvrK8oKjrnso+FjBoT7lHV/H6ny4ufxIEDAJ/FEBV/gMizt5fDZ+DvmMw4 -q+a088/lXoiI/vWPoGfOa77H5BQOt3y70Pmwv2uVYp46dtU8oat+ZvyW9iMmgP1h -JSEHj1WGGGlp45d10l4OghwfTB0OSuPUYwWR+lZnV8sukGvQzC9iRV1DGl/rREMC -cQ5ajRAtO5NPnThvN5/Zuh4n8JoDc0GK4jEZsIivAgMBAAEwDQYJKoZIhvcNAQEL -BQADggEBAGHMRbPV0WTI9r1w6m2iJRrMbZtbBb+mQr8NtOoXQwvSXWT1lXMP2N8u -LQ1a8U5UaUjeg7TnoUWTEOqU05HpwA8GZtdWZqPPQpe691kMNvfqF64g0le2kzOL -huMP9kpDGzSD8pEKf1ihxvEWNUBmwewrZTC3+b4gM+MJ3BBCfb5SCzMURLirfFST -axCNzc7veb2M98hS73w5ZE6vO+C/wz0GTsxuK0AoLitApT5naQnjvxSvSsjFPEGD -sUNUEU2Decyp0jxLVnrrpz6Y5UupfBR0V8yAv1t5Od/mCKLc5DxHsDWiKOpsob9U -JN+bdzJil2NNftihD4Dm7Ha7OS3O8W0= ------END CERTIFICATE-----', - ], - 15 - => [ - 'id' => 'nextant', - 'categories' - => [ - 0 => 'files', - 1 => 'tools', - ], - 'userDocs' => '', - 'adminDocs' => 'https://github.com/nextcloud/nextant/wiki', - 'developerDocs' => '', - 'issueTracker' => 'https://github.com/nextcloud/nextant/issues', - 'website' => 'https://github.com/nextcloud/nextant/wiki', - 'created' => '2016-09-14T14:34:35.977699Z', - 'lastModified' => '2016-11-22T16:02:57.758477Z', - 'releases' - => [ - 0 - => [ - 'version' => '0.6.6', - 'phpExtensions' - => [ - ], - 'databases' - => [ - ], - 'shellCommands' - => [ - ], - 'phpVersionSpec' => '*', - 'platformVersionSpec' => '>=9.0.0 <12.0.0', - 'minIntSize' => 32, - 'download' => 'https://github.com/nextcloud/nextant/releases/download/v0.6.6/nextant-0.6.6.tar.gz', - 'created' => '2016-11-16T15:11:14.344704Z', - 'licenses' - => [ - 0 => 'agpl', - ], - 'lastModified' => '2016-11-16T20:39:59.030384Z', - 'isNightly' => false, - 'rawPhpVersionSpec' => '*', - 'rawPlatformVersionSpec' => '>=9 <=11', - 'signature' => 'aOZeEeThyZ0V/vXBcn6c+Z0vyCsZcN6nfSJ8oWEea4zXh4g705Si+MFZESqix3M2 -OPCnA/U8eASwdRTAEwQJrW5ECmu1THXSIsrzQzc9kFycvyOGzCgAWtuu0ayzZD2/ -U5aDWlzpLHC1Czg9QJ5UnfZR0AfChWQ402N1YzGqMShdJv6AHXFrVE+uYnIyxuYI -oPJQBUYbQwthVUjpYwFwSxw50YU17gmx5RZ0Y0OPz3i/EiuEUrxopXtfDVYAuCML -pDw37LOTRQ2JqxSU3teALh8LcrwJbTeOP0n4bTeV+vU3jvtiaEoRrwfVrK41F701 -QymGXy1/EFG0kxPGS2dRNPBAXYLZfeoWlROl3D5BWlbsCcXKU1S+22yn0TEdS7x1 -Y44x8jRKnBddDE7qkn+QoQYHNNcxOREsFFLmIoyCUpdNOdDX2PvTFUYkIqdnXaJy -oAKv2GkvWPQ0aiiBtA1i4oXuzvHW/M2wOrK7v7DCpNfILrD/sjxpljxcX082nRCd -9P3iPd2hQ6yOM9fG21LVN74b6wggI81BzFf/xJPd4ZqYLjfeG/yqd0zaiMOzMm1W -se+kc/a4iB3BoCNX3E942pBBzew4ya8LkCXdCHUUsuelDf1va1ikTh/G7D84ll9/ -2avNqQnUh3hgOnxFCLI/5VrbqxfSTVdO6O/LTuAmwgw=', - 'translations' - => [ - ], - ], - ], - 'screenshots' - => [ - 0 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/nextant/master/screenshots/displayResult.jpg', - ], - 1 - => [ - 'url' => 'https://raw.githubusercontent.com/nextcloud/nextant/master/screenshots/admin.jpg', - ], - ], - 'translations' - => [ - 'en' - => [ - 'name' => 'Nextant', - 'summary' => 'Navigate through your cloud using Solr', - 'description' => ' - Navigate through your cloud using Solr - - -**Nextant** performs fast and concise _Full-Text Search_ within: - -- your own files, -- shared files, -- external storage, -- bookmarks - - -### Recognized file format: -- plain text, -- rtf, -- pdf, -- html, -- openoffice, -- microsoft office, -- image JPEG and TIFF (will requiert Tesseract installed) -- pdf with no text layer (will also requiert Tesseract) _[work in progress]_ - - - -## Installation - -- [You first need to install a Solr servlet](https://github.com/nextcloud/nextant/wiki) -- Download the .zip from the appstore, unzip and place this app in **nextcloud/apps/** (or clone the github and build the app yourself) -- Enable the app in the app list, -- Edit the settings in the administration page. -- Extract the current files from your cloud using the **./occ nextant:index** commands -- Have a look to this [explanation on how Nextant works](https://github.com/nextcloud/nextant/wiki/Extracting,-Live-Update) -- _(Optional)_ [Installing Tesseract](https://github.com/tesseract-ocr/tesseract/wiki) ([Optical Character Recognition](https://en.wikipedia.org/wiki/Optical_character_recognition) (OCR) Engine) will allow Nextant to extract text from images and pdfs without text layer. - - - ', - ], - ], - 'isFeatured' => false, - 'authors' - => [ - 0 - => [ - 'name' => 'Maxence Lange', - 'mail' => '', - 'homepage' => '', - ], - ], - 'ratingRecent' => 0.5, - 'ratingOverall' => 0.5, - 'ratingNumRecent' => 0, - 'ratingNumOverall' => 0, - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAFMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYwOTE0MTI1NDQwWhcNMjYxMjIxMTI1NDQwWjASMRAwDgYD -VQQDDAduZXh0YW50MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsbnQ -+9acjKHfcrUj4yqBpD++GmQ5z2Sp8C8uOz4ZbLyM9bUXEYHo4a4u3CdC49kGUkb3 -p5MkEAEzslTWDi1eh5MZgPWpbPgItsDsXY1o55O3jtxNkzSG5/yYcPQcuKtIOm9S -7DY0K+UQt3nK+RrXEZfARMNrzFbEzpE3b7w901Yl5n+m/B8rhW4pqg8uSfx3u04J -wduV1fHwoHUB0Ox5HyWib4Pq1XppNh7xdc2Fg93JxshwuCPJyOOzrFTnxC7s1yzQ -UvaqkjPW5QeQRunQjZ2XtpYH8f8v01W18bNEiHwqtFwuDEyCVx1rvEMgUDVXdPkP -gZrlB5TzGmz0U3HzYvf6205WuzfHxz7kPj502wP51PoZBKpniggKzmuXkx6BpsZC -ZX45VpDHdiATLwRj1t2bMs3C01nzpIWO5ZwFtkepH3Y+mvwX5lDh/XDsqJC2Yo8o -WMmniWNW7dspufYOsBUqqYGP7rkailgVT4oYk6D1j6oFZ5SSpfPF5lsyYedDSM6y -bIGVkSF+sjLK6R9BenBijKceAKsS//WwRYCBPC+JHlsYpXKW12bL+C47Kj2/N6d4 -rYryzV6ofVSF6pwIq0oEjoyfBfNpYavf3xrRkSSmIIlPSnMY7DT1xkGD5retxSm6 -+WIfkWKRZpv2S6PhMHGLspYc4H5Dj8c48rG5Co8CAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEAOZUwyPaUi+1BOUgQJMWqYRoTVZUyBshTXSC7jSwa97b7qADV9ooA6TYF -zgsPcE41k7jRkUbnjcY45RGtL3vqsgZbx5TjPa5fGMxlqJ6eYBOY61Q6VIHEVm3u -xnPEO9dsMoDBijvo5D7KtE+Ccs907Rq70kCsbrdgPHkyb5tDSnCKogN1LiQrg1EP -my7Z1C7jG9/h57vx0+QBMDCYnTmqLsvMKqo27uHskzAiB7VXLEdSZ2FtMGHkLUQO -0bfhnvTZ2VhMmK83t7ovo71An4ycmsolGD/MA0vNI78VrVISrdI8rRh2WntUnCBU -EJL3BaQAQaASSsvFrcozYxrQG4VzEg== ------END CERTIFICATE-----', - ], - ], - 'timestamp' => 1234, - 'ncversion' => '11.0.0.2', - 'ETag' => '"myETag"', - ]; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - /** @var Factory|\PHPUnit\Framework\MockObject\MockObject $factory */ - $factory = $this->createMock(Factory::class); - $this->appData = $this->createMock(AppData::class); - $factory->expects($this->once()) - ->method('get') - ->with('appstore') - ->willReturn($this->appData); - $this->clientService = $this->createMock(IClientService::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->config = $this->createMock(IConfig::class); - $this->compareVersion = new CompareVersion(); - $this->logger = $this->createMock(LoggerInterface::class); - $this->registry = $this->createMock(IRegistry::class); - - $this->fetcher = $this->getMockBuilder(AppFetcher::class) - ->onlyMethods(['getChannel']) - ->setConstructorArgs([ - $factory, - $this->clientService, - $this->timeFactory, - $this->config, - $this->compareVersion, - $this->logger, - $this->registry, - ]) - ->getMock(); - - $this->fetcher->method('getChannel') - ->willReturn('stable'); - } - - public function testGetWithFilter(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } elseif ($key === 'appstoreurl' && $default === 'https://apps.nextcloud.com/api/v1') { - return 'https://custom.appsstore.endpoint/api/v1'; - } else { - return $default; - } - }); - $this->config - ->method('getSystemValueBool') - ->willReturnArgument(1); - - $file = $this->createMock(ISimpleFile::class); - $folder = $this->createMock(ISimpleFolder::class); - $folder - ->expects($this->once()) - ->method('getFile') - ->with('apps.json') - ->willThrowException(new NotFoundException()); - $folder - ->expects($this->once()) - ->method('newFile') - ->with('apps.json') - ->willReturn($file); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->method('get') - ->with('https://custom.appsstore.endpoint/api/v1/apps.json') - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn(self::$responseJson); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1234); - - $dataToPut = self::$expectedResponse; - $dataToPut['data'] = array_values($dataToPut['data']); - $originalArray = json_decode(self::$responseJson, true); - $mappedOriginalArray = []; - foreach ($originalArray as $key => $value) { - foreach ($value as $releaseKey => $releaseValue) { - if ($releaseKey === 'id') { - $mappedOriginalArray[$releaseValue] = $originalArray[$key]; - } - } - } - foreach ($dataToPut['data'] as $key => $appValue) { - foreach ($appValue as $appKey => $value) { - if ($appKey === 'certificate' || $appKey === 'description') { - $dataToPut['data'][$key][$appKey] = $mappedOriginalArray[$appValue['id']][$appKey]; - } - } - } - - $file - ->expects($this->once()) - ->method('putContent'); - $file - ->method('getContent') - ->willReturn(json_encode(self::$expectedResponse)); - - $this->assertEquals(self::$expectedResponse['data'], $this->fetcher->get()); - } - - public function testAppstoreDisabled(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'has_internet_connection') { - return true; - } elseif ($var === 'appstoreenabled') { - return false; - } - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } - - public function testNoInternet(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'has_internet_connection') { - return false; - } elseif ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'has_internet_connection') { - return false; - } elseif ($var === 'appstoreenabled') { - return true; - } - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } - - public function testSetVersion(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '10.0.7.2'; - } elseif ($key === 'appstoreurl' && $default === 'https://apps.nextcloud.com/api/v1') { - return 'https://custom.appsstore.endpoint/api/v1'; - } else { - return $default; - } - }); - $this->config - ->method('getSystemValueBool') - ->willReturnArgument(1); - - $file = $this->createMock(ISimpleFile::class); - $folder = $this->createMock(ISimpleFolder::class); - $folder - ->expects($this->once()) - ->method('getFile') - ->with('future-apps.json') - ->willThrowException(new NotFoundException()); - $folder - ->expects($this->once()) - ->method('newFile') - ->with('future-apps.json') - ->willReturn($file); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->method('get') - ->with('https://custom.appsstore.endpoint/api/v1/apps.json') - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn(self::$responseJson); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1234); - - $dataToPut = self::$expectedResponse; - $dataToPut['data'] = array_values($dataToPut['data']); - $originalArray = json_decode(self::$responseJson, true); - $mappedOriginalArray = []; - foreach ($originalArray as $key => $value) { - foreach ($value as $releaseKey => $releaseValue) { - if ($releaseKey === 'id') { - $mappedOriginalArray[$releaseValue] = $originalArray[$key]; - } - } - } - foreach ($dataToPut['data'] as $key => $appValue) { - foreach ($appValue as $appKey => $value) { - if ($appKey === 'certificate' || $appKey === 'description') { - $dataToPut['data'][$key][$appKey] = $mappedOriginalArray[$appValue['id']][$appKey]; - } - } - } - - $file - ->expects($this->once()) - ->method('putContent'); - $file - ->method('getContent') - ->willReturn(json_encode(self::$expectedResponse)); - - $this->fetcher->setVersion('11.0.0.2', 'future-apps.json', false); - $this->assertEquals(self::$expectedResponse['data'], $this->fetcher->get()); - } - - public function testGetAppsAllowlist(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } else { - return $default; - } - }); - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'appsallowlist') { - return ['contacts']; - } - return $default; - }); - $this->config->method('getAppValue') - ->willReturnCallback(function ($app, $key, $default) { - if ($app === 'support' && $key === 'subscription_key') { - return 'subscription-key'; - } - return $default; - }); - $this->config - ->method('getSystemValueBool') - ->willReturnArgument(1); - - $file = $this->createMock(ISimpleFile::class); - $folder = $this->createMock(ISimpleFolder::class); - $folder - ->expects($this->once()) - ->method('getFile') - ->with('apps.json') - ->willThrowException(new NotFoundException()); - $folder - ->expects($this->once()) - ->method('newFile') - ->with('apps.json') - ->willReturn($file); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://apps.nextcloud.com/api/v1/apps.json', [ - 'timeout' => 120, - 'headers' => [ - 'X-NC-Subscription-Key' => 'subscription-key', - ], - ]) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn(self::$responseJson); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1234); - - $this->registry - ->expects($this->exactly(2)) - ->method('delegateHasValidSubscription') - ->willReturn(true); - - $file - ->expects($this->once()) - ->method('putContent'); - $file - ->method('getContent') - ->willReturn(json_encode(self::$expectedResponse)); - - $apps = array_values($this->fetcher->get()); - $this->assertEquals(count($apps), 1); - $this->assertEquals($apps[0]['id'], 'contacts'); - } - - public function testGetAppsAllowlistCustomAppstore(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } elseif ($key === 'appstoreurl' && $default === 'https://apps.nextcloud.com/api/v1') { - return 'https://custom.appsstore.endpoint/api/v1'; - } else { - return $default; - } - }); - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'appsallowlist') { - return ['contacts']; - } else { - return $default; - } - }); - $this->config - ->method('getSystemValueBool') - ->willReturnArgument(1); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['settings', 'appstore-fetcher-lastFailure', '0', '0'], - ['settings', 'appstore-timeout', '120', '120'], - ]); - - $file = $this->createMock(ISimpleFile::class); - $folder = $this->createMock(ISimpleFolder::class); - $folder - ->expects($this->once()) - ->method('getFile') - ->with('apps.json') - ->willThrowException(new NotFoundException()); - $folder - ->expects($this->once()) - ->method('newFile') - ->with('apps.json') - ->willReturn($file); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://custom.appsstore.endpoint/api/v1/apps.json', [ - 'timeout' => 120, - ]) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn(self::$responseJson); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1234); - - $this->registry - ->expects($this->exactly(1)) - ->method('delegateHasValidSubscription') - ->willReturn(true); - - $file - ->expects($this->once()) - ->method('putContent'); - $file - ->method('getContent') - ->willReturn(json_encode(self::$expectedResponse)); - - $apps = array_values($this->fetcher->get()); - $this->assertEquals(count($apps), 1); - $this->assertEquals($apps[0]['id'], 'contacts'); - } -} --- a/tests/lib/App/AppStore/Fetcher/CategoryFetcherTest.php +++ /dev/null @@ -1,67 +0,0 @@ -fileName = 'categories.json'; - $this->endpoint = 'https://apps.nextcloud.com/api/v1/categories.json'; - - $this->fetcher = new CategoryFetcher( - $this->appDataFactory, - $this->clientService, - $this->timeFactory, - $this->config, - $this->logger, - $this->registry - ); - } - - public function testAppstoreDisabled(): void { - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreenabled') { - return false; - } - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } - - public function testNoInternet(): void { - $this->config - ->method('getSystemValueBool') - ->willReturnCallback(function ($var, $default) { - if ($var === 'has_internet_connection') { - return false; - } - return $default; - }); - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - return $default; - }); - $this->appData - ->expects($this->never()) - ->method('getFolder'); - - $this->assertEquals([], $this->fetcher->get()); - } -} --- a/tests/lib/App/AppStore/Fetcher/FetcherBase.php +++ /dev/null @@ -1,772 +0,0 @@ -appDataFactory = $this->createMock(Factory::class); - $this->appData = $this->createMock(AppData::class); - $this->appDataFactory->expects($this->once()) - ->method('get') - ->with('appstore') - ->willReturn($this->appData); - $this->clientService = $this->createMock(IClientService::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->registry = $this->createMock(IRegistry::class); - } - - public function testGetWithAlreadyExistingFileAndUpToDateTimestampAndVersion(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $file - ->expects($this->once()) - ->method('getContent') - ->willReturn('{"timestamp":1200,"data":[{"id":"MyApp"}],"ncversion":"11.0.0.2"}'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1499); - - $expected = [ - [ - 'id' => 'MyApp', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithNotExistingFileAndUpToDateTimestampAndVersion(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreurl') { - return 'https://apps.nextcloud.com/api/v1'; - } elseif ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willThrowException(new NotFoundException()); - $folder - ->expects($this->once()) - ->method('newFile') - ->with($this->fileName) - ->willReturn($file); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->once()) - ->method('getContent') - ->willReturn($fileData); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1502); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithAlreadyExistingFileAndOutdatedTimestamp(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } else { - return $default; - } - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}},"ncversion":"11.0.0.2"}', - $fileData - ); - $this->timeFactory - ->expects($this->exactly(2)) - ->method('getTime') - ->willReturnOnConsecutiveCalls( - 4801, - 1502 - ); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithAlreadyExistingFileAndNoVersion(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreurl') { - return 'https://apps.nextcloud.com/api/v1'; - } elseif ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1201,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}', - $fileData - ); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1201); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithAlreadyExistingFileAndOutdatedVersion(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreurl') { - return 'https://apps.nextcloud.com/api/v1'; - } elseif ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1201,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - '{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}},"ncversion":"11.0.0.1"', - $fileData - ); - $this->timeFactory - ->method('getTime') - ->willReturn(1201); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithExceptionInClient(): void { - $this->config->method('getSystemValueString') - ->willReturnArgument(1); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $file - ->expects($this->once()) - ->method('getContent') - ->willReturn('{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}}'); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willThrowException(new \Exception()); - - $this->assertSame([], $this->fetcher->get()); - } - - public function testGetMatchingETag(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } else { - return $default; - } - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['settings', 'appstore-fetcher-lastFailure', '0', '0'], - ['settings', 'appstore-timeout', '120', '120'], - ]); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $origData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - - $newData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":4802,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($newData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - $origData, - $newData, - ); - $this->timeFactory - ->expects($this->exactly(2)) - ->method('getTime') - ->willReturnOnConsecutiveCalls( - 4801, - 4802 - ); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - $this->equalTo($this->endpoint), - $this->equalTo([ - 'timeout' => 120, - 'headers' => [ - 'If-None-Match' => '"myETag"' - ] - ]) - )->willReturn($response); - $response->method('getStatusCode') - ->willReturn(304); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetNoMatchingETag(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.2'; - } else { - return $default; - } - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['settings', 'appstore-fetcher-lastFailure', '0', '0'], - ['settings', 'appstore-timeout', '120', '120'], - ]); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":4802,"ncversion":"11.0.0.2","ETag":"\"newETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - '{"data":[{"id":"MyOldApp","abc":"def"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}', - $fileData, - ); - $this->timeFactory - ->expects($this->exactly(2)) - ->method('getTime') - ->willReturnOnConsecutiveCalls( - 4801, - 4802, - ); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - $this->equalTo($this->endpoint), - $this->equalTo([ - 'timeout' => 120, - 'headers' => [ - 'If-None-Match' => '"myETag"', - ] - ]) - ) - ->willReturn($response); - $response->method('getStatusCode') - ->willReturn(200); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"newETag"'); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testFetchAfterUpgradeNoETag(): void { - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'version') { - return '11.0.0.3'; - } else { - return $default; - } - }); - $this->config->method('getSystemValueBool') - ->willReturnArgument(1); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['settings', 'appstore-fetcher-lastFailure', '0', '0'], - ['settings', 'appstore-timeout', '120', '120'], - ]); - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($file); - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1501,"ncversion":"11.0.0.3","ETag":"\"newETag\""}'; - $file - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $file - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - '{"data":[{"id":"MyOldApp","abc":"def"}],"timestamp":1200,"ncversion":"11.0.0.2","ETag":"\"myETag\""}', - $fileData - ); - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - $this->equalTo($this->endpoint), - $this->equalTo([ - 'timeout' => 120, - ]) - ) - ->willReturn($response); - $response->method('getStatusCode') - ->willReturn(200); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"newETag"'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1501); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } - - public function testGetWithUnreadableCacheFileRecreatesAndFetches(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($var, $default) { - if ($var === 'appstoreurl') { - return 'https://apps.nextcloud.com/api/v1'; - } elseif ($var === 'version') { - return '11.0.0.2'; - } - return $default; - }); - $this->config->method('getSystemValueBool') - ->willReturn(true); - - $folder = $this->createMock(ISimpleFolder::class); - $corruptedFile = $this->createMock(ISimpleFile::class); - $freshFile = $this->createMock(ISimpleFile::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('/') - ->willReturn($folder); - $folder - ->expects($this->once()) - ->method('getFile') - ->with($this->fileName) - ->willReturn($corruptedFile); - $corruptedFile - ->expects($this->once()) - ->method('getContent') - ->willThrowException(new GenericFileException()); - $corruptedFile - ->expects($this->once()) - ->method('delete'); - $folder - ->expects($this->once()) - ->method('newFile') - ->with($this->fileName) - ->willReturn($freshFile); - - $client = $this->createMock(IClient::class); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $response = $this->createMock(IResponse::class); - $client - ->expects($this->once()) - ->method('get') - ->with($this->endpoint) - ->willReturn($response); - $response - ->expects($this->once()) - ->method('getBody') - ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); - $response->method('getHeader') - ->with($this->equalTo('ETag')) - ->willReturn('"myETag"'); - - $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502,"ncversion":"11.0.0.2","ETag":"\"myETag\""}'; - $freshFile - ->expects($this->once()) - ->method('putContent') - ->with($fileData); - $freshFile - ->expects($this->once()) - ->method('getContent') - ->willReturn($fileData); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(1502); - - $expected = [ - [ - 'id' => 'MyNewApp', - 'foo' => 'foo', - ], - [ - 'id' => 'bar', - ], - ]; - $this->assertSame($expected, $this->fetcher->get()); - } -} --- a/tests/lib/App/AppStore/Version/VersionParserTest.php +++ /dev/null @@ -1,85 +0,0 @@ -versionParser = new VersionParser(); - } - - /** - * @return array - */ - public static function versionProvider(): array { - return [ - [ - '*', - new Version('', ''), - ], - [ - '<=8.1.2', - new Version('', '8.1.2'), - ], - [ - '<=9', - new Version('', '9'), - ], - [ - '>=9.3.2', - new Version('9.3.2', ''), - ], - [ - '>=8.1.2 <=9.3.2', - new Version('8.1.2', '9.3.2'), - ], - [ - '>=8.2 <=9.1', - new Version('8.2', '9.1'), - ], - [ - '>=9 <=11', - new Version('9', '11'), - ], - ]; - } - - /** - * - * @param string $input - * @param Version $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] - public function testGetVersion($input, - Version $expected): void { - $this->assertEquals($expected, $this->versionParser->getVersion($input)); - } - - public function testGetVersionException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Version cannot be parsed: BogusVersion'); - - $this->versionParser->getVersion('BogusVersion'); - } - - public function testGetVersionExceptionWithMultiple(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Version cannot be parsed: >=8.2 <=9.1a'); - - $this->versionParser->getVersion('>=8.2 <=9.1a'); - } -} --- a/tests/lib/App/AppStore/Version/VersionTest.php +++ /dev/null @@ -1,25 +0,0 @@ -assertSame('9', $version->getMinimumVersion()); - } - - public function testGetMaximumVersion(): void { - $version = new Version('9', '10'); - $this->assertSame('10', $version->getMaximumVersion()); - } -} --- a/tests/lib/App/CompareVersionTest.php +++ /dev/null @@ -1,79 +0,0 @@ -compare = new CompareVersion(); - } - - public static function comparisonData(): array { - return [ - // Compatible versions - ['13.0.0.3', '13.0.0', '>=', true], - ['13.0.0.3', '13.0.0', '<=', true], - ['13.0.0', '13.0.0', '>=', true], - ['13.0.0', '13.0', '<=', true], - ['13.0.0', '13', '>=', true], - ['13.0.1', '13', '>=', true], - ['13.0.1', '13', '<=', true], - ['13.0.1.9', '13', '<=', true], - ['13.0.1-beta.1', '13', '<=', true], - ['7.4.14', '7.4', '<=', true], - ['7.4.14-ubuntu', '7.4', '<=', true], - ['7.4.14-ubuntu', '7.4.15', '<=', true], - ['7.4.16-ubuntu', '7.4.15', '<=', false], - // Incompatible major versions - ['13.0.0.3', '13.0.0', '<', false], - ['12.0.0', '13.0.0', '>=', false], - ['12.0.0', '13.0', '>=', false], - ['12.0.0', '13', '>=', false], - ['7.4.15-ubuntu', '7.4.15', '>=', true], - // Incompatible minor and patch versions - ['13.0.0', '13.0.1', '>=', false], - ['13.0.0', '13.1', '>=', false], - // Compatible minor and patch versions - ['13.0.1', '13.0.0', '>=', true], - ['13.2.0', '13.1', '>=', true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('comparisonData')] - public function testComparison(string $actualVersion, string $requiredVersion, - string $comparator, bool $expected): void { - $isCompatible = $this->compare->isCompatible($actualVersion, $requiredVersion, - $comparator); - - $this->assertEquals($expected, $isCompatible); - } - - public function testInvalidServerVersion(): void { - $actualVersion = '13'; - $this->expectException(InvalidArgumentException::class); - - $this->compare->isCompatible($actualVersion, '13.0.0'); - } - - public function testInvalidRequiredVersion(): void { - $actualVersion = '13.0.0'; - $this->expectException(InvalidArgumentException::class); - - $this->compare->isCompatible($actualVersion, '13.0.0.9'); - } -} --- a/tests/lib/App/DependencyAnalyzerTest.php +++ /dev/null @@ -1,762 +0,0 @@ -platformMock = $this->getMockBuilder(Platform::class) - ->disableOriginalConstructor() - ->getMock(); - $this->platformMock->expects($this->any()) - ->method('getPhpVersion') - ->willReturn('5.4.3'); - $this->platformMock->expects($this->any()) - ->method('getIntSize') - ->willReturn(4); - $this->platformMock->expects($this->any()) - ->method('getDatabase') - ->willReturn('mysql'); - $this->platformMock->expects($this->any()) - ->method('getOS') - ->willReturn('Linux'); - $this->platformMock->expects($this->any()) - ->method('isCommandKnown') - ->willReturnCallback(function ($command) { - return ($command === 'grep'); - }); - $this->platformMock->expects($this->any()) - ->method('getLibraryVersion') - ->willReturnCallback(function ($lib) { - if ($lib === 'curl') { - return '2.3.4'; - } - return null; - }); - $this->platformMock->expects($this->any()) - ->method('getOcVersion') - ->willReturn('8.0.2'); - - $this->analyser = new DependencyAnalyzer($this->platformMock); - } - - /** - * - * @param string $expectedMissing - * @param string $minVersion - * @param string $maxVersion - * @param string $intSize - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesPhpVersion')] - public function testPhpVersion($expectedMissing, $minVersion, $maxVersion, $intSize): void { - $app = [ - 'dependencies' => [ - 'php' => [] - ] - ]; - if (!is_null($minVersion)) { - $app['dependencies']['php']['@attributes']['min-version'] = $minVersion; - } - if (!is_null($maxVersion)) { - $app['dependencies']['php']['@attributes']['max-version'] = $maxVersion; - } - if (!is_null($intSize)) { - $app['dependencies']['php']['@attributes']['min-int-size'] = $intSize; - } - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * @param $expectedMissing - * @param $databases - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesDatabases')] - public function testDatabases($expectedMissing, $databases): void { - $app = [ - 'dependencies' => [ - ] - ]; - if (!is_null($databases)) { - $app['dependencies']['database'] = $databases; - } - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * - * @param string $expectedMissing - * @param string|null $commands - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesCommands')] - public function testCommand($expectedMissing, $commands): void { - $app = [ - 'dependencies' => [ - ] - ]; - if (!is_null($commands)) { - $app['dependencies']['command'] = $commands; - } - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * @param $expectedMissing - * @param $libs - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesLibs')] - public function testLibs($expectedMissing, $libs): void { - $app = [ - 'dependencies' => [ - ] - ]; - if (!is_null($libs)) { - $app['dependencies']['lib'] = $libs; - } - - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * @param $expectedMissing - * @param $oss - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesOS')] - public function testOS($expectedMissing, $oss): void { - $app = [ - 'dependencies' => [] - ]; - if (!is_null($oss)) { - $app['dependencies']['os'] = $oss; - } - - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * @param $expectedMissing - * @param $oc - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providesOC')] - public function testOC($expectedMissing, $oc): void { - $app = [ - 'dependencies' => [] - ]; - if (!is_null($oc)) { - $app['dependencies'] = $oc; - } - - $missing = $this->analyser->analyze($app); - - $this->assertTrue(is_array($missing)); - $this->assertEquals($expectedMissing, $missing); - } - - /** - * @return array - */ - public static function providesOC(): array { - return [ - // no version -> no missing dependency - [ - [], - null, - ], - [ - [], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '8', - 'max-version' => '8', - ], - ], - ], - ], - [ - [], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '8.0', - 'max-version' => '8.0', - ], - ], - ], - ], - [ - [], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '8.0.2', - 'max-version' => '8.0.2' - ], - ], - ], - ], - [ - [ - 'Server version 8.0.3 or higher is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '8.0.3' - ], - ], - ], - ], - [ - [ - 'Server version 9 or higher is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9' - ], - ], - ], - ], - [ - [ - 'Server version 10 or higher is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '10' - ], - ], - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '9' - ], - ], - ], - ], - [ - [ - 'Server version 9.2 or higher is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9.2', - ], - ], - ], - ], - [ - [ - 'Server version 11 or higher is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '11', - ], - ], - ], - ], - [ - [ - 'Server version 8.0.1 or lower is required.', - ], - [ - 'nextcloud' => [ - '@attributes' => [ - 'max-version' => '8.0.1', - ], - ], - ], - ], - [ - [], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '8', - 'max-version' => '8', - ], - ], - ], - ], - [ - [], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '8.0', - 'max-version' => '8.0', - ], - ], - ], - ], - [ - [], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '8.0.2', - 'max-version' => '8.0.2' - ], - ], - ], - ], - [ - [ - 'Server version 8.0.3 or higher is required.', - ], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '8.0.3' - ], - ], - ], - ], - [ - [ - 'Server version 9 or higher is required.', - ], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '9' - ], - ], - ], - ], - [ - [ - 'Server version 9.2 or higher is required.', - ], - [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '9.2', - ], - ], - ], - ], - [ - [ - 'Server version 8.0.1 or lower is required.', - ], - [ - 'owncloud' => [ - '@attributes' => [ - 'max-version' => '8.0.1', - ], - ], - ], - ], - ]; - } - - /** - * @return array - */ - public static function providesOS(): array { - return [ - [[], null], - [[], []], - [['The following platforms are supported: ANDROID'], 'ANDROID'], - [['The following platforms are supported: WINNT'], ['WINNT']] - ]; - } - - /** - * @return array - */ - public static function providesLibs(): array { - return [ - // we expect curl to exist - [[], 'curl'], - // we expect abcde to exist - [['The library abcde is not available.'], ['abcde']], - // curl in version 100.0 does not exist - [['Library curl with a version higher than 100.0 is required - available version 2.3.4.'], - [['@attributes' => ['min-version' => '100.0'], '@value' => 'curl']]], - // curl in version 100.0 does not exist - [['Library curl with a version lower than 1.0.0 is required - available version 2.3.4.'], - [['@attributes' => ['max-version' => '1.0.0'], '@value' => 'curl']]], - [['Library curl with a version lower than 2.3.3 is required - available version 2.3.4.'], - [['@attributes' => ['max-version' => '2.3.3'], '@value' => 'curl']]], - [['Library curl with a version higher than 2.3.5 is required - available version 2.3.4.'], - [['@attributes' => ['min-version' => '2.3.5'], '@value' => 'curl']]], - [[], - [['@attributes' => ['min-version' => '2.3.4', 'max-version' => '2.3.4'], '@value' => 'curl']]], - [[], - [['@attributes' => ['min-version' => '2.3', 'max-version' => '2.3'], '@value' => 'curl']]], - [[], - [['@attributes' => ['min-version' => '2', 'max-version' => '2'], '@value' => 'curl']]], - [[], - ['@attributes' => ['min-version' => '2', 'max-version' => '2'], '@value' => 'curl']], - ]; - } - - /** - * @return array - */ - public static function providesCommands(): array { - return [ - [[], null], - // grep is known on linux - [[], [['@attributes' => ['os' => 'Linux'], '@value' => 'grep']]], - // grepp is not known on linux - [['The command line tool grepp could not be found'], [['@attributes' => ['os' => 'Linux'], '@value' => 'grepp']]], - // we don't care about tools on Windows - we are on Linux - [[], [['@attributes' => ['os' => 'Windows'], '@value' => 'grepp']]], - // grep is known on all systems - [[], 'grep'], - [[], ['@attributes' => ['os' => 'Linux'], '@value' => 'grep']], - ]; - } - - /** - * @return array - */ - public static function providesDatabases(): array { - return [ - // non BC - in case on databases are defined -> all are supported - [[], null], - [[], []], - [['The following databases are supported: mongodb'], 'mongodb'], - [['The following databases are supported: sqlite, postgres'], ['sqlite', ['@value' => 'postgres']]], - ]; - } - - /** - * @return array - */ - public static function providesPhpVersion(): array { - return [ - [[], null, null, null], - [[], '5.4', null, null], - [[], null, '5.5', null], - [[], '5.4', '5.5', null], - [['PHP 5.4.4 or higher is required.'], '5.4.4', null, null], - [['PHP with a version lower than 5.4.2 is required.'], null, '5.4.2', null], - [['64bit or higher PHP required.'], null, null, 64], - [[], '5.4', '5.4', null], - ]; - } - - public static function appVersionsProvider(): array { - return [ - // exact match - [ - '6.0.0.0', - [ - 'requiremin' => '6.0', - 'requiremax' => '6.0', - ], - true - ], - // in-between match - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - ], - true - ], - // app too old - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '5.0', - ], - false - ], - // app too new - [ - '5.0.0.0', - [ - 'requiremin' => '6.0', - 'requiremax' => '6.0', - ], - false - ], - // only min specified - [ - '6.0.0.0', - [ - 'requiremin' => '6.0', - ], - true - ], - // only min specified fail - [ - '5.0.0.0', - [ - 'requiremin' => '6.0', - ], - false - ], - // only min specified legacy - [ - '6.0.0.0', - [ - 'require' => '6.0', - ], - true - ], - // only min specified legacy fail - [ - '4.0.0.0', - [ - 'require' => '6.0', - ], - false - ], - // only max specified - [ - '5.0.0.0', - [ - 'requiremax' => '6.0', - ], - true - ], - // only max specified fail - [ - '7.0.0.0', - [ - 'requiremax' => '6.0', - ], - false - ], - // variations of versions - // single OC number - [ - '4', - [ - 'require' => '4.0', - ], - true - ], - // multiple OC number - [ - '4.3.1', - [ - 'require' => '4.3', - ], - true - ], - // single app number - [ - '4', - [ - 'require' => '4', - ], - true - ], - // single app number fail - [ - '4.3', - [ - 'require' => '5', - ], - false - ], - // complex - [ - '5.0.0', - [ - 'require' => '4.5.1', - ], - true - ], - // complex fail - [ - '4.3.1', - [ - 'require' => '4.3.2', - ], - false - ], - // two numbers - [ - '4.3.1', - [ - 'require' => '4.4', - ], - false - ], - // one number fail - [ - '4.3.1', - [ - 'require' => '5', - ], - false - ], - // pre-alpha app - [ - '5.0.3', - [ - 'require' => '4.93', - ], - true - ], - // pre-alpha OC - [ - '6.90.0.2', - [ - 'require' => '6.90', - ], - true - ], - // pre-alpha OC max - [ - '6.90.0.2', - [ - 'requiremax' => '7', - ], - true - ], - // expect same major number match - [ - '5.0.3', - [ - 'require' => '5', - ], - true - ], - // expect same major number match - [ - '5.0.3', - [ - 'requiremax' => '5', - ], - true - ], - // dependencies versions before require* - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '7.0', - 'max-version' => '7.0', - ], - ], - ], - ], - false - ], - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '5.0', - 'max-version' => '5.0', - ], - ], - ], - ], - false - ], - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '5.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '5.0', - 'max-version' => '7.0', - ], - ], - ], - ], - true - ], - [ - '9.2.0.0', - [ - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '9.0', - 'max-version' => '9.1', - ], - ], - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9.1', - 'max-version' => '9.2', - ], - ], - ], - ], - true - ], - [ - '9.2.0.0', - [ - 'dependencies' => [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9.1', - 'max-version' => '9.2', - ], - ], - ], - ], - true - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('appVersionsProvider')] - public function testServerVersion($ncVersion, $appInfo, $expectedResult): void { - $this->assertEquals($expectedResult, count($this->analyser->analyzeServerVersion($ncVersion, $appInfo, false)) === 0); - } -} --- a/tests/lib/App/InfoParserTest.php +++ /dev/null @@ -1,130 +0,0 @@ -parse(OC::$SERVERROOT . "/tests/data/app/$xmlFile"); - - $this->assertEquals($expectedData, $data); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesInfoXml')] - public function testParsingValidXmlWithoutCache($expectedJson, $xmlFile): void { - $this->parserTest($expectedJson, $xmlFile); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesInfoXml')] - public function testParsingValidXmlWithCache($expectedJson, $xmlFile): void { - $this->parserTest($expectedJson, $xmlFile, self::$cache); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('appDataProvider')] - public function testApplyL10N(array $data, array $expected, string $language): void { - $parser = new InfoParser(); - $this->assertEqualsCanonicalizing($expected, $parser->applyL10N($data, $language)); - } - - public static function providesInfoXml(): array { - return [ - 'Only one value in each list' => ['appinfo-multi-once.json', 'appinfo-multi-once.xml'], - 'Only one value in each list with attributes' => ['appinfo-attributes-once.json', 'appinfo-attributes-once.xml'], - 'Multiple values in each list' => ['appinfo-multi-twice.json', 'appinfo-multi-twice.xml'], - 'Valid info' => ['expected-info.json', 'valid-info.xml'], - 'Invalid info' => [null, 'invalid-info.xml'], - 'Navigation one item' => ['navigation-one-item.json', 'navigation-one-item.xml'], - 'Navigation two items' => ['navigation-two-items.json', 'navigation-two-items.xml'], - 'Various single item' => ['various-single-item.json', 'various-single-item.xml'], - ]; - } - - /** - * Providers for the app data values - */ - public static function appDataProvider(): array { - $FULL_TRANSLATED = [ - 'name' => [ - ['@attributes' => ['lang' => 'en'], '@value' => 'App'], - ['@attributes' => ['lang' => 'fr'], '@value' => 'Application'] - ], - 'summary' => [ - 'Summary', - ['@attributes' => ['lang' => 'fr'], '@value' => 'Résumé'] - ], - 'description' => [ - ['@attributes' => ['lang' => 'en'], '@value' => 'Description'], - ['@attributes' => ['lang' => 'fr'], '@value' => 'Description (fr)'] - ] - ]; - - return [ - // test trimming - [ - ['description' => " \t This is a multiline \n test with \n \t \n \n some new lines "], - ['description' => "This is a multiline \n test with \n \t \n \n some new lines", 'summary' => '', 'name' => ''], - 'en' - ], - [ - ['description' => " \t This is a multiline \n test with \n \t some new lines "], - ['description' => "This is a multiline \n test with \n \t some new lines", 'summary' => '', 'name' => ''], - 'en' - ], - [ - ['description' => hex2bin('5065726d657420646520732761757468656e7469666965722064616e732070697769676f20646972656374656d656e74206176656320736573206964656e74696669616e7473206f776e636c6f75642073616e73206c65732072657461706572206574206d657420c3a0206a6f757273206365757820636920656e20636173206465206368616e67656d656e74206465206d6f742064652070617373652e0d0a0d')], - ['description' => "Permet de s'authentifier dans piwigo directement avec ses identifiants owncloud sans les retaper et met à jours ceux ci en cas de changement de mot de passe.", 'summary' => '', 'name' => ''], - 'fr' - ], - // test proper translation handling - // just strings: - [ - ['name' => 'App', 'summary' => 'Summary', 'description' => 'Description'], - ['name' => 'App', 'summary' => 'Summary', 'description' => 'Description'], - 'en' - ], - // translated and requesting English: - [ - $FULL_TRANSLATED, - ['name' => 'App', 'summary' => 'Summary', 'description' => 'Description'], - 'en' - ], - // translated and requesting translation: - [ - $FULL_TRANSLATED, - ['name' => 'Application', 'summary' => 'Résumé', 'description' => 'Description (fr)'], - 'fr' - ], - // translated but requesting non existing translation, should fallback to English: - [ - $FULL_TRANSLATED, - ['name' => 'App', 'summary' => 'Summary', 'description' => 'Description'], - 'de' - ] - ]; - } -} --- a/tests/lib/App/PlatformRepositoryTest.php +++ /dev/null @@ -1,62 +0,0 @@ -normalizeVersion($input); - $this->assertEquals($expected, $normalizedVersion); - } - - public static function providesVersions(): array { - return [ - 'none' => ['1.0.0', '1.0.0.0'], - 'none/2' => ['1.2.3.4', '1.2.3.4'], - 'parses state' => ['1.0.0RC1dev', '1.0.0.0-RC1-dev'], - 'CI parsing' => ['1.0.0-rC15-dev', '1.0.0.0-RC15-dev'], - 'delimiters' => ['1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'], - 'RC uppercase' => ['1.0.0-rc1', '1.0.0.0-RC1'], - 'patch replace' => ['1.0.0.pl3-dev', '1.0.0.0-patch3-dev'], - 'forces w.x.y.z' => ['1.0-dev', '1.0.0.0-dev'], - 'forces w.x.y.z/2' => ['0', '0.0.0.0'], - 'parses long' => ['10.4.13-beta', '10.4.13.0-beta'], - 'parses long/2' => ['10.4.13beta2', '10.4.13.0-beta2'], - 'parses long/semver' => ['10.4.13beta.2', '10.4.13.0-beta2'], - 'expand shorthand' => ['10.4.13-b', '10.4.13.0-beta'], - 'expand shorthand2' => ['10.4.13-b5', '10.4.13.0-beta5'], - 'strips leading v' => ['v1.0.0', '1.0.0.0'], - 'strips v/datetime' => ['v20100102', '20100102'], - 'parses dates y-m' => ['2010.01', '2010-01'], - 'parses dates w/ .' => ['2010.01.02', '2010-01-02'], - 'parses dates w/ -' => ['2010-01-02', '2010-01-02'], - 'parses numbers' => ['2010-01-02.5', '2010-01-02-5'], - 'parses dates y.m.Y' => ['2010.1.555', '2010.1.555.0'], - 'parses datetime' => ['20100102-203040', '20100102-203040'], - 'parses dt+number' => ['20100102203040-10', '20100102203040-10'], - 'parses dt+patch' => ['20100102-203040-p1', '20100102-203040-patch1'], - 'parses master' => ['dev-master', '9999999-dev'], - 'parses trunk' => ['dev-trunk', '9999999-dev'], - // 'parses branches' => array('1.x-dev', '1.9999999.9999999.9999999-dev'), - 'parses arbitrary' => ['dev-feature-foo', 'dev-feature-foo'], - 'parses arbitrary2' => ['DEV-FOOBAR', 'dev-FOOBAR'], - 'parses arbitrary3' => ['dev-feature/foo', 'dev-feature/foo'], - 'ignores aliases' => ['dev-master as 1.0.0', '9999999-dev'], - // 'semver metadata' => array('dev-master+foo.bar', '9999999-dev'), - // 'semver metadata/2' => array('1.0.0-beta.5+foo', '1.0.0.0-beta5'), - // 'semver metadata/3' => array('1.0.0+foo', '1.0.0.0'), - // 'metadata w/ alias' => array('1.0.0+foo as 2.0', '1.0.0.0'), - ]; - } -} --- a/tests/lib/AppConfigIntegrationTest.php +++ /dev/null @@ -1,1514 +0,0 @@ ->> - * [appId => [configKey, configValue, valueType, lazy, sensitive]] - */ - private static array $baseStruct - = [ - 'testapp' => [ - 'enabled' => ['enabled', 'yes'], - 'installed_version' => ['installed_version', '1.2.3'], - 'depends_on' => ['depends_on', 'someapp'], - 'deletethis' => ['deletethis', 'deletethis'], - 'key' => ['key', 'value'] - ], - 'searchtest' => [ - 'search_key1' => ['search_key1', 'key1', IAppConfig::VALUE_STRING], - 'search_key2' => ['search_key2', 'key2', IAppConfig::VALUE_STRING], - 'search_key3' => ['search_key3', 'key3', IAppConfig::VALUE_STRING], - 'searchnot_key4' => ['searchnot_key4', 'key4', IAppConfig::VALUE_STRING], - 'search_key5_lazy' => ['search_key5_lazy', 'key5', IAppConfig::VALUE_STRING, true], - ], - 'someapp' => [ - 'key' => ['key', 'value'], - 'otherkey' => ['otherkey', 'othervalue'] - ], - '123456' => [ - 'enabled' => ['enabled', 'yes'], - 'key' => ['key', 'value'] - ], - 'anotherapp' => [ - 'enabled' => ['enabled', 'no'], - 'installed_version' => ['installed_version', '3.2.1'], - 'key' => ['key', 'value'] - ], - 'non-sensitive-app' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, false], - 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, false], - ], - 'sensitive-app' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, true], - 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, true], - ], - 'only-lazy' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true] - ], - 'typed' => [ - 'mixed' => ['mixed', 'mix', IAppConfig::VALUE_MIXED], - 'string' => ['string', 'value', IAppConfig::VALUE_STRING], - 'int' => ['int', '42', IAppConfig::VALUE_INT], - 'float' => ['float', '3.14', IAppConfig::VALUE_FLOAT], - 'bool' => ['bool', '1', IAppConfig::VALUE_BOOL], - 'array' => ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], - ], - 'prefix-app' => [ - 'key1' => ['key1', 'value'], - 'prefix1' => ['prefix1', 'value'], - 'prefix-2' => ['prefix-2', 'value'], - 'key-2' => ['key-2', 'value'], - ] - ]; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->connection = Server::get(IDBConnection::class); - $this->config = Server::get(IConfig::class); - $this->configManager = Server::get(ConfigManager::class); - $this->presetManager = Server::get(PresetManager::class); - $this->logger = Server::get(LoggerInterface::class); - $this->crypto = Server::get(ICrypto::class); - $this->cacheFactory = $this->createMock(CacheFactory::class); - $this->cacheFactory->method('isLocalCacheAvailable')->willReturn(false); - - // storing current config and emptying the data table - $sql = $this->connection->getQueryBuilder(); - $sql->select('*') - ->from('appconfig'); - $result = $sql->executeQuery(); - $this->originalConfig = $result->fetchAllAssociative(); - $result->closeCursor(); - - $sql = $this->connection->getQueryBuilder(); - $sql->delete('appconfig'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('appconfig') - ->values( - [ - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'type' => $sql->createParameter('type'), - 'lazy' => $sql->createParameter('lazy') - ] - ); - - foreach (self::$baseStruct as $appId => $appData) { - foreach ($appData as $key => $row) { - $value = $row[1]; - $type = $row[2] ?? IAppConfig::VALUE_MIXED; - if (($row[4] ?? false) === true) { - $type |= IAppConfig::VALUE_SENSITIVE; - if (!isset(self::$baseStruct[$appId][$key]['encrypted'])) { - $value = self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX') . $this->crypto->encrypt($value); - self::$baseStruct[$appId][$key]['encrypted'] = $value; - } else { - $value = self::$baseStruct[$appId][$key]['encrypted']; - } - } - - $sql->setParameters( - [ - 'appid' => $appId, - 'configkey' => $row[0], - 'configvalue' => $value, - 'type' => $type, - 'lazy' => (($row[3] ?? false) === true) ? 1 : 0 - ] - )->executeStatement(); - } - } - } - - #[\Override] - protected function tearDown(): void { - $sql = $this->connection->getQueryBuilder(); - $sql->delete('appconfig'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('appconfig') - ->values( - [ - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'lazy' => $sql->createParameter('lazy'), - 'type' => $sql->createParameter('type'), - ] - ); - - foreach ($this->originalConfig as $key => $configs) { - $sql->setParameter('appid', $configs['appid']) - ->setParameter('configkey', $configs['configkey']) - ->setParameter('configvalue', $configs['configvalue']) - ->setParameter('lazy', ($configs['lazy'] === '1') ? '1' : '0') - ->setParameter('type', $configs['type']); - $sql->executeStatement(); - } - - // $this->restoreService(AppConfig::class); - parent::tearDown(); - } - - /** - * @param bool $preLoading TRUE will preload the 'fast' cache, which is the normal behavior of usual - * IAppConfig - * - * @return IAppConfig - */ - private function generateAppConfig(bool $preLoading = true): IAppConfig { - /** @var AppConfig $config */ - $config = new AppConfig( - $this->connection, - $this->config, - $this->configManager, - $this->presetManager, - $this->logger, - $this->crypto, - $this->cacheFactory, - ); - $msg = ' generateAppConfig() failed to confirm cache status'; - - // confirm cache status - $status = $config->statusCache(); - $this->assertSame(false, $status['fastLoaded'], $msg); - $this->assertSame(false, $status['lazyLoaded'], $msg); - $this->assertSame([], $status['fastCache'], $msg); - $this->assertSame([], $status['lazyCache'], $msg); - if ($preLoading) { - // simple way to initiate the load of non-lazy config values in cache - $config->getValueString('core', 'preload', ''); - - // confirm cache status - $status = $config->statusCache(); - $this->assertSame(true, $status['fastLoaded'], $msg); - $this->assertSame(false, $status['lazyLoaded'], $msg); - - $apps = array_values(array_diff(array_keys(self::$baseStruct), ['only-lazy'])); - $this->assertEqualsCanonicalizing($apps, array_keys($status['fastCache']), $msg); - $this->assertSame([], array_keys($status['lazyCache']), $msg); - } - - return $config; - } - - public function testGetApps(): void { - $config = $this->generateAppConfig(false); - - $this->assertEqualsCanonicalizing(array_keys(self::$baseStruct), $config->getApps()); - } - - public function testGetAppInstalledVersions(): void { - $config = $this->generateAppConfig(false); - - $this->assertEquals( - ['testapp' => '1.2.3', 'anotherapp' => '3.2.1'], - $config->getAppInstalledVersions(false) - ); - $this->assertEquals( - ['testapp' => '1.2.3'], - $config->getAppInstalledVersions(true) - ); - } - - /** - * returns list of app and their keys - * - * @return array ['appId' => ['key1', 'key2', ]] - * @see testGetKeys - */ - public static function providerGetAppKeys(): array { - $appKeys = []; - foreach (self::$baseStruct as $appId => $appData) { - $keys = []; - foreach ($appData as $row) { - $keys[] = $row[0]; - } - $appKeys[] = [(string)$appId, $keys]; - } - - return $appKeys; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppKeys')] - public function testGetKeys(string $appId, array $expectedKeys): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing($expectedKeys, $config->getKeys($appId)); - $this->assertEqualsCanonicalizing($expectedKeys, array_keys($config->getAllValues($appId, ''))); - } - - public function testGetKeysOnUnknownAppShouldReturnsEmptyArray(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing([], $config->getKeys('unknown-app')); - } - - public function testHasKeyOnNonExistentKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertEquals(false, $config->hasKey(array_keys(self::$baseStruct)[0], 'inexistant-key')); - } - - public function testHasKeyOnUnknownAppReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertEquals(false, $config->hasKey('inexistant-app', 'inexistant-key')); - } - - public function testHasKeyOnMistypedAsLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->hasKey('non-sensitive-app', 'non-lazy-key', true)); - } - - public function testHasKeyOnMistypeAsNonLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->hasKey('non-sensitive-app', 'lazy-key', false)); - } - - public function testHasKeyOnMistypeAsNonLazyReturnsTrueWithLazyArgumentIsNull(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->hasKey('non-sensitive-app', 'lazy-key', null)); - } - - /** - * returns list of config keys - * - * @return array [appId, key, value, type, lazy, sensitive] - * @see testIsSensitiveLazyHasKey - */ - public static function providerGetKeys(): array { - $appKeys = []; - foreach (self::$baseStruct as $appId => $appData) { - foreach ($appData as $row) { - $appKeys[] = [ - (string)$appId, $row[0], $row[1], $row[2] ?? IAppConfig::VALUE_MIXED, $row[3] ?? false, - $row[4] ?? false - ]; - } - } - - return $appKeys; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] - public function testIsSensitiveLazyHasKey( - string $appId, string $configKey, string $configValue, int $type, bool $lazy, bool $sensitive, - ): void { - $config = $this->generateAppConfig(); - - $this->assertEquals($sensitive, $config->isSensitive($appId, $configKey, $lazy)); - - $this->assertEquals($lazy, $config->isLazy($appId, $configKey)); - - $this->assertEquals(true, $config->hasKey($appId, $configKey, $lazy)); - } - - public function testIsSensitiveOnNonExistentKeyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive(array_keys(self::$baseStruct)[0], 'inexistant-key'); - } - - public function testIsSensitiveOnUnknownAppThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('unknown-app', 'inexistant-key'); - } - - public function testIsSensitiveOnSensitiveMistypedAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->isSensitive('sensitive-app', 'non-lazy-key', true)); - } - - public function testIsSensitiveOnNonSensitiveMistypedAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->isSensitive('non-sensitive-app', 'non-lazy-key', true)); - } - - public function testIsSensitiveOnSensitiveMistypedAsNonLazyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('sensitive-app', 'lazy-key', false); - } - - public function testIsSensitiveOnNonSensitiveMistypedAsNonLazyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('non-sensitive-app', 'lazy-key', false); - } - - public function testIsLazyOnNonExistentKeyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isLazy(array_keys(self::$baseStruct)[0], 'inexistant-key'); - } - - public function testIsLazyOnUnknownAppThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isLazy('unknown-app', 'inexistant-key'); - } - - public function testGetAllValues(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'array' => ['test' => 1], - 'bool' => true, - 'float' => 3.14, - 'int' => 42, - 'mixed' => 'mix', - 'string' => 'value', - ], - $config->getAllValues('typed') - ); - } - - public function testGetAllValuesWithEmptyApp(): void { - $config = $this->generateAppConfig(); - $this->expectException(InvalidArgumentException::class); - $config->getAllValues(''); - } - - public function testGetAllValuesWithPrefix(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['prefix1', 'prefix-2'], array_keys($config->getAllValues('prefix-app', 'prefix'))); - } - - public function testSearchValues(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['testapp' => 'yes', '123456' => 'yes', 'anotherapp' => 'no'], $config->searchValues('enabled')); - } - - public function testGetValueString(): void { - $config = $this->generateAppConfig(); - $this->assertSame('value', $config->getValueString('typed', 'string', '')); - } - - public function testGetValueStringOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame('default-1', $config->getValueString('typed-1', 'string', 'default-1')); - } - - public function testGetValueStringOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame('default-2', $config->getValueString('typed', 'string-2', 'default-2')); - } - - public function testGetValueStringOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueString('typed', 'int'); - } - - public function testGetNonLazyValueStringAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame('value', $config->getValueString('non-sensitive-app', 'non-lazy-key', 'default', lazy: true)); - } - - public function testGetValueInt(): void { - $config = $this->generateAppConfig(); - $this->assertSame(42, $config->getValueInt('typed', 'int', 0)); - } - - public function testGetValueIntOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(1, $config->getValueInt('typed-1', 'int', 1)); - } - - public function testGetValueIntOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(2, $config->getValueInt('typed', 'int-2', 2)); - } - - public function testGetValueIntOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueInt('typed', 'float'); - } - - public function testGetValueFloat(): void { - $config = $this->generateAppConfig(); - $this->assertSame(3.14, $config->getValueFloat('typed', 'float', 0)); - } - - public function testGetValueFloatOnNonUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(1.11, $config->getValueFloat('typed-1', 'float', 1.11)); - } - - public function testGetValueFloatOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(2.22, $config->getValueFloat('typed', 'float-2', 2.22)); - } - - public function testGetValueFloatOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueFloat('typed', 'bool'); - } - - public function testGetValueBool(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->getValueBool('typed', 'bool')); - } - - /** - * Untyped values predate the typed config API and are still in the database of - * every upgraded instance, so they have to keep resolving to a boolean. The - * deprecated setter is used on purpose, as it is the only way to write a value - * without a type. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataUntypedBool')] - public function testGetValueBoolOnUntypedValue(string $stored, bool $expected): void { - /** @var AppConfig $config */ - $config = $this->generateAppConfig(); - $config->setValue('feed', 'untyped-bool', $stored); - - $this->assertSame($expected, $config->getValueBool('feed', 'untyped-bool')); - } - - public static function dataUntypedBool(): array { - return [ - 'yes' => ['yes', true], - 'no' => ['no', false], - 'true' => ['true', true], - 'false' => ['false', false], - 'on' => ['on', true], - '1' => ['1', true], - '0' => ['0', false], - 'empty' => ['', false], - ]; - } - - public function testGetValueBoolOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->getValueBool('typed-1', 'bool', false)); - } - - public function testGetValueBoolOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->getValueBool('typed', 'bool-2')); - } - - public function testGetValueBoolOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueBool('typed', 'array'); - } - - public function testGetValueArray(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('typed', 'array', [])); - } - - public function testGetValueArrayOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame([1], $config->getValueArray('typed-1', 'array', [1])); - } - - public function testGetValueArrayOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame([1, 2], $config->getValueArray('typed', 'array-2', [1, 2])); - } - - public function testGetValueArrayOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueArray('typed', 'string'); - } - - /** - * @return array - * @see testGetValueType - * - * @see testGetValueMixed - */ - public static function providerGetValueMixed(): array { - return [ - // key, value, type - ['mixed', 'mix', IAppConfig::VALUE_MIXED], - ['string', 'value', IAppConfig::VALUE_STRING], - ['int', '42', IAppConfig::VALUE_INT], - ['float', '3.14', IAppConfig::VALUE_FLOAT], - ['bool', '1', IAppConfig::VALUE_BOOL], - ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], - ]; - } - - /** - * - * @param string $key - * @param string $value - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] - public function testGetValueMixed(string $key, string $value): void { - $config = $this->generateAppConfig(); - $this->assertSame($value, $config->getValueMixed('typed', $key)); - } - - /** - * - * @param string $key - * @param string $value - * @param int $type - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] - public function testGetValueType(string $key, string $value, int $type): void { - $config = $this->generateAppConfig(); - $this->assertSame($type, $config->getValueType('typed', $key)); - } - - public function testGetValueTypeOnUnknownApp(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->getValueType('typed-1', 'string'); - } - - public function testGetValueTypeOnNonExistentKey(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->getValueType('typed', 'string-2'); - } - - public function testSetValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $status = $config->statusCache(); - $this->assertSame('value-1', $status['fastCache']['feed']['string']); - } - - public function testSetValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueStringIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame(true, $config->setValueString('feed', 'string', 'value-2')); - } - - public function testSetValueStringIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame(false, $config->setValueString('feed', 'string', 'value-1')); - } - - public function testSetValueStringIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->setValueString('feed', 'string', 'value-2'); - $status = $config->statusCache(); - $this->assertSame('value-2', $status['fastCache']['feed']['string']); - } - - public function testSetValueStringIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->setValueString('feed', 'string', 'value-2'); - $config->clearCache(); - $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $status = $config->statusCache(); - $this->assertSame('42', $status['fastCache']['feed']['int']); - } - - public function testSetValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueIntIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(true, $config->setValueInt('feed', 'int', 17)); - } - - public function testSetValueIntIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(false, $config->setValueInt('feed', 'int', 42)); - } - - public function testSetValueIntIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 17); - $status = $config->statusCache(); - $this->assertSame('17', $status['fastCache']['feed']['int']); - } - - public function testSetValueIntIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 17); - $config->clearCache(); - $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $status = $config->statusCache(); - $this->assertSame('3.14', $status['fastCache']['feed']['float']); - } - - public function testSetValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueFloatIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(true, $config->setValueFloat('feed', 'float', 1.23)); - } - - public function testSetValueFloatIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(false, $config->setValueFloat('feed', 'float', 3.14)); - } - - public function testSetValueFloatIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 1.23); - $status = $config->statusCache(); - $this->assertSame('1.23', $status['fastCache']['feed']['float']); - } - - public function testSetValueFloatIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 1.23); - $config->clearCache(); - $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueBool(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $status = $config->statusCache(); - $this->assertSame('1', $status['fastCache']['feed']['bool']); - } - - public function testSetValueBoolDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->clearCache(); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(true, $config->setValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(false, $config->setValueBool('feed', 'bool', true)); - } - - public function testSetValueBoolIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->setValueBool('feed', 'bool', false); - $status = $config->statusCache(); - $this->assertSame('0', $status['fastCache']['feed']['bool']); - } - - public function testSetValueBoolIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->setValueBool('feed', 'bool', false); - $config->clearCache(); - $this->assertSame(false, $config->getValueBool('feed', 'bool', true)); - } - - public function testSetValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $status = $config->statusCache(); - $this->assertSame('{"test":1}', $status['fastCache']['feed']['array']); - } - - public function testSetValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->clearCache(); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetValueArrayIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(true, $config->setValueArray('feed', 'array', ['test' => 2])); - } - - public function testSetValueArrayIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(false, $config->setValueArray('feed', 'array', ['test' => 1])); - } - - public function testSetValueArrayIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 2]); - $status = $config->statusCache(); - $this->assertSame('{"test":2}', $status['fastCache']['feed']['array']); - } - - public function testSetValueArrayIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 2]); - $config->clearCache(); - $this->assertSame(['test' => 2], $config->getValueArray('feed', 'array', [])); - } - - public function testSetLazyValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetLazyValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $status = $config->statusCache(); - $this->assertSame('value-1', $status['lazyCache']['feed']['string']); - } - - public function testSetLazyValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetLazyValueStringAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $config->setValueString('feed', 'string', 'value-1', false); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetNonLazyValueStringAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', false); - $config->setValueString('feed', 'string', 'value-1', true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetSensitiveValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetSensitiveValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['string']); - } - - public function testSetSensitiveValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetNonSensitiveValueStringAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: false); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'string')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetSensitiveValueStringAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->setValueString('feed', 'string', 'value-2', sensitive: false); - $this->assertSame(true, $config->isSensitive('feed', 'string')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetSensitiveValueStringAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->setValueString('feed', 'string', 'value-2', sensitive: false); - $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetLazyValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetLazyValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $status = $config->statusCache(); - $this->assertSame('42', $status['lazyCache']['feed']['int']); - } - - public function testSetLazyValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetLazyValueIntAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $config->setValueInt('feed', 'int', 42, false); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetNonLazyValueIntAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, false); - $config->setValueInt('feed', 'int', 42, true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetSensitiveValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetSensitiveValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['int']); - } - - public function testSetSensitiveValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetNonSensitiveValueIntAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'int')); - } - - public function testSetSensitiveValueIntAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->setValueInt('feed', 'int', 17); - $this->assertSame(true, $config->isSensitive('feed', 'int')); - } - - public function testSetSensitiveValueIntAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->setValueInt('feed', 'int', 17); - $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetLazyValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetLazyValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $status = $config->statusCache(); - $this->assertSame('3.14', $status['lazyCache']['feed']['float']); - } - - public function testSetLazyValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetLazyValueFloatAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $config->setValueFloat('feed', 'float', 3.14, false); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetNonLazyValueFloatAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, false); - $config->setValueFloat('feed', 'float', 3.14, true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetSensitiveValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetSensitiveValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['float']); - } - - public function testSetSensitiveValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetNonSensitiveValueFloatAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'float')); - } - - public function testSetSensitiveValueFloatAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->setValueFloat('feed', 'float', 1.23); - $this->assertSame(true, $config->isSensitive('feed', 'float')); - } - - public function testSetSensitiveValueFloatAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->setValueFloat('feed', 'float', 1.23); - $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetLazyValueBool(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueBoolCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $status = $config->statusCache(); - $this->assertSame('1', $status['lazyCache']['feed']['bool']); - } - - public function testSetLazyValueBoolDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $config->clearCache(); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueBoolAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $config->setValueBool('feed', 'bool', true, false); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetNonLazyValueBoolAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, false); - $config->setValueBool('feed', 'bool', true, true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - public function testSetLazyValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $status = $config->statusCache(); - $this->assertSame('{"test":1}', $status['lazyCache']['feed']['array']); - } - - public function testSetLazyValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $config->clearCache(); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - public function testSetLazyValueArrayAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $config->setValueArray('feed', 'array', ['test' => 1], false); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetNonLazyValueArrayAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], false); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - public function testSetSensitiveValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetSensitiveValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['array']); - } - - public function testSetSensitiveValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->clearCache(); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetNonSensitiveValueArrayAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'array')); - } - - public function testSetSensitiveValueArrayAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->setValueArray('feed', 'array', ['test' => 2]); - $this->assertSame(true, $config->isSensitive('feed', 'array')); - } - - public function testSetSensitiveValueArrayAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->setValueArray('feed', 'array', ['test' => 2]); - $this->assertEqualsCanonicalizing(['test' => 2], $config->getValueArray('feed', 'array', [])); - } - - public function testUpdateNotSensitiveToSensitive(): void { - $config = $this->generateAppConfig(); - $config->updateSensitive('non-sensitive-app', 'lazy-key', true); - $this->assertSame(true, $config->isSensitive('non-sensitive-app', 'lazy-key', true)); - } - - public function testUpdateSensitiveToNotSensitive(): void { - $config = $this->generateAppConfig(); - $config->updateSensitive('sensitive-app', 'lazy-key', false); - $this->assertSame(false, $config->isSensitive('sensitive-app', 'lazy-key', true)); - } - - public function testUpdateSensitiveToSensitiveReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('sensitive-app', 'lazy-key', true)); - } - - public function testUpdateNotSensitiveToNotSensitiveReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'lazy-key', false)); - } - - public function testUpdateSensitiveOnUnknownKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'unknown-key', true)); - } - - public function testUpdateNotLazyToLazy(): void { - $config = $this->generateAppConfig(); - $config->updateLazy('non-sensitive-app', 'non-lazy-key', true); - $this->assertSame(true, $config->isLazy('non-sensitive-app', 'non-lazy-key')); - } - - public function testUpdateLazyToNotLazy(): void { - $config = $this->generateAppConfig(); - $config->updateLazy('non-sensitive-app', 'lazy-key', false); - $this->assertSame(false, $config->isLazy('non-sensitive-app', 'lazy-key')); - } - - public function testUpdateLazyToLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'lazy-key', true)); - } - - public function testUpdateNotLazyToNotLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'non-lazy-key', false)); - } - - public function testUpdateLazyOnUnknownKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'unknown-key', true)); - } - - public function testGetDetails(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'non-sensitive-app', - 'key' => 'lazy-key', - 'value' => 'value', - 'type' => 4, - 'lazy' => true, - 'typeString' => 'string', - 'sensitive' => false, - ], - $config->getDetails('non-sensitive-app', 'lazy-key') - ); - } - - public function testGetDetailsSensitive(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'sensitive-app', - 'key' => 'lazy-key', - 'value' => 'value', - 'type' => 4, - 'lazy' => true, - 'typeString' => 'string', - 'sensitive' => true, - ], - $config->getDetails('sensitive-app', 'lazy-key') - ); - } - - public function testGetDetailsInt(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'int', - 'value' => '42', - 'type' => 8, - 'lazy' => false, - 'typeString' => 'integer', - 'sensitive' => false - ], - $config->getDetails('typed', 'int') - ); - } - - public function testGetDetailsFloat(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'float', - 'value' => '3.14', - 'type' => 16, - 'lazy' => false, - 'typeString' => 'float', - 'sensitive' => false - ], - $config->getDetails('typed', 'float') - ); - } - - public function testGetDetailsBool(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'bool', - 'value' => '1', - 'type' => 32, - 'lazy' => false, - 'typeString' => 'boolean', - 'sensitive' => false - ], - $config->getDetails('typed', 'bool') - ); - } - - public function testGetDetailsArray(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'array', - 'value' => '{"test": 1}', - 'type' => 64, - 'lazy' => false, - 'typeString' => 'array', - 'sensitive' => false - ], - $config->getDetails('typed', 'array') - ); - } - - public function testDeleteKey(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - } - - public function testDeleteKeyCache(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $status = $config->statusCache(); - $this->assertEqualsCanonicalizing(['enabled' => 'no', 'installed_version' => '3.2.1'], $status['fastCache']['anotherapp']); - } - - public function testDeleteKeyDatabase(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $config->clearCache(); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - } - - public function testDeleteApp(): void { - $config = $this->generateAppConfig(); - $config->deleteApp('anotherapp'); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); - } - - public function testDeleteAppCache(): void { - $config = $this->generateAppConfig(); - $status = $config->statusCache(); - $this->assertSame(true, isset($status['fastCache']['anotherapp'])); - $config->deleteApp('anotherapp'); - $status = $config->statusCache(); - $this->assertSame(false, isset($status['fastCache']['anotherapp'])); - } - - public function testDeleteAppDatabase(): void { - $config = $this->generateAppConfig(); - $config->deleteApp('anotherapp'); - $config->clearCache(); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); - } - - public function testClearCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', '123454'); - $config->clearCache(); - $status = $config->statusCache(); - $this->assertSame([], $status['fastCache']); - } - - public function testSensitiveValuesAreEncrypted(): void { - $key = self::getUniqueID('secret'); - - $appConfig = $this->generateAppConfig(); - $secret = md5((string)time()); - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Can get in same run - $actualSecret = $appConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - } - - public function testMigratingNonSensitiveValueToSensitiveWithSetValue(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Unencrypted - $appConfig->setValueString('testapp', $key, $secret); - $this->assertConfigKey('testapp', $key, $secret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Encrypting on change - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Can get in same run - $actualSecret = $appConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - } - - public function testUpdateSensitiveValueToNonSensitiveWithUpdateSensitive(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Encrypted - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Migrate to non-sensitive / non-encrypted - $appConfig->updateSensitive('testapp', $key, false); - $this->assertConfigKey('testapp', $key, $secret); - } - - public function testUpdateNonSensitiveValueToSensitiveWithUpdateSensitive(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Unencrypted - $appConfig->setValueString('testapp', $key, $secret); - $this->assertConfigKey('testapp', $key, $secret); - - // Migrate to sensitive / encrypted - $appConfig->updateSensitive('testapp', $key, true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - } - - public function testSearchKeyNoLazyLoading(): void { - $appConfig = $this->generateAppConfig(); - $appConfig->searchKeys('searchtest', 'search_'); - $status = $appConfig->statusCache(); - $this->assertFalse($status['lazyLoaded'], 'searchKeys() loaded lazy config'); - } - - public function testSearchKeyFast(): void { - $appConfig = $this->generateAppConfig(); - $this->assertEquals(['search_key1', 'search_key2', 'search_key3'], $appConfig->searchKeys('searchtest', 'search_')); - } - - public function testSearchKeyLazy(): void { - $appConfig = $this->generateAppConfig(); - $this->assertEquals(['search_key5_lazy'], $appConfig->searchKeys('searchtest', 'search_', true)); - } - - protected function loadConfigValueFromDatabase(string $app, string $key): string|false { - $sql = $this->connection->getQueryBuilder(); - $sql->select('configvalue') - ->from('appconfig') - ->where($sql->expr()->eq('appid', $sql->createParameter('appid'))) - ->andWhere($sql->expr()->eq('configkey', $sql->createParameter('configkey'))) - ->setParameter('appid', $app) - ->setParameter('configkey', $key); - $query = $sql->executeQuery(); - $actual = $query->fetchOne(); - $query->closeCursor(); - - return $actual; - } - - protected function assertConfigKey(string $app, string $key, string|false $expected): void { - $this->assertEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); - } - - protected function assertConfigValueNotEquals(string $app, string $key, string|false $expected): void { - $this->assertNotEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); - } -} --- a/tests/lib/AppConfigMigrationFallbackTest.php +++ /dev/null @@ -1,186 +0,0 @@ -connection = $this->createMock(IDBConnection::class); - $this->config = $this->createMock(IConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->presetManager = $this->createMock(PresetManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->cacheFactory = $this->createMock(CacheFactory::class); - } - - private function getAppConfig(): AppConfig { - $this->config->method('getSystemValueBool') - ->with('cache_app_config', true) - ->willReturn(true); - $this->cacheFactory->method('isLocalCacheAvailable')->willReturn(false); - - return new AppConfig( - $this->connection, - $this->config, - $this->configManager, - $this->presetManager, - $this->logger, - $this->crypto, - $this->cacheFactory, - ); - } - - private function createInvalidFieldNameException(): DBException { - $driverException = $this->createMock(\Doctrine\DBAL\Driver\Exception::class); - $dbalException = new InvalidFieldNameException($driverException, null); - return DbalException::wrap($dbalException); - } - - private function createMockQueryBuilder(): IQueryBuilder&MockObject { - $expression = $this->createMock(IExpressionBuilder::class); - $qb = $this->createMock(IQueryBuilder::class); - $qb->method('from')->willReturn($qb); - $qb->method('select')->willReturn($qb); - $qb->method('addSelect')->willReturn($qb); - $qb->method('where')->willReturn($qb); - $qb->method('andWhere')->willReturn($qb); - $qb->method('set')->willReturn($qb); - $qb->method('expr')->willReturn($expression); - $qb->method('insert')->willReturn($qb); - $qb->method('setValue')->willReturn($qb); - $qb->method('createNamedParameter')->willReturn('?'); - return $qb; - } - - /** - * Test that loadConfig retries without type/lazy columns on InvalidFieldNameException. - */ - public function testLoadConfigFallsBackOnMissingColumns(): void { - $exception = $this->createInvalidFieldNameException(); - - $successResult = $this->createMock(IResult::class); - $successResult->method('fetchAllAssociative')->willReturn([ - ['appid' => 'core', 'configkey' => 'vendor', 'configvalue' => 'owncloud'], - ['appid' => 'core', 'configkey' => 'installedat', 'configvalue' => '1234567890'], - ]); - - $qb = $this->createMockQueryBuilder(); - // First call throws (columns missing), second call succeeds (fallback query) - $qb->method('executeQuery') - ->willReturnOnConsecutiveCalls( - $this->throwException($exception), - $successResult, - ); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $appConfig = $this->getAppConfig(); - - // getValueString triggers loadConfig internally - $value = $appConfig->getValueString('core', 'vendor'); - $this->assertSame('owncloud', $value); - } - - /** - * Test that non-INVALID_FIELD_NAME exceptions are re-thrown, not swallowed. - */ - public function testLoadConfigRethrowsOtherExceptions(): void { - $driverException = $this->createMock(\Doctrine\DBAL\Driver\Exception::class); - $dbalException = new \Doctrine\DBAL\Exception\SyntaxErrorException($driverException, null); - $exception = DbalException::wrap($dbalException); - - $qb = $this->createMockQueryBuilder(); - $qb->method('executeQuery')->willThrowException($exception); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $appConfig = $this->getAppConfig(); - - $this->expectException(DBException::class); - $appConfig->getValueString('core', 'vendor'); - } - - /** - * Test that insert omits lazy/type columns when migration is not completed. - */ - public function testInsertOmitsNewColumnsInFallbackMode(): void { - $exception = $this->createInvalidFieldNameException(); - - $loadResult = $this->createMock(IResult::class); - $loadResult->method('fetchAllAssociative')->willReturn([]); - - $qb = $this->createMockQueryBuilder(); - - $callCount = 0; - $qb->method('executeQuery') - ->willReturnCallback(function () use ($exception, $loadResult, &$callCount) { - $callCount++; - if ($callCount === 1) { - throw $exception; - } - return $loadResult; - }); - - // Verify insert() is called (meaning we reached the insert path) - $qb->expects(self::once())->method('insert')->with('appconfig')->willReturn($qb); - $qb->method('executeStatement')->willReturn(1); - - // Track which columns are set via setValue - $setColumns = []; - $qb->method('setValue') - ->willReturnCallback(function (string $column) use ($qb, &$setColumns) { - $setColumns[] = $column; - return $qb; - }); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $appConfig = $this->getAppConfig(); - $appConfig->setValueString('core', 'vendor', 'owncloud'); - - $this->assertContains('appid', $setColumns); - $this->assertContains('configkey', $setColumns); - $this->assertContains('configvalue', $setColumns); - $this->assertNotContains('lazy', $setColumns, 'lazy column should be omitted in fallback mode'); - $this->assertNotContains('type', $setColumns, 'type column should be omitted in fallback mode'); - } -} --- a/tests/lib/AppConfigTest.php +++ /dev/null @@ -1,234 +0,0 @@ -connection = $this->createMock(IDBConnection::class); - $this->config = $this->createMock(IConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->presetManager = $this->createMock(PresetManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->cacheFactory = $this->createMock(CacheFactory::class); - $this->localCache = $this->createMock(ICache::class); - } - - protected function getAppConfig($cached = false): AppConfig { - $this->config->method('getSystemValueBool') - ->with('cache_app_config', true) - ->willReturn(true); - $this->cacheFactory->method('isLocalCacheAvailable')->willReturn($cached); - if ($cached) { - $this->cacheFactory->method('withServerVersionPrefix')->willReturnCallback(function (\Closure $closure): void { - $closure($this->cacheFactory); - }); - $this->cacheFactory->method('createLocal')->willReturn($this->localCache); - } - - return new AppConfig( - $this->connection, - $this->config, - $this->configManager, - $this->presetManager, - $this->logger, - $this->crypto, - $this->cacheFactory, - ); - } - - public function testCachedRead(): void { - $this->localCache->expects(self::once()) - ->method('get') - ->with('OC\\AppConfig') - ->willReturn([ - 'fastCache' => [ - 'appid' => [ - 'some-key' => 'some-value', - 'other-key' => 'other value' - ], - ], - 'valueTypes' => [ - 'appid' => [ - 'some-key' => AppConfig::VALUE_STRING, - 'other-key' => AppConfig::VALUE_STRING, - ], - ], - ]); - - $this->connection->expects(self::never())->method('getQueryBuilder'); - $config = $this->getAppConfig(true); - - $this->assertSame('some-value', $config->getValueString('appid', 'some-key')); - $this->assertSame('other value', $config->getValueString('appid', 'other-key')); - $this->assertSame(AppConfig::VALUE_STRING, $config->getValueType('appid', 'some-key', false)); - } - - public function testCachedLazyRead(): void { - $this->localCache->expects(self::once()) - ->method('get') - ->with('OC\\AppConfig') - ->willReturn([ - 'fastCache' => [ - 'appid' => [ - 'fast-key' => 'fast value', - ], - ], - 'lazyCache' => [ - 'appid' => [ - 'lazy-key' => 'lazy value', - ], - ], - 'valueTypes' => [ - 'appid' => [ - 'some-key' => AppConfig::VALUE_STRING, - 'lazy-key' => AppConfig::VALUE_STRING, - ], - ], - ]); - - $this->connection->expects(self::never())->method('getQueryBuilder'); - $config = $this->getAppConfig(true); - - $this->assertSame('fast value', $config->getValueString('appid', 'fast-key')); - $this->assertSame('lazy value', $config->getValueString('appid', 'lazy-key', '', true)); - } - - public function testOnlyFastKeyCached(): void { - $this->localCache->expects(self::atLeastOnce()) - ->method('get') - ->with('OC\\AppConfig') - ->willReturn([ - 'fastCache' => [ - 'appid' => [ - 'fast-key' => 'fast value', - ], - ], - 'valueTypes' => [ - 'appid' => [ - 'fast-key' => AppConfig::VALUE_STRING, - ], - ], - ]); - - $result = $this->createMock(IResult::class); - $result->method('fetchAllAssociative')->willReturn([ - ['lazy' => 1, 'appid' => 'appid', 'configkey' => 'lazy-key', 'configvalue' => 'lazy value'], - ]); - $expression = $this->createMock(IExpressionBuilder::class); - $queryBuilder = $this->createMock(IQueryBuilder::class); - $queryBuilder->method('from')->willReturn($queryBuilder); - $queryBuilder->method('expr')->willReturn($expression); - $queryBuilder->method('executeQuery')->willReturn($result); - - $this->connection->expects(self::once())->method('getQueryBuilder')->willReturn($queryBuilder); - $config = $this->getAppConfig(true); - - $this->assertSame('fast value', $config->getValueString('appid', 'fast-key')); - $this->assertSame('lazy value', $config->getValueString('appid', 'lazy-key', '', true)); - } - - public function testWritesAreCached(): void { - $this->localCache->expects(self::atLeastOnce()) - ->method('get') - ->with('OC\\AppConfig') - ->willReturn([ - 'fastCache' => [ - 'appid' => [ - 'first-key' => 'first value', - ], - ], - 'valueTypes' => [ - 'appid' => [ - 'first-key' => AppConfig::VALUE_STRING, - ], - ], - ]); - - $expression = $this->createMock(IExpressionBuilder::class); - $queryBuilder = $this->createMock(IQueryBuilder::class); - $queryBuilder->expects(self::once()) - ->method('update') - ->with('appconfig', null) - ->willReturn($queryBuilder); - $queryBuilder->method('set')->willReturn($queryBuilder); - $queryBuilder->method('where')->willReturn($queryBuilder); - $queryBuilder->method('andWhere')->willReturn($queryBuilder); - $queryBuilder->method('expr')->willReturn($expression); - $this->connection->expects(self::once())->method('getQueryBuilder')->willReturn($queryBuilder); - - $config = $this->getAppConfig(true); - - $this->assertSame('first value', $config->getValueString('appid', 'first-key')); - $config->setValueString('appid', 'first-key', 'new value'); - $this->assertSame('new value', $config->getValueString('appid', 'first-key')); - } - - public function testFilteredValuesMaskTheEuroOfficeSecret(): void { - $this->localCache->expects(self::atLeastOnce()) - ->method('get') - ->with('OC\\AppConfig') - ->willReturn([ - 'fastCache' => [ - 'eurooffice' => [ - 'jwt_secret' => 'a-document-server-signing-key', - 'jwt_header' => 'AuthorizationJwt', - ], - ], - 'lazyCache' => [ - 'eurooffice' => [], - ], - 'valueTypes' => [ - 'eurooffice' => [ - 'jwt_secret' => AppConfig::VALUE_STRING, - 'jwt_header' => AppConfig::VALUE_STRING, - ], - ], - ]); - - $this->connection->expects(self::never())->method('getQueryBuilder'); - $config = $this->getAppConfig(true); - - $this->assertSame([ - 'jwt_secret' => IConfig::SENSITIVE_VALUE, - 'jwt_header' => 'AuthorizationJwt', - ], $config->getFilteredValues('eurooffice')); - } -} --- a/tests/lib/AppFramework/AppTest.php +++ /dev/null @@ -1,214 +0,0 @@ -container = new DIContainer('test', []); - $this->controller = $this->createMock(Controller::class); - $this->dispatcher = $this->createMock(Dispatcher::class); - $this->io = $this->createMock(IOutput::class); - - $this->headers = ['key' => 'value']; - $this->output = 'hi'; - $this->controllerName = 'Controller'; - $this->controllerMethod = 'method'; - - $this->container[$this->controllerName] = $this->controller; - $this->container[Dispatcher::class] = $this->dispatcher; - $this->container[IOutput::class] = $this->io; - $this->container['urlParams'] = ['_route' => 'not-profiler']; - - $this->appPath = __DIR__ . '/../../../apps/namespacetestapp'; - $infoXmlPath = $this->appPath . '/appinfo/info.xml'; - mkdir($this->appPath . '/appinfo', 0777, true); - - $xml = '' - . '' - . 'namespacetestapp' - . 'NameSpaceTestApp' - . ''; - file_put_contents($infoXmlPath, $xml); - } - - public function testControllerNameAndMethodAreBeingPassed(): void { - $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - - $this->io->expects($this->never()) - ->method('setOutput'); - - App::main($this->controllerName, $this->controllerMethod, - $this->container); - } - - public function testBuildAppNamespace(): void { - $ns = App::buildAppNamespace('someapp'); - $this->assertEquals('OCA\Someapp', $ns); - } - - public function testBuildAppNamespaceCore(): void { - $ns = App::buildAppNamespace('someapp', 'OC\\'); - $this->assertEquals('OC\Someapp', $ns); - } - - public function testBuildAppNamespaceInfoXml(): void { - $ns = App::buildAppNamespace('namespacetestapp', 'OCA\\'); - $this->assertEquals('OCA\NameSpaceTestApp', $ns); - } - - #[\Override] - protected function tearDown(): void { - rrmdir($this->appPath); - parent::tearDown(); - } - - public function testOutputIsPrinted(): void { - $return = ['HTTP/2.0 200 OK', [], [], $this->output, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - $this->io->expects($this->once()) - ->method('setOutput') - ->with($this->equalTo($this->output)); - App::main($this->controllerName, $this->controllerMethod, $this->container, []); - } - - public static function dataNoOutput(): array { - return [ - ['HTTP/2.0 204 No content'], - ['HTTP/2.0 304 Not modified'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOutput')] - public function testNoOutput(string $statusCode): void { - $return = [$statusCode, [], [], $this->output, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - $this->io->expects($this->once()) - ->method('setHeader') - ->with($this->equalTo($statusCode)); - $this->io->expects($this->never()) - ->method('setOutput'); - App::main($this->controllerName, $this->controllerMethod, $this->container, []); - } - - public function testCallbackIsCalled(): void { - $mock = $this->getMockBuilder('OCP\AppFramework\Http\ICallbackResponse') - ->getMock(); - - $return = ['HTTP/2.0 200 OK', [], [], $this->output, $mock]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - $mock->expects($this->once()) - ->method('callback'); - App::main($this->controllerName, $this->controllerMethod, $this->container, []); - } - - public function testCoreApp(): void { - $this->container['appName'] = 'core'; - $this->container['OC\Core\Controller\Foo'] = $this->controller; - $this->container['urlParams'] = ['_route' => 'not-profiler']; - - $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - - $this->io->expects($this->never()) - ->method('setOutput'); - - App::main('Foo', $this->controllerMethod, $this->container); - } - - public function testSettingsApp(): void { - $this->container['appName'] = 'settings'; - $this->container['OCA\Settings\Controller\Foo'] = $this->controller; - $this->container['urlParams'] = ['_route' => 'not-profiler']; - - $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - - $this->io->expects($this->never()) - ->method('setOutput'); - - App::main('Foo', $this->controllerMethod, $this->container); - } - - public function testApp(): void { - $this->container['appName'] = 'bar'; - $this->container['OCA\Bar\Controller\Foo'] = $this->controller; - $this->container['urlParams'] = ['_route' => 'not-profiler']; - - $return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willReturn($return); - - $this->io->expects($this->never()) - ->method('setOutput'); - - App::main('Foo', $this->controllerMethod, $this->container); - } -} --- a/tests/lib/AppFramework/Bootstrap/BootContextTest.php +++ /dev/null @@ -1,50 +0,0 @@ -appContainer = $this->createMock(IAppContainer::class); - - $this->context = new BootContext( - $this->appContainer - ); - } - - public function testGetAppContainer(): void { - $container = $this->context->getAppContainer(); - - $this->assertSame($this->appContainer, $container); - } - - public function testGetServerContainer(): void { - $serverContainer = $this->createMock(IServerContainer::class); - $this->appContainer->method('get') - ->with(IServerContainer::class) - ->willReturn($serverContainer); - - $container = $this->context->getServerContainer(); - - $this->assertSame($serverContainer, $container); - } -} --- a/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php +++ /dev/null @@ -1,112 +0,0 @@ -appManager = $this->createMock(AppManager::class); - $this->serverContainer = $this->createMock(ContainerInterface::class); - $this->crashReporterRegistry = $this->createMock(Registry::class); - $this->dashboardManager = $this->createMock(IManager::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->eventLogger = $this->createMock(IEventLogger::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->appManager->expects($this->any()) - ->method('getAppNamespace') - ->with('settings') - ->willReturn('OCA\\Settings'); - - $this->coordinator = new Coordinator( - $this->serverContainer, - $this->crashReporterRegistry, - $this->dashboardManager, - $this->eventDispatcher, - $this->eventLogger, - $this->appManager, - $this->logger, - ); - } - - public function testBootAppNotLoadable(): void { - $appId = 'settings'; - $this->serverContainer->expects($this->once()) - ->method('get') - ->with(Application::class) - ->willThrowException(new QueryException('')); - $this->logger->expects($this->once()) - ->method('error'); - - $this->coordinator->bootApp($appId); - } - - public function testBootAppNotBootable(): void { - $appId = 'settings'; - $mockApp = $this->createMock(Application::class); - $this->serverContainer->expects($this->once()) - ->method('get') - ->with(Application::class) - ->willReturn($mockApp); - - $this->coordinator->bootApp($appId); - } - - public function testBootApp(): void { - $appId = 'settings'; - $mockApp = new class extends App implements IBootstrap { - public function __construct() { - parent::__construct('test', []); - } - - #[\Override] - public function register(IRegistrationContext $context): void { - } - - #[\Override] - public function boot(IBootContext $context): void { - } - }; - $this->serverContainer->expects($this->once()) - ->method('get') - ->with(Application::class) - ->willReturn($mockApp); - - $this->coordinator->bootApp($appId); - } -} --- a/tests/lib/AppFramework/Bootstrap/FunctionInjectorTest.php +++ /dev/null @@ -1,68 +0,0 @@ -container = new SimpleContainer(); - } - - public function testInjectFnNotRegistered(): void { - $this->expectException(ContainerExceptionInterface::class); - - (new FunctionInjector($this->container))->injectFn(static function (Foo $p1): void { - }); - } - - public function testInjectFnNotRegisteredButNullable(): void { - (new FunctionInjector($this->container))->injectFn(static function (?Foo $p1): void { - }); - - // Nothing to assert. No errors means everything is fine. - $this->addToAssertionCount(1); - } - - public function testInjectFnByType(): void { - $this->container->registerService(Foo::class, function () { - $this->addToAssertionCount(1); - return new class implements Foo { - }; - }); - - (new FunctionInjector($this->container))->injectFn(static function (Foo $p1): void { - }); - - // Nothing to assert. No errors means everything is fine. - $this->addToAssertionCount(1); - } - - public function testInjectFnByName(): void { - $this->container->registerParameter('test', 'abc'); - - (new FunctionInjector($this->container))->injectFn(static function ($test): void { - }); - - // Nothing to assert. No errors means everything is fine. - $this->addToAssertionCount(1); - } -} --- a/tests/lib/AppFramework/Bootstrap/RegistrationContextTest.php +++ /dev/null @@ -1,171 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - - $this->context = new RegistrationContext( - $this->logger - ); - } - - public function testRegisterCapability(): void { - $app = $this->createMock(App::class); - $name = 'abc'; - $container = $this->createMock(DIContainer::class); - $app->method('getContainer') - ->willReturn($container); - $container->expects($this->once()) - ->method('registerCapability') - ->with($name); - $this->logger->expects($this->never()) - ->method('error'); - - $this->context->for('myapp')->registerCapability($name); - $this->context->delegateCapabilityRegistrations([ - 'myapp' => $app, - ]); - } - - public function testRegisterEventListener(): void { - $event = 'abc'; - $service = 'def'; - $dispatcher = $this->createMock(IEventDispatcher::class); - $dispatcher->expects($this->once()) - ->method('addServiceListener') - ->with($event, $service, 0); - $this->logger->expects($this->never()) - ->method('error'); - - $this->context->for('myapp')->registerEventListener($event, $service); - $this->context->delegateEventListenerRegistrations($dispatcher); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider_TrueFalse')] - public function testRegisterService(bool $shared): void { - $app = $this->createMock(App::class); - $service = 'abc'; - $factory = function () { - return 'def'; - }; - $container = $this->createMock(DIContainer::class); - $app->method('getContainer') - ->willReturn($container); - $container->expects($this->once()) - ->method('registerService') - ->with($service, $factory, $shared); - $this->logger->expects($this->never()) - ->method('error'); - - $this->context->for('myapp')->registerService($service, $factory, $shared); - $this->context->delegateContainerRegistrations([ - 'myapp' => $app, - ]); - } - - public function testRegisterServiceAlias(): void { - $app = $this->createMock(App::class); - $alias = 'abc'; - $target = 'def'; - $container = $this->createMock(DIContainer::class); - $app->method('getContainer') - ->willReturn($container); - $container->expects($this->once()) - ->method('registerAlias') - ->with($alias, $target); - $this->logger->expects($this->never()) - ->method('error'); - - $this->context->for('myapp')->registerServiceAlias($alias, $target); - $this->context->delegateContainerRegistrations([ - 'myapp' => $app, - ]); - } - - public function testRegisterParameter(): void { - $app = $this->createMock(App::class); - $name = 'abc'; - $value = 'def'; - $container = $this->createMock(DIContainer::class); - $app->method('getContainer') - ->willReturn($container); - $container->expects($this->once()) - ->method('registerParameter') - ->with($name, $value); - $this->logger->expects($this->never()) - ->method('error'); - - $this->context->for('myapp')->registerParameter($name, $value); - $this->context->delegateContainerRegistrations([ - 'myapp' => $app, - ]); - } - - public function testRegisterUserMigrator(): void { - $appIdA = 'myapp'; - $migratorClassA = 'OCA\App\UserMigration\AppMigrator'; - - $appIdB = 'otherapp'; - $migratorClassB = 'OCA\OtherApp\UserMigration\OtherAppMigrator'; - - $serviceRegistrationA = new ServiceRegistration($appIdA, $migratorClassA); - $serviceRegistrationB = new ServiceRegistration($appIdB, $migratorClassB); - - $this->context - ->for($appIdA) - ->registerUserMigrator($migratorClassA); - $this->context - ->for($appIdB) - ->registerUserMigrator($migratorClassB); - - $this->assertEquals( - [ - $serviceRegistrationA, - $serviceRegistrationB, - ], - $this->context->getUserMigrators(), - ); - } - - public static function dataProvider_TrueFalse(): array { - return[ - [true], - [false] - ]; - } - - public function testGetMiddlewareRegistrations(): void { - $this->context->registerMiddleware('core', TwoFactorMiddleware::class, false); - - $registrations = $this->context->getMiddlewareRegistrations(); - - self::assertNotEmpty($registrations); - self::assertSame('core', $registrations[0]->getAppId()); - self::assertSame(TwoFactorMiddleware::class, $registrations[0]->getService()); - } -} --- a/tests/lib/AppFramework/Controller/ApiControllerTest.php +++ /dev/null @@ -1,43 +0,0 @@ - ['HTTP_ORIGIN' => 'test']], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->controller = new ChildApiController('app', $request, 'verbs', - 'headers', 100); - - $response = $this->controller->preflightedCors(); - - $headers = $response->getHeaders(); - - $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); - $this->assertEquals('verbs', $headers['Access-Control-Allow-Methods']); - $this->assertEquals('headers', $headers['Access-Control-Allow-Headers']); - $this->assertEquals('false', $headers['Access-Control-Allow-Credentials']); - $this->assertEquals(100, $headers['Access-Control-Max-Age']); - } -} --- a/tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php +++ /dev/null @@ -1,137 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->session = $this->createMock(ISession::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - - $this->controller = $this->getMockBuilder(AuthPublicShareController::class) - ->setConstructorArgs([ - 'app', - $this->request, - $this->session, - $this->urlGenerator - ])->onlyMethods([ - 'authFailed', - 'getPasswordHash', - 'isAuthenticated', - 'isPasswordProtected', - 'isValidToken', - 'showShare', - 'verifyPassword', - 'validateIdentity', - 'generatePassword' - ])->getMock(); - } - - public function testShowAuthenticate(): void { - $expects = new TemplateResponse('core', 'publicshareauth', [], 'guest'); - - $this->assertEquals($expects, $this->controller->showAuthenticate()); - } - - public function testAuthenticateAuthenticated(): void { - $this->controller->method('isAuthenticated') - ->willReturn(true); - - $this->controller->setToken('myToken'); - - $this->session->method('get') - ->willReturnMap([ - ['public_link_authenticate_redirect', json_encode(['foo' => 'bar'])], - ]); - - $this->urlGenerator->method('linkToRoute') - ->willReturn('myLink!'); - - $result = $this->controller->authenticate('password'); - $this->assertInstanceOf(RedirectResponse::class, $result); - $this->assertSame('myLink!', $result->getRedirectURL()); - } - - public function testAuthenticateInvalidPassword(): void { - $this->controller->setToken('token'); - $this->controller->method('isPasswordProtected') - ->willReturn(true); - - $this->controller->method('verifyPassword') - ->with('password') - ->willReturn(false); - - $this->controller->expects($this->once()) - ->method('authFailed'); - - $expects = new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest'); - $expects->throttle(); - - $result = $this->controller->authenticate('password'); - - $this->assertEquals($expects, $result); - } - - public function testAuthenticateValidPassword(): void { - $this->controller->setToken('token'); - $this->controller->method('isPasswordProtected') - ->willReturn(true); - $this->controller->method('verifyPassword') - ->with('password') - ->willReturn(true); - $this->controller->method('getPasswordHash') - ->willReturn('hash'); - - $this->session->expects($this->once()) - ->method('regenerateId'); - $this->session->method('get') - ->willReturnMap([ - ['public_link_authenticate_redirect', json_encode(['foo' => 'bar'])], - ]); - - $tokenStored = false; - $this->session - ->method('set') - ->willReturnCallback(function ($key, $value) use (&$tokenStored) { - if ($key === AuthPublicShareController::DAV_AUTHENTICATED_FRONTEND) { - $decoded = json_decode($value, true); - if (isset($decoded['token']) && $decoded['token'] === 'hash') { - $tokenStored = true; - } - return true; - } - return false; - }); - - $this->urlGenerator->method('linkToRoute') - ->willReturn('myLink!'); - - $result = $this->controller->authenticate('password'); - $this->assertInstanceOf(RedirectResponse::class, $result); - $this->assertSame('myLink!', $result->getRedirectURL()); - $this->assertTrue($tokenStored); - } -} --- a/tests/lib/AppFramework/Controller/ControllerTest.php +++ /dev/null @@ -1,128 +0,0 @@ -registerResponder('tom', function ($respone) { - return 'hi'; - }); - } - - public function custom($in) { - $this->registerResponder('json', function ($response) { - return new JSONResponse([strlen($response)]); - }); - - return $in; - } - - public function customDataResponse(mixed $in): DataResponse { - $response = new DataResponse($in, 300); - $response->addHeader('test', 'something'); - return $response; - } -}; - -class ControllerTest extends \Test\TestCase { - private Controller $controller; - private Request $request; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $request = new Request( - [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'post' => ['name' => 'Jane Doe', 'nickname' => 'Janey'], - 'urlParams' => ['name' => 'Johnny Weissmüller'], - 'files' => ['file' => 'filevalue'], - 'env' => ['PATH' => 'daheim'], - 'session' => ['sezession' => 'kein'], - 'method' => 'hi', - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - - $this->controller = new ChildController('apptemplate_advanced', $request); - $this->overwriteService(IRequest::class, $request); - $this->request = $request; - } - - public function testFormatResonseInvalidFormat(): void { - $this->expectException(\DomainException::class); - - $this->controller->buildResponse(null, 'test'); - } - - public function testFormat(): void { - $response = $this->controller->buildResponse(['hi'], 'json'); - - $this->assertEquals(['hi'], $response->getData()); - } - - public function testFormatDataResponseJSON(): void { - $expectedHeaders = [ - 'test' => 'something', - 'Cache-Control' => 'no-cache, no-store, must-revalidate', - 'Content-Type' => 'application/json; charset=utf-8', - 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'", - 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", - 'X-Request-Id' => $this->request->getId(), - 'X-Robots-Tag' => 'noindex, nofollow', - ]; - - $response = $this->controller->customDataResponse(['hi']); - $response = $this->controller->buildResponse($response, 'json'); - - $this->assertEquals(['hi'], $response->getData()); - $this->assertEquals(300, $response->getStatus()); - $this->assertEquals($expectedHeaders, $response->getHeaders()); - } - - public function testCustomFormatter(): void { - $response = $this->controller->custom('hi'); - $response = $this->controller->buildResponse($response, 'json'); - - $this->assertEquals([2], $response->getData()); - } - - public function testDefaultResponderToJSON(): void { - $responder = $this->controller->getResponderByHTTPHeader('*/*'); - - $this->assertEquals('json', $responder); - } - - public function testResponderAcceptHeaderParsed(): void { - $responder = $this->controller->getResponderByHTTPHeader( - '*/*, application/tom, application/json' - ); - - $this->assertEquals('tom', $responder); - } - - public function testResponderAcceptHeaderParsedUpperCase(): void { - $responder = $this->controller->getResponderByHTTPHeader( - '*/*, apPlication/ToM, application/json' - ); - - $this->assertEquals('tom', $responder); - } -} --- a/tests/lib/AppFramework/Controller/OCSControllerTest.php +++ /dev/null @@ -1,135 +0,0 @@ - [ - 'HTTP_ORIGIN' => 'test', - ], - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $controller = new ChildOCSController('app', $request, 'verbs', - 'headers', 100); - - $response = $controller->preflightedCors(); - - $headers = $response->getHeaders(); - - $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); - $this->assertEquals('verbs', $headers['Access-Control-Allow-Methods']); - $this->assertEquals('headers', $headers['Access-Control-Allow-Headers']); - $this->assertEquals('false', $headers['Access-Control-Allow-Credentials']); - $this->assertEquals(100, $headers['Access-Control-Max-Age']); - } - - public function testXML(): void { - $controller = new ChildOCSController('app', new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - )); - $controller->setOCSVersion(1); - - $expected = "\n" - . "\n" - . " \n" - . " ok\n" - . " 100\n" - . " OK\n" - . " \n" - . " \n" - . " \n" - . " \n" - . " hi\n" - . " \n" - . "\n"; - - $params = new DataResponse(['test' => 'hi']); - - $response = $controller->buildResponse($params, 'xml'); - $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); - $this->assertEquals($expected, $response->render()); - } - - public function testJSON(): void { - $controller = new ChildOCSController('app', new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - )); - $controller->setOCSVersion(1); - $expected = '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK",' - . '"totalitems":"","itemsperpage":""},"data":{"test":"hi"}}}'; - $params = new DataResponse(['test' => 'hi']); - - $response = $controller->buildResponse($params, 'json'); - $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); - $this->assertEquals($expected, $response->render()); - $this->assertEquals($expected, $response->render()); - } - - public function testXMLV2(): void { - $controller = new ChildOCSController('app', new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - )); - $controller->setOCSVersion(2); - - $expected = "\n" - . "\n" - . " \n" - . " ok\n" - . " 200\n" - . " OK\n" - . " \n" - . " \n" - . " hi\n" - . " \n" - . "\n"; - - $params = new DataResponse(['test' => 'hi']); - - $response = $controller->buildResponse($params, 'xml'); - $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); - $this->assertEquals($expected, $response->render()); - } - - public function testJSONV2(): void { - $controller = new ChildOCSController('app', new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - )); - $controller->setOCSVersion(2); - $expected = '{"ocs":{"meta":{"status":"ok","statuscode":200,"message":"OK"},"data":{"test":"hi"}}}'; - $params = new DataResponse(['test' => 'hi']); - - $response = $controller->buildResponse($params, 'json'); - $this->assertSame(EmptyContentSecurityPolicy::class, get_class($response->getContentSecurityPolicy())); - $this->assertEquals($expected, $response->render()); - } -} --- a/tests/lib/AppFramework/Controller/PublicShareControllerTest.php +++ /dev/null @@ -1,88 +0,0 @@ -hash; - } - - #[\Override] - public function isValidToken(): bool { - return false; - } - - #[\Override] - protected function isPasswordProtected(): bool { - return $this->isProtected; - } -} - -class PublicShareControllerTest extends \Test\TestCase { - private IRequest&MockObject $request; - private ISession&MockObject $session; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->request = $this->createMock(IRequest::class); - $this->session = $this->createMock(ISession::class); - } - - public function testGetToken(): void { - $controller = new TestController('app', $this->request, $this->session, 'hash', false); - - $controller->setToken('test'); - $this->assertEquals('test', $controller->getToken()); - } - - public static function dataIsAuthenticated(): array { - return [ - [false, 'token1', 'token1', 'hash1', 'hash1', true], - [false, 'token1', 'token1', 'hash1', 'hash2', true], - [false, 'token1', 'token2', 'hash1', 'hash1', true], - [false, 'token1', 'token2', 'hash1', 'hash2', true], - [ true, 'token1', 'token1', 'hash1', 'hash1', true], - [ true, 'token1', 'token1', 'hash1', 'hash2', false], - [ true, 'token1', 'token2', 'hash1', 'hash1', false], - [ true, 'token1', 'token2', 'hash1', 'hash2', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsAuthenticated')] - public function testIsAuthenticatedNotPasswordProtected(bool $protected, string $token1, string $token2, string $hash1, string $hash2, bool $expected): void { - $controller = new TestController('app', $this->request, $this->session, $hash2, $protected); - - $this->session->method('get') - ->with(PublicShareController::DAV_AUTHENTICATED_FRONTEND) - ->willReturn("{\"$token1\":\"$hash1\"}"); - - $controller->setToken($token2); - - $this->assertEquals($expected, $controller->isAuthenticated()); - } -} --- a/tests/lib/AppFramework/Db/EntityTest.php +++ /dev/null @@ -1,298 +0,0 @@ -addType('testId', Types::INTEGER); - $this->addType('smallInt', Types::SMALLINT); - $this->addType('bigInt', Types::BIGINT); - $this->addType('anotherBool', Types::BOOLEAN); - $this->addType('text', Types::TEXT); - $this->addType('longText', Types::BLOB); - $this->addType('time', Types::TIME); - $this->addType('datetime', Types::DATETIME_IMMUTABLE); - - // Legacy types - $this->addType('trueOrFalse', 'bool'); - $this->addType('legacyInt', 'int'); - $this->addType('doubleNowFloat', 'double'); - } - - public function setAnotherBool(bool $anotherBool): void { - parent::setAnotherBool($anotherBool); - } -} - -class EntityTest extends \Test\TestCase { - private TestEntity $entity; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->entity = new TestEntity(); - } - - public function testResetUpdatedFields(): void { - $entity = new TestEntity(); - $entity->setId(3); - $entity->resetUpdatedFields(); - - $this->assertEquals([], $entity->getUpdatedFields()); - } - - public function testFromRow(): void { - $row = [ - 'pre_name' => 'john', - 'email' => 'john@something.com', - 'another_bool' => 1, - ]; - $this->entity = TestEntity::fromRow($row); - - $this->assertEquals($row['pre_name'], $this->entity->getPreName()); - $this->assertEquals($row['email'], $this->entity->getEmail()); - $this->assertEquals($row['another_bool'], $this->entity->getAnotherBool()); - } - - public function testGetSetId(): void { - $id = 3; - $this->entity->setId(3); - - $this->assertEquals($id, $this->entity->getId()); - } - - public function testColumnToPropertyNoReplacement(): void { - $column = 'my'; - $this->assertEquals('my', - $this->entity->columnToProperty($column)); - } - - public function testColumnToProperty(): void { - $column = 'my_attribute'; - $this->assertEquals('myAttribute', - $this->entity->columnToProperty($column)); - } - - public function testPropertyToColumnNoReplacement(): void { - $property = 'my'; - $this->assertEquals('my', - $this->entity->propertyToColumn($property)); - } - - public function testSetterMarksFieldUpdated(): void { - $this->entity->setId(3); - - $this->assertContains('id', array_keys($this->entity->getUpdatedFields())); - } - - public function testCallShouldOnlyWorkForGetterSetter(): void { - $this->expectException(\BadFunctionCallException::class); - - $this->entity->something(); - } - - public function testGetterShouldFailIfAttributeNotDefined(): void { - $this->expectException(\BadFunctionCallException::class); - - $this->entity->getTest(); - } - - public function testSetterShouldFailIfAttributeNotDefined(): void { - $this->expectException(\BadFunctionCallException::class); - - $this->entity->setTest(); - } - - public function testFromRowShouldNotAssignEmptyArray(): void { - $row = []; - $entity2 = new TestEntity(); - - $this->entity = TestEntity::fromRow($row); - $this->assertEquals($entity2, $this->entity); - } - - public function testIdGetsConvertedToInt(): void { - $row = ['id' => '4']; - - $this->entity = TestEntity::fromRow($row); - $this->assertSame(4, $this->entity->getId()); - } - - public function testSetType(): void { - $row = ['testId' => '4']; - - $this->entity = TestEntity::fromRow($row); - $this->assertSame(4, $this->entity->getTestId()); - } - - public function testFromParams(): void { - $params = [ - 'testId' => 4, - 'email' => 'john@doe' - ]; - - $entity = TestEntity::fromParams($params); - - $this->assertEquals($params['testId'], $entity->getTestId()); - $this->assertEquals($params['email'], $entity->getEmail()); - $this->assertTrue($entity instanceof TestEntity); - } - - public function testSlugify(): void { - $entity = new TestEntity(); - $entity->setName('Slugify this!'); - $this->assertEquals('slugify-this', $entity->slugify('name')); - $entity->setName('°!"§$%&/()=?`´ß\}][{³²#\'+~*-_.:,;<>|äöüÄÖÜSlugify this!'); - $this->assertEquals('slugify-this', $entity->slugify('name')); - } - - public static function dataSetterCasts(): array { - return [ - ['Id', '3', 3], - ['smallInt', '3', 3], - ['bigInt', '' . PHP_INT_MAX, PHP_INT_MAX], - ['trueOrFalse', 0, false], - ['trueOrFalse', 1, true], - ['anotherBool', 0, false], - ['anotherBool', 1, true], - ['text', 33, '33'], - ['longText', PHP_INT_MAX, '' . PHP_INT_MAX], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetterCasts')] - public function testSetterCasts(string $field, mixed $in, mixed $out): void { - $entity = new TestEntity(); - $entity->{'set' . $field}($in); - $this->assertSame($out, $entity->{'get' . $field}()); - } - - public function testSetterDoesNotCastOnNull(): void { - $entity = new TestEntity(); - $entity->setId(null); - $this->assertSame(null, $entity->getId()); - } - - public function testSetterConvertsResourcesToStringProperly(): void { - $string = 'Definitely a string'; - $stream = fopen('php://memory', 'r+'); - fwrite($stream, $string); - rewind($stream); - - $entity = new TestEntity(); - $entity->setLongText($stream); - fclose($stream); - $this->assertSame($string, $entity->getLongText()); - } - - public function testSetterConvertsDatetime() { - $entity = new TestEntity(); - $entity->setDatetime('2024-08-19 15:26:00'); - $this->assertEquals(new \DateTimeImmutable('2024-08-19 15:26:00'), $entity->getDatetime()); - } - - public function testSetterDoesNotConvertNullOnDatetime() { - $entity = new TestEntity(); - $entity->setDatetime(null); - $this->assertNull($entity->getDatetime()); - } - - public function testSetterConvertsTime() { - $entity = new TestEntity(); - $entity->setTime('15:26:00'); - $this->assertEquals(new \DateTime('15:26:00'), $entity->getTime()); - } - - public function testGetFieldTypes(): void { - $entity = new TestEntity(); - $this->assertEquals([ - 'id' => Types::INTEGER, - 'testId' => Types::INTEGER, - 'smallInt' => Types::SMALLINT, - 'bigInt' => Types::BIGINT, - 'anotherBool' => Types::BOOLEAN, - 'text' => Types::TEXT, - 'longText' => Types::BLOB, - 'time' => Types::TIME, - 'datetime' => Types::DATETIME_IMMUTABLE, - 'trueOrFalse' => Types::BOOLEAN, - 'legacyInt' => Types::INTEGER, - 'doubleNowFloat' => Types::FLOAT, - ], $entity->getFieldTypes()); - } - - public function testGetItInt(): void { - $entity = new TestEntity(); - $entity->setId(3); - $this->assertEquals(Types::INTEGER, gettype($entity->getId())); - } - - public function testFieldsNotMarkedUpdatedIfNothingChanges(): void { - $entity = new TestEntity('hey'); - $entity->setName('hey'); - $this->assertEquals(0, count($entity->getUpdatedFields())); - } - - public function testIsGetter(): void { - $entity = new TestEntity(); - $entity->setTrueOrFalse(false); - $entity->setAnotherBool(false); - $this->assertThat($entity->isTrueOrFalse(), new IsType(IsType::TYPE_BOOL)); - $this->assertThat($entity->isAnotherBool(), new IsType(IsType::TYPE_BOOL)); - } - - public function testIsGetterShoudFailForOtherType(): void { - $this->expectException(\BadFunctionCallException::class); - - $entity = new TestEntity(); - $entity->setName('hello'); - $this->assertThat($entity->isName(), new IsType(IsType::TYPE_BOOL)); - } -} --- a/tests/lib/AppFramework/Db/QBMapperDBTest.php +++ /dev/null @@ -1,160 +0,0 @@ -addType('time', Types::TIME); - $this->addType('datetime', Types::DATETIME_IMMUTABLE); - } -} - -/** - * Class QBDBTestMapper - * - * @package Test\AppFramework\Db - */ -class QBDBTestMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'testing', QBDBTestEntity::class); - } - - public function getParameterTypeForPropertyForTest(Entity $entity, string $property) { - return parent::getParameterTypeForProperty($entity, $property); - } - - public function getById(int $id): QBDBTestEntity { - $qb = $this->db->getQueryBuilder(); - $query = $qb - ->select('*') - ->from($this->tableName) - ->where( - $qb->expr()->eq('id', $qb->createPositionalParameter($id, IQueryBuilder::PARAM_INT)), - ); - return $this->findEntity($query); - } -} - -/** - * Test real database handling (serialization) - */ -#[\PHPUnit\Framework\Attributes\Group('DB')] -class QBMapperDBTest extends TestCase { - protected IDBConnection $connection; - protected bool $schemaSetup = false; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->connection = Server::get(IDBConnection::class); - $this->prepareTestingTable(); - } - - public function testInsertDateTime(): void { - $mapper = new QBDBTestMapper($this->connection); - $entity = new QBDBTestEntity(); - $entity->setTime(new \DateTime('2003-01-01 12:34:00')); - $entity->setDatetime(new \DateTimeImmutable('2000-01-01 23:45:00')); - - $result = $mapper->insert($entity); - $this->assertNotNull($result->getId()); - } - - public function testRetrieveDateTime(): void { - $time = new \DateTime('2000-01-01 01:01:00'); - $datetime = new \DateTimeImmutable('2000-01-01 02:02:00'); - - $mapper = new QBDBTestMapper($this->connection); - $entity = new QBDBTestEntity(); - $entity->setTime($time); - $entity->setDatetime($datetime); - - $result = $mapper->insert($entity); - $this->assertNotNull($result->getId()); - - $dbEntity = $mapper->getById($result->getId()); - $this->assertEquals($time->format('H:i:s'), $dbEntity->getTime()->format('H:i:s')); - $this->assertEquals($datetime->format('Y-m-d H:i:s'), $dbEntity->getDatetime()->format('Y-m-d H:i:s')); - // The date is not saved for "time" - $this->assertNotEquals($time->format('Y'), $dbEntity->getTime()->format('Y')); - } - - public function testUpdateDateTime(): void { - $time = new \DateTime('2000-01-01 01:01:00'); - $datetime = new \DateTimeImmutable('2000-01-01 02:02:00'); - - $mapper = new QBDBTestMapper($this->connection); - $entity = new QBDBTestEntity(); - $entity->setTime('now'); - $entity->setDatetime('now'); - - /** @var QBDBTestEntity */ - $entity = $mapper->insert($entity); - $this->assertNotNull($entity->getId()); - - // Update the values - $entity->setTime($time); - $entity->setDatetime($datetime); - $mapper->update($entity); - - $dbEntity = $mapper->getById($entity->getId()); - $this->assertEquals($time->format('H:i:s'), $dbEntity->getTime()->format('H:i:s')); - $this->assertEquals($datetime->format('Y-m-d H:i:s'), $dbEntity->getDatetime()->format('Y-m-d H:i:s')); - } - - protected function prepareTestingTable(): void { - if ($this->schemaSetup) { - $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); - } - - $prefix = Server::get(IConfig::class)->getSystemValueString('dbtableprefix', 'oc_'); - $schema = $this->connection->createSchema(); - try { - $schema->getTable($prefix . 'testing'); - $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); - } catch (SchemaException $e) { - $this->schemaSetup = true; - $table = $schema->createTable($prefix . 'testing'); - $table->addColumn('id', Types::BIGINT, [ - 'autoincrement' => true, - 'notnull' => true, - ]); - - $table->addColumn('time', Types::TIME, [ - 'notnull' => false, - ]); - - $table->addColumn('datetime', Types::DATETIME_IMMUTABLE, [ - 'notnull' => false, - ]); - - $table->setPrimaryKey(['id']); - $this->connection->migrateToSchema($schema); - } - } -} --- a/tests/lib/AppFramework/Db/QBMapperTest.php +++ /dev/null @@ -1,242 +0,0 @@ -addType('intProp', 'int'); - $this->addType('boolProp', 'bool'); - $this->addType('stringProp', Types::STRING); - $this->addType('integerProp', Types::INTEGER); - $this->addType('booleanProp', Types::BOOLEAN); - $this->addType('jsonProp', Types::JSON); - $this->addType('datetimeProp', Types::DATETIME_IMMUTABLE); - } -} - -/** - * Class QBTestMapper - * - * @package Test\AppFramework\Db - */ -class QBTestMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'table'); - } - - public function getParameterTypeForPropertyForTest(Entity $entity, string $property) { - return parent::getParameterTypeForProperty($entity, $property); - } -} - -/** - * Class QBMapperTest - * - * @package Test\AppFramework\Db - */ -class QBMapperTest extends \Test\TestCase { - - protected IDBConnection&MockObject $db; - protected IQueryBuilder&MockObject $qb; - protected IExpressionBuilder&MockObject $expr; - protected QBTestMapper $mapper; - - /** - * @throws \ReflectionException - */ - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->db = $this->getMockBuilder(IDBConnection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->qb = $this->getMockBuilder(IQueryBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->expr = $this->getMockBuilder(IExpressionBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->qb->method('expr')->willReturn($this->expr); - $this->db->method('getQueryBuilder')->willReturn($this->qb); - - $this->mapper = new QBTestMapper($this->db); - } - - public function testInsertEntityParameterTypeMapping(): void { - $datetime = new \DateTimeImmutable(); - $entity = new QBTestEntity(); - $entity->setIntProp(123); - $entity->setBoolProp(true); - $entity->setStringProp('string'); - $entity->setIntegerProp(456); - $entity->setBooleanProp(false); - $entity->setDatetimeProp($datetime); - - $intParam = $this->qb->createNamedParameter('int_prop', IQueryBuilder::PARAM_INT); - $boolParam = $this->qb->createNamedParameter('bool_prop', IQueryBuilder::PARAM_BOOL); - $stringParam = $this->qb->createNamedParameter('string_prop', IQueryBuilder::PARAM_STR); - $integerParam = $this->qb->createNamedParameter('integer_prop', IQueryBuilder::PARAM_INT); - $booleanParam = $this->qb->createNamedParameter('boolean_prop', IQueryBuilder::PARAM_BOOL); - $datetimeParam = $this->qb->createNamedParameter('datetime_prop', IQueryBuilder::PARAM_DATETIME_IMMUTABLE); - - $createNamedParameterCalls = [ - [123, IQueryBuilder::PARAM_INT, null], - [true, IQueryBuilder::PARAM_BOOL, null], - ['string', IQueryBuilder::PARAM_STR, null], - [456, IQueryBuilder::PARAM_INT, null], - [false, IQueryBuilder::PARAM_BOOL, null], - [$datetime, IQueryBuilder::PARAM_DATETIME_IMMUTABLE, null], - ]; - $this->qb->expects($this->exactly(6)) - ->method('createNamedParameter') - ->willReturnCallback(function () use (&$createNamedParameterCalls): void { - $expected = array_shift($createNamedParameterCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $setValueCalls = [ - ['int_prop', $intParam], - ['bool_prop', $boolParam], - ['string_prop', $stringParam], - ['integer_prop', $integerParam], - ['boolean_prop', $booleanParam], - ['datetime_prop', $datetimeParam], - ]; - $this->qb->expects($this->exactly(6)) - ->method('setValue') - ->willReturnCallback(function () use (&$setValueCalls): void { - $expected = array_shift($setValueCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->mapper->insert($entity); - } - - public function testUpdateEntityParameterTypeMapping(): void { - $datetime = new \DateTimeImmutable(); - $entity = new QBTestEntity(); - $entity->setId(789); - $entity->setIntProp(123); - $entity->setBoolProp('true'); - $entity->setStringProp('string'); - $entity->setIntegerProp(456); - $entity->setBooleanProp(false); - $entity->setJsonProp(['hello' => 'world']); - $entity->setDatetimeProp($datetime); - - $idParam = $this->qb->createNamedParameter('id', IQueryBuilder::PARAM_INT); - $intParam = $this->qb->createNamedParameter('int_prop', IQueryBuilder::PARAM_INT); - $boolParam = $this->qb->createNamedParameter('bool_prop', IQueryBuilder::PARAM_BOOL); - $stringParam = $this->qb->createNamedParameter('string_prop', IQueryBuilder::PARAM_STR); - $integerParam = $this->qb->createNamedParameter('integer_prop', IQueryBuilder::PARAM_INT); - $booleanParam = $this->qb->createNamedParameter('boolean_prop', IQueryBuilder::PARAM_BOOL); - $jsonParam = $this->qb->createNamedParameter('json_prop', IQueryBuilder::PARAM_JSON); - $datetimeParam = $this->qb->createNamedParameter('datetime_prop', IQueryBuilder::PARAM_DATETIME_IMMUTABLE); - - $createNamedParameterCalls = [ - [123, IQueryBuilder::PARAM_INT, null], - [true, IQueryBuilder::PARAM_BOOL, null], - ['string', IQueryBuilder::PARAM_STR, null], - [456, IQueryBuilder::PARAM_INT, null], - [false, IQueryBuilder::PARAM_BOOL, null], - [['hello' => 'world'], IQueryBuilder::PARAM_JSON, null], - [$datetime, IQueryBuilder::PARAM_DATETIME_IMMUTABLE, null], - [789, IQueryBuilder::PARAM_INT, null], - ]; - $this->qb->expects($this->exactly(8)) - ->method('createNamedParameter') - ->willReturnCallback(function () use (&$createNamedParameterCalls): void { - $expected = array_shift($createNamedParameterCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $setCalls = [ - ['int_prop', $intParam], - ['bool_prop', $boolParam], - ['string_prop', $stringParam], - ['integer_prop', $integerParam], - ['boolean_prop', $booleanParam], - ['json_prop', $datetimeParam], - ['datetime_prop', $datetimeParam], - ]; - $this->qb->expects($this->exactly(7)) - ->method('set') - ->willReturnCallback(function () use (&$setCalls): void { - $expected = array_shift($setCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->expr->expects($this->once()) - ->method('eq') - ->with($this->equalTo('id'), $this->equalTo($idParam)); - - $this->mapper->update($entity); - } - - public function testGetParameterTypeForProperty(): void { - $entity = new QBTestEntity(); - - $intType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'intProp'); - $this->assertEquals(IQueryBuilder::PARAM_INT, $intType, 'Int type property mapping incorrect'); - - $integerType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'integerProp'); - $this->assertEquals(IQueryBuilder::PARAM_INT, $integerType, 'Integer type property mapping incorrect'); - - $boolType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'boolProp'); - $this->assertEquals(IQueryBuilder::PARAM_BOOL, $boolType, 'Bool type property mapping incorrect'); - - $booleanType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'booleanProp'); - $this->assertEquals(IQueryBuilder::PARAM_BOOL, $booleanType, 'Boolean type property mapping incorrect'); - - $stringType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'stringProp'); - $this->assertEquals(IQueryBuilder::PARAM_STR, $stringType, 'String type property mapping incorrect'); - - $jsonType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'jsonProp'); - $this->assertEquals(IQueryBuilder::PARAM_JSON, $jsonType, 'JSON type property mapping incorrect'); - - $datetimeType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'datetimeProp'); - $this->assertEquals(IQueryBuilder::PARAM_DATETIME_IMMUTABLE, $datetimeType, 'DateTimeImmutable type property mapping incorrect'); - - $unknownType = $this->mapper->getParameterTypeForPropertyForTest($entity, 'someProp'); - $this->assertEquals(IQueryBuilder::PARAM_STR, $unknownType, 'Unknown type property mapping incorrect'); - } -} --- a/tests/lib/AppFramework/Db/TransactionalTest.php +++ /dev/null @@ -1,79 +0,0 @@ -db = $this->createMock(IDBConnection::class); - } - - public function testAtomicRollback(): void { - $test = new class($this->db) { - use TTransactional; - - public function __construct( - private IDBConnection $db, - ) { - } - - public function fail(): void { - $this->atomic(function (): void { - throw new RuntimeException('nope'); - }, $this->db); - } - }; - $this->db->expects(self::once()) - ->method('beginTransaction'); - $this->db->expects(self::once()) - ->method('rollback'); - $this->db->expects(self::never()) - ->method('commit'); - $this->expectException(RuntimeException::class); - - $test->fail(); - } - - public function testAtomicCommit(): void { - $test = new class($this->db) { - use TTransactional; - - public function __construct( - private IDBConnection $db, - ) { - } - - public function succeed(): int { - return $this->atomic(function () { - return 3; - }, $this->db); - } - }; - $this->db->expects(self::once()) - ->method('beginTransaction'); - $this->db->expects(self::never()) - ->method('rollback'); - $this->db->expects(self::once()) - ->method('commit'); - - $result = $test->succeed(); - - self::assertEquals(3, $result); - } -} --- a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php +++ /dev/null @@ -1,143 +0,0 @@ -container = $this->getMockBuilder(DIContainer::class) - ->onlyMethods(['isAdminUser']) - ->setConstructorArgs(['name']) - ->getMock(); - } - - public function testProvidesRequest(): void { - $this->assertTrue(isset($this->container['Request'])); - } - - public function testProvidesMiddlewareDispatcher(): void { - $this->assertTrue(isset($this->container['MiddlewareDispatcher'])); - } - - public function testProvidesAppName(): void { - $this->assertTrue(isset($this->container['AppName'])); - $this->assertTrue(isset($this->container['appName'])); - } - - public function testAppNameIsSetCorrectly(): void { - $this->assertEquals('name', $this->container['AppName']); - $this->assertEquals('name', $this->container['appName']); - } - - public function testMiddlewareDispatcherIncludesSecurityMiddleware(): void { - $this->container['Request'] = new Request( - ['method' => 'GET'], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $dispatcher = $this->container['MiddlewareDispatcher']; - $middlewares = $dispatcher->getMiddlewares(); - - $found = false; - foreach ($middlewares as $middleware) { - if ($middleware instanceof SecurityMiddleware) { - $found = true; - } - } - - $this->assertTrue($found); - } - - public function testMiddlewareDispatcherIncludesBootstrapMiddlewares(): void { - $coordinator = $this->createMock(Coordinator::class); - $this->container[Coordinator::class] = $coordinator; - $this->container['Request'] = $this->createMock(Request::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $registrationContext->method('getMiddlewareRegistrations') - ->willReturn([ - new MiddlewareRegistration($this->container['appName'], 'foo', false), - new MiddlewareRegistration('otherapp', 'bar', false), - ]); - $this->container['foo'] = new class extends Middleware { - }; - $this->container['bar'] = new class extends Middleware { - }; - $coordinator->method('getRegistrationContext')->willReturn($registrationContext); - - $dispatcher = $this->container['MiddlewareDispatcher']; - - $middlewares = $dispatcher->getMiddlewares(); - self::assertNotEmpty($middlewares); - foreach ($middlewares as $middleware) { - if ($middleware === $this->container['bar']) { - $this->fail('Container must not register this middleware'); - } - if ($middleware === $this->container['foo']) { - // It is done - return; - } - } - $this->fail('Bootstrap registered middleware not found'); - } - - public function testMiddlewareDispatcherIncludesGlobalBootstrapMiddlewares(): void { - $coordinator = $this->createMock(Coordinator::class); - $this->container[Coordinator::class] = $coordinator; - $this->container['Request'] = $this->createMock(Request::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $registrationContext->method('getMiddlewareRegistrations') - ->willReturn([ - new MiddlewareRegistration('otherapp', 'foo', true), - new MiddlewareRegistration('otherapp', 'bar', false), - ]); - $this->container['foo'] = new class extends Middleware { - }; - $this->container['bar'] = new class extends Middleware { - }; - $coordinator->method('getRegistrationContext')->willReturn($registrationContext); - - $dispatcher = $this->container['MiddlewareDispatcher']; - - $middlewares = $dispatcher->getMiddlewares(); - self::assertNotEmpty($middlewares); - foreach ($middlewares as $middleware) { - if ($middleware === $this->container['bar']) { - $this->fail('Container must not register this middleware'); - } - if ($middleware === $this->container['foo']) { - // It is done - return; - } - } - $this->fail('Bootstrap registered middleware not found'); - } - - public function testInvalidAppClass(): void { - $this->expectException(QueryException::class); - $this->container->query('\OCA\Name\Foo'); - } -} --- a/tests/lib/AppFramework/DependencyInjection/DIIntergrationTests.php +++ /dev/null @@ -1,122 +0,0 @@ -server = new ServerContainer(); - $this->container = new DIContainer('App1', [], $this->server); - } - - public function testInjectFromServer(): void { - $this->server->registerService(Interface1::class, function () { - return new ClassA1(); - }); - - $this->server->registerService(ClassB::class, function (SimpleContainer $c) { - return new ClassB( - $c->query(Interface1::class) - ); - }); - - /** @var ClassB $res */ - $res = $this->container->query(ClassB::class); - $this->assertSame(ClassA1::class, get_class($res->interface1)); - } - - public function testInjectDepFromServer(): void { - $this->server->registerService(Interface1::class, function () { - return new ClassA1(); - }); - - $this->container->registerService(ClassB::class, function (SimpleContainer $c) { - return new ClassB( - $c->query(Interface1::class) - ); - }); - - /** @var ClassB $res */ - $res = $this->container->query(ClassB::class); - $this->assertSame(ClassA1::class, get_class($res->interface1)); - } - - public function testOverwriteDepFromServer(): void { - $this->server->registerService(Interface1::class, function () { - return new ClassA1(); - }); - - $this->container->registerService(Interface1::class, function () { - return new ClassA2(); - }); - - $this->container->registerService(ClassB::class, function (SimpleContainer $c) { - return new ClassB( - $c->query(Interface1::class) - ); - }); - - /** @var ClassB $res */ - $res = $this->container->query(ClassB::class); - $this->assertSame(ClassA2::class, get_class($res->interface1)); - } - - public function testIgnoreOverwriteInServerClass(): void { - $this->server->registerService(Interface1::class, function () { - return new ClassA1(); - }); - - $this->container->registerService(Interface1::class, function () { - return new ClassA2(); - }); - - $this->server->registerService(ClassB::class, function (SimpleContainer $c) { - return new ClassB( - $c->query(Interface1::class) - ); - }); - - /** @var ClassB $res */ - $res = $this->container->query(ClassB::class); - $this->assertSame(ClassA1::class, get_class($res->interface1)); - } -} --- a/tests/lib/AppFramework/Http/ContentSecurityPolicyTest.php +++ /dev/null @@ -1,461 +0,0 @@ -contentSecurityPolicy = new ContentSecurityPolicy(); - } - - public function testGetPolicyDefault(): void { - $defaultPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - $this->assertSame($defaultPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com www.nextcloud.org;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' www.nextcloud.com;style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomainMultipleStacked(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org')->disallowScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptDisallowEval(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com www.nextcloud.org 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomainMultipleStacked(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org')->disallowStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleAllowInline(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->allowInlineStyle(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleAllowInlineWithDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' www.nextcloud.com 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDisallowInline(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->allowInlineStyle(false); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyImageDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyImageDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com www.nextcloud.org;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: www.nextcloud.com;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org')->disallowImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFontDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFontDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com www.nextcloud.org;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data: www.nextcloud.com;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org')->disallowFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyConnectDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com;media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyConnectDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com www.nextcloud.org;media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self' www.nextcloud.com;media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org')->disallowConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyMediaDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyMediaDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self' www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org')->disallowMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyObjectDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyObjectDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';object-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org')->disallowObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetAllowedFrameDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFrameDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-src www.nextcloud.com;frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org')->disallowFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetAllowedFrameAncestorDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' sub.nextcloud.com;form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('sub.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFrameAncestorValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' sub.nextcloud.com foo.nextcloud.com;form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('sub.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('foo.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameAncestorDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameAncestorDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameAncestorDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self' www.nextcloud.com;form-action 'self'"; - - $this->contentSecurityPolicy->addAllowedFrameAncestorDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameAncestorDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyUnsafeWasmEval(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'wasm-unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->allowEvalWasm(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyNonce(): void { - $nonce = base64_encode('my-nonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyNonceDefault(): void { - $nonce = base64_encode('my-nonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';script-src-elem 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyNonceStrictDynamic(): void { - $nonce = base64_encode('my-nonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->contentSecurityPolicy->useStrictDynamic(true); - $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyNonceStrictDynamicDefault(): void { - $nonce = base64_encode('my-nonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->contentSecurityPolicy->useStrictDynamic(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStrictDynamicOnScriptsOff(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useStrictDynamicOnScripts(false); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStrictDynamicAndStrictDynamicOnScripts(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'"; - - $this->contentSecurityPolicy->useStrictDynamic(true); - $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } -} --- a/tests/lib/AppFramework/Http/DataResponseTest.php +++ /dev/null @@ -1,71 +0,0 @@ -response = new DataResponse(); - } - - public function testSetData(): void { - $params = ['hi', 'yo']; - $this->response->setData($params); - - $this->assertEquals(['hi', 'yo'], $this->response->getData()); - } - - public function testConstructorAllowsToSetData(): void { - $data = ['hi']; - $code = 300; - $response = new DataResponse($data, $code); - - $this->assertEquals($data, $response->getData()); - $this->assertEquals($code, $response->getStatus()); - } - - public function testConstructorAllowsToSetHeaders(): void { - $data = ['hi']; - $code = 300; - $headers = ['test' => 'something']; - $response = new DataResponse($data, $code, $headers); - - $expectedHeaders = [ - 'Cache-Control' => 'no-cache, no-store, must-revalidate', - 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'", - 'Feature-Policy' => "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'", - 'X-Robots-Tag' => 'noindex, nofollow', - 'X-Request-Id' => Server::get(IRequest::class)->getId(), - ]; - $expectedHeaders = array_merge($expectedHeaders, $headers); - - $this->assertEquals($data, $response->getData()); - $this->assertEquals($code, $response->getStatus()); - $this->assertEquals($expectedHeaders, $response->getHeaders()); - } - - public function testChainability(): void { - $params = ['hi', 'yo']; - $this->response->setData($params) - ->setStatus(Http::STATUS_NOT_FOUND); - - $this->assertEquals(Http::STATUS_NOT_FOUND, $this->response->getStatus()); - $this->assertEquals(['hi', 'yo'], $this->response->getData()); - } -} --- a/tests/lib/AppFramework/Http/DispatcherTest.php +++ /dev/null @@ -1,813 +0,0 @@ -registerResponder('text', function ($in) { - return new JSONResponse(['text' => $in]); - }); - return [$int, $bool, $test, $test2]; - } - - /** - * @param int $int - * @param bool $bool - * @param int $test - * @param int $test2 - * @return DataResponse - */ - public function execDataResponse($int, $bool, $test = 4, $test2 = 1) { - return new DataResponse([ - 'text' => [$int, $bool, $test, $test2] - ]); - } - - public function test(): Response { - return new DataResponse(); - } - - public function execStringBackedEnum(TestStringBackedEnum $enum) { - return [$enum]; - } - - public function execIntBackedEnum(TestIntBackedEnum $enum) { - return [$enum]; - } - - public function execNullableBackedEnum(?TestStringBackedEnum $enum = null) { - return [$enum]; - } -} - -/** - * Class DispatcherTest - * - * @package Test\AppFramework\Http - */ -#[\PHPUnit\Framework\Attributes\Group('DB')] -class DispatcherTest extends \Test\TestCase { - /** @var MiddlewareDispatcher */ - private $middlewareDispatcher; - /** @var Dispatcher */ - private $dispatcher; - private $controllerMethod; - /** @var Controller|MockObject */ - private $controller; - private $response; - /** @var IRequest|MockObject */ - private $request; - private $lastModified; - private $etag; - /** @var Http|MockObject */ - private $http; - private $reflector; - /** @var IConfig|MockObject */ - private $config; - /** @var LoggerInterface|MockObject */ - private $logger; - /** @var IEventLogger|MockObject */ - private $eventLogger; - /** @var ContainerInterface|MockObject */ - private $container; - /** @var IUserSession|MockObject */ - private $userSession; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->controllerMethod = 'test'; - - $this->config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->eventLogger = $this->createMock(IEventLogger::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->userSession = $this->createMock(IUserSession::class); - $app = $this->createMock(DIContainer::class); - $request = $this->createMock(Request::class); - $this->http = $this->createMock(\OC\AppFramework\Http::class); - - $this->middlewareDispatcher = $this->createMock(MiddlewareDispatcher::class); - $this->controller = $this->getMockBuilder(TestController::class) - ->onlyMethods([$this->controllerMethod]) - ->setConstructorArgs([$app, $request]) - ->getMock(); - - $this->request = $this->createMock(Request::class); - - $this->reflector = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - - $this->dispatcher = new Dispatcher( - $this->http, - $this->middlewareDispatcher, - $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - - $this->response = $this->createMock(Response::class); - - $this->lastModified = new \DateTime('now', new \DateTimeZone('GMT')); - $this->etag = 'hi'; - } - - /** - * @param string $out - * @param string $httpHeaders - */ - private function setMiddlewareExpectations($out = null, - $httpHeaders = null, $responseHeaders = [], - $ex = false, $catchEx = true) { - if ($ex) { - $exception = new \Exception(); - $this->middlewareDispatcher->expects($this->once()) - ->method('beforeController') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)) - ->willThrowException($exception); - if ($catchEx) { - $this->middlewareDispatcher->expects($this->once()) - ->method('afterException') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod), - $this->equalTo($exception)) - ->willReturn($this->response); - } else { - $this->middlewareDispatcher->expects($this->once()) - ->method('afterException') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod), - $this->equalTo($exception)) - ->willThrowException($exception); - return; - } - } else { - $this->middlewareDispatcher->expects($this->once()) - ->method('beforeController') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod)); - $this->controller->expects($this->once()) - ->method($this->controllerMethod) - ->willReturn($this->response); - } - - $this->response->expects($this->once()) - ->method('render') - ->willReturn($out); - $this->response->expects($this->once()) - ->method('getStatus') - ->willReturn(Http::STATUS_OK); - $this->response->expects($this->once()) - ->method('getHeaders') - ->willReturn($responseHeaders); - $this->http->expects($this->once()) - ->method('getStatusHeader') - ->with($this->equalTo(Http::STATUS_OK)) - ->willReturn($httpHeaders); - - $this->middlewareDispatcher->expects($this->once()) - ->method('afterController') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod), - $this->equalTo($this->response)) - ->willReturn($this->response); - - $this->middlewareDispatcher->expects($this->once()) - ->method('afterController') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod), - $this->equalTo($this->response)) - ->willReturn($this->response); - - $this->middlewareDispatcher->expects($this->once()) - ->method('beforeOutput') - ->with($this->equalTo($this->controller), - $this->equalTo($this->controllerMethod), - $this->equalTo($out)) - ->willReturn($out); - } - - public function testDispatcherReturnsArrayWith2Entries(): void { - $this->setMiddlewareExpectations(''); - - $response = $this->dispatcher->dispatch($this->controller, $this->controllerMethod); - $this->assertNull($response[0]); - $this->assertEquals([], $response[1]); - $this->assertNull($response[2]); - } - - public function testHeadersAndOutputAreReturned(): void { - $out = 'yo'; - $httpHeaders = 'Http'; - $responseHeaders = ['hell' => 'yeah']; - $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders); - - $response = $this->dispatcher->dispatch($this->controller, - $this->controllerMethod); - - $this->assertEquals($httpHeaders, $response[0]); - $this->assertEquals($responseHeaders, $response[1]); - $this->assertEquals($out, $response[3]); - } - - public function testExceptionCallsAfterException(): void { - $out = 'yo'; - $httpHeaders = 'Http'; - $responseHeaders = ['hell' => 'yeah']; - $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders, true); - - $response = $this->dispatcher->dispatch($this->controller, - $this->controllerMethod); - - $this->assertEquals($httpHeaders, $response[0]); - $this->assertEquals($responseHeaders, $response[1]); - $this->assertEquals($out, $response[3]); - } - - public function testExceptionThrowsIfCanNotBeHandledByAfterException(): void { - $out = 'yo'; - $httpHeaders = 'Http'; - $responseHeaders = ['hell' => 'yeah']; - $this->setMiddlewareExpectations($out, $httpHeaders, $responseHeaders, true, false); - - $this->expectException(\Exception::class); - $this->dispatcher->dispatch( - $this->controller, - $this->controllerMethod - ); - } - - private function dispatcherPassthrough() { - $this->middlewareDispatcher->expects($this->once()) - ->method('beforeController'); - $this->middlewareDispatcher->expects($this->once()) - ->method('afterController') - ->willReturnCallback(function ($a, $b, $in) { - return $in; - }); - $this->middlewareDispatcher->expects($this->once()) - ->method('beforeOutput') - ->willReturnCallback(function ($a, $b, $in) { - return $in; - }); - } - - public function testControllerParametersInjected(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'method' => 'POST' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('[3,false,4,1]', $response[3]); - } - - public function testControllerParametersInjectedStringBackedEnum(): void { - $this->request = new Request( - [ - 'post' => [ - 'enum' => 'foo', - ], - 'method' => 'POST', - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'execStringBackedEnum'); - - $this->assertEquals('["foo"]', $response[3]); - } - - public function testControllerParametersInjectedIntBackedEnum(): void { - $this->request = new Request( - [ - 'post' => [ - 'enum' => '2', - ], - 'method' => 'POST', - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'execIntBackedEnum'); - - $this->assertEquals('[2]', $response[3]); - } - - public function testControllerParametersInjectedNullableBackedEnumDefault(): void { - $this->request = new Request( - [ - 'post' => [], - 'method' => 'POST', - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'execNullableBackedEnum'); - - $this->assertEquals('[null]', $response[3]); - } - - public function testControllerParametersInjectedDefaultOverwritten(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - 'test2' => 7 - ], - 'method' => 'POST', - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('[3,false,4,7]', $response[3]); - } - - public function testResponseTransformedByUrlFormat(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'urlParams' => [ - 'format' => 'text' - ], - 'method' => 'GET' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); - } - - public function testResponseTransformsDataResponse(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'urlParams' => [ - 'format' => 'json' - ], - 'method' => 'GET' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'execDataResponse'); - - $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); - } - - public function testResponseTransformedByAcceptHeader(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'server' => [ - 'HTTP_ACCEPT' => 'application/text, test', - 'HTTP_CONTENT_TYPE' => 'application/x-www-form-urlencoded' - ], - 'method' => 'PUT' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); - } - - public function testResponseTransformedBySendingMultipartFormData(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'server' => [ - 'HTTP_ACCEPT' => 'application/text, test', - 'HTTP_CONTENT_TYPE' => 'multipart/form-data' - ], - 'method' => 'POST' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); - } - - public function testResponsePrimarilyTransformedByParameterFormat(): void { - $this->request = new Request( - [ - 'post' => [ - 'int' => '3', - 'bool' => 'false', - 'double' => 1.2, - ], - 'get' => [ - 'format' => 'text' - ], - 'server' => [ - 'HTTP_ACCEPT' => 'application/json, test' - ], - 'method' => 'POST' - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->dispatcher = new Dispatcher( - $this->http, $this->middlewareDispatcher, $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - $controller = new TestController('app', $this->request); - - // reflector is supposed to be called once - $this->dispatcherPassthrough(); - $response = $this->dispatcher->dispatch($controller, 'exec'); - - $this->assertEquals('{"text":[3,false,4,1]}', $response[3]); - } - - public static function rangeDataProvider(): array { - return [ - [PHP_INT_MIN, PHP_INT_MAX, 42, false], - [0, 12, -5, true], - [-12, 0, 5, true], - [1, 200, 0, true], - [-15, -5, 0, true], - [-15, 15, 0, false], - [0, 200, 0, false], - [-200, 0, 0, false], - [7, 14, 5, true], - [7, 14, 10, false], - [-14, -7, -10, false], - [null, null, -1, false], - - // $limit comes with default limits of self::DEFAULT_MIN (1) <= $limit <= self::DEFAULT_MAX (500) - [null, null, -1, true, 'limit'], - [null, null, -1, false, 'limit', -1], - [null, null, 0, true, 'limit'], - [null, null, 0, true, 'limit', -1], - [null, null, 1, false, 'limit'], - [null, null, 500, false, 'limit'], - [null, null, 501, true, 'limit'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('rangeDataProvider')] - public function testEnsureParameterValueSatisfiesRange(?int $min, ?int $max, int $input, bool $throw, string $param = 'myArgument', ?int $default = null): void { - $this->reflector = $this->createMock(ControllerMethodReflector::class); - if ($min === null && $max === null) { - $this->reflector->expects($this->any()) - ->method('getRange') - ->willReturn(null); - } else { - $this->reflector->expects($this->any()) - ->method('getRange') - ->willReturn([ - 'min' => $min, - 'max' => $max, - ]); - } - - $this->dispatcher = new Dispatcher( - $this->http, - $this->middlewareDispatcher, - $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - - if ($throw) { - $this->expectException(ParameterOutOfRangeException::class); - } - - self::invokePrivate($this->dispatcher, 'ensureParameterValueSatisfiesRange', [$param, $input, $default]); - if (!$throw) { - // do not mark this test risky - $this->assertTrue(true); - } - } - - public static function stringConstraintDataProvider(): array { - return [ - [true, null, false], - [true, 'non-empty-string', false], - [false, 'non-empty-string', true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('stringConstraintDataProvider')] - public function testEnsureParameterValueSatisfiesStringConstraint(bool $satisfies, ?string $constraint, bool $throw): void { - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->reflector->expects($this->any()) - ->method('satisfiesStringConstraint') - ->willReturn($satisfies); - $this->reflector->expects($this->any()) - ->method('getStringConstraint') - ->willReturn($constraint); - - $this->dispatcher = new Dispatcher( - $this->http, - $this->middlewareDispatcher, - $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - - if ($throw) { - $this->expectException(InvalidStringParameterException::class); - } - - self::invokePrivate($this->dispatcher, 'ensureParameterValueSatisfiesStringConstraint', ['myArgument', '']); - if (!$throw) { - // do not mark this test risky - $this->assertTrue(true); - } - } - - public static function backedEnumDataProvider(): array { - return [ - [TestStringBackedEnum::class, 'foo', TestStringBackedEnum::Foo], - [TestStringBackedEnum::class, 'bar', TestStringBackedEnum::Bar], - [TestIntBackedEnum::class, '1', TestIntBackedEnum::One], - [TestIntBackedEnum::class, 1, TestIntBackedEnum::One], - [TestIntBackedEnum::class, 2, TestIntBackedEnum::Two], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('backedEnumDataProvider')] - public function testResolveBackedEnumValue(string $enumClass, string|int $input, \BackedEnum $expected): void { - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->dispatcher = new Dispatcher( - $this->http, - $this->middlewareDispatcher, - $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - - $result = self::invokePrivate($this->dispatcher, 'resolveBackedEnumValue', ['myArgument', $enumClass, $input]); - $this->assertSame($expected, $result); - } - - public static function invalidBackedEnumDataProvider(): array { - return [ - [TestStringBackedEnum::class, 'invalid'], - [TestIntBackedEnum::class, 'not-a-number'], - [TestIntBackedEnum::class, 99], - [TestStringBackedEnum::class, ['array']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('invalidBackedEnumDataProvider')] - public function testResolveBackedEnumValueThrowsOnInvalidValue(string $enumClass, mixed $input): void { - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->dispatcher = new Dispatcher( - $this->http, - $this->middlewareDispatcher, - $this->reflector, - $this->request, - $this->config, - Server::get(IDBConnection::class), - $this->logger, - $this->eventLogger, - $this->container, - $this->userSession, - ); - - $this->expectException(InvalidEnumParameterException::class); - - self::invokePrivate($this->dispatcher, 'resolveBackedEnumValue', ['myArgument', $enumClass, $input]); - } -} --- a/tests/lib/AppFramework/Http/DownloadResponseTest.php +++ /dev/null @@ -1,59 +0,0 @@ -getHeaders(); - - $this->assertEquals('attachment; filename=file', $headers['Content-Disposition']); - $this->assertEquals('content', $headers['Content-Type']); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('filenameEncodingProvider')] - public function testFilenameEncoding(string $input, string $expectedDisposition): void { - $response = new ChildDownloadResponse($input, 'content'); - $headers = $response->getHeaders(); - - $this->assertEquals($expectedDisposition, $headers['Content-Disposition']); - } - - public static function filenameEncodingProvider(): array { - return [ - ['TestName.txt', 'attachment; filename=TestName.txt'], - ['A "Quoted" Filename.txt', 'attachment; filename="A \"Quoted\" Filename.txt"'], - ['A "Quoted" Filename.txt', 'attachment; filename="A \"Quoted\" Filename.txt"'], - ['A "Quoted" Filename With A Backslash \\.txt', 'attachment; filename="A \"Quoted\" Filename With A Backslash -.txt"'], - ['A "Very" Weird Filename \ / & <> " >\'""""\.text', 'attachment; filename="A \"Very\" Weird Filename - - & <> \" >\'\"\"\"\"-.text"'], - ['\\\\\\\\\\\\', 'attachment; filename=------'], - ]; - } - - public function testSpecialCharactersInFilename(): void { - $filename = 'document "draft" with; special&chars.pdf'; - $response = new ChildDownloadResponse($filename, 'application/pdf'); - $headers = $response->getHeaders(); - - $this->assertEquals( - 'attachment; filename="document \"draft\" with; special&chars.pdf"', - $headers['Content-Disposition'] - ); - } -} --- a/tests/lib/AppFramework/Http/EmptyContentSecurityPolicyTest.php +++ /dev/null @@ -1,453 +0,0 @@ -contentSecurityPolicy = new EmptyContentSecurityPolicy(); - } - - public function testGetPolicyDefault(): void { - $defaultPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - $this->assertSame($defaultPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowScriptDomainMultipleStacked(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowScriptDomain('www.nextcloud.org')->disallowScriptDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyScriptAllowWasmEval(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'wasm-unsafe-eval';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->allowEvalWasm(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowStyleDomainMultipleStacked(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowStyleDomain('www.nextcloud.org')->disallowStyleDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleAllowInline(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src 'unsafe-inline';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->allowInlineStyle(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleAllowInlineWithDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';style-src www.nextcloud.com 'unsafe-inline';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedStyleDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->allowInlineStyle(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyStyleDisallowInline(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->allowInlineStyle(false); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyImageDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyImageDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';img-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowImageDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedImageDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowImageDomain('www.nextcloud.org')->disallowImageDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFontDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFontDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';font-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFontDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFontDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFontDomain('www.nextcloud.org')->disallowFontDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyConnectDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyConnectDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';connect-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowConnectDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedConnectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowConnectDomain('www.nextcloud.org')->disallowConnectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyMediaDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyMediaDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';media-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowMediaDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedMediaDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowMediaDomain('www.nextcloud.org')->disallowMediaDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyObjectDomainValid(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyObjectDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';object-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowObjectDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedObjectDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowObjectDomain('www.nextcloud.org')->disallowObjectDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetAllowedFrameDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyFrameDomainValidMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomainMultiple(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-src www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyDisallowFrameDomainMultipleStakes(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedFrameDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->disallowFrameDomain('www.nextcloud.org')->disallowFrameDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithJsNonceAndScriptDomains(): void { - $nonce = base64_encode('MyJsNonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce' www.nextcloud.com www.nextcloud.org;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithJsNonceAndStrictDynamic(): void { - $nonce = base64_encode('MyJsNonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->useStrictDynamic(true); - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithJsNonceAndStrictDynamicAndStrictDynamicOnScripts(): void { - $nonce = base64_encode('MyJsNonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->useStrictDynamic(true); - $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); - $this->contentSecurityPolicy->useJsNonce($nonce); - // Should be same as `testGetPolicyWithJsNonceAndStrictDynamic` because of fallback - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithJsNonceAndStrictDynamicOnScripts(): void { - $nonce = base64_encode('MyJsNonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce' www.nextcloud.com;script-src-elem 'strict-dynamic' 'nonce-$nonce' www.nextcloud.com;frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain('www.nextcloud.com'); - $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithStrictDynamicOnScripts(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->useStrictDynamicOnScripts(true); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithJsNonceAndSelfScriptDomain(): void { - $nonce = base64_encode('MyJsNonce'); - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'nonce-$nonce';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->useJsNonce($nonce); - $this->contentSecurityPolicy->addAllowedScriptDomain("'self'"); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithoutJsNonceAndSelfScriptDomain(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';frame-ancestors 'none'"; - - $this->contentSecurityPolicy->addAllowedScriptDomain("'self'"); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithReportUri(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none';report-uri https://my-report-uri.com"; - - $this->contentSecurityPolicy->addReportTo('https://my-report-uri.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } - - public function testGetPolicyWithMultipleReportUri(): void { - $expectedPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none';report-uri https://my-report-uri.com https://my-other-report-uri.com"; - - $this->contentSecurityPolicy->addReportTo('https://my-report-uri.com'); - $this->contentSecurityPolicy->addReportTo('https://my-other-report-uri.com'); - $this->assertSame($expectedPolicy, $this->contentSecurityPolicy->buildPolicy()); - } -} --- a/tests/lib/AppFramework/Http/EmptyFeaturePolicyTest.php +++ /dev/null @@ -1,117 +0,0 @@ -policy = new EmptyFeaturePolicy(); - } - - public function testGetPolicyDefault(): void { - $defaultPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyAutoplayDomainValid(): void { - $expectedPolicy = "autoplay www.nextcloud.com;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyAutoplayDomainValidMultiple(): void { - $expectedPolicy = "autoplay www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); - $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyCameraDomainValid(): void { - $expectedPolicy = "autoplay 'none';camera www.nextcloud.com;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedCameraDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyCameraDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'none';camera www.nextcloud.com www.nextcloud.org;fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedCameraDomain('www.nextcloud.com'); - $this->policy->addAllowedCameraDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyFullScreenDomainValid(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyFullScreenDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); - $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyGeoLocationDomainValid(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; - - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyGeoLocationDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; - - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyMicrophoneDomainValid(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; - - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyMicrophoneDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; - - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyPaymentDomainValid(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com"; - - $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyPaymentDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; - - $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); - $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } -} --- a/tests/lib/AppFramework/Http/FeaturePolicyTest.php +++ /dev/null @@ -1,117 +0,0 @@ -policy = new FeaturePolicy(); - } - - public function testGetPolicyDefault(): void { - $defaultPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; - $this->assertSame($defaultPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyAutoplayDomainValid(): void { - $expectedPolicy = "autoplay 'self' www.nextcloud.com;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyAutoplayDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self' www.nextcloud.com www.nextcloud.org;camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedAutoplayDomain('www.nextcloud.com'); - $this->policy->addAllowedAutoplayDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyCameraDomainValid(): void { - $expectedPolicy = "autoplay 'self';camera www.nextcloud.com;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedCameraDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyCameraDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self';camera www.nextcloud.com www.nextcloud.org;fullscreen 'self';geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedCameraDomain('www.nextcloud.com'); - $this->policy->addAllowedCameraDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyFullScreenDomainValid(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com;geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyFullScreenDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self' www.nextcloud.com www.nextcloud.org;geolocation 'none';microphone 'none';payment 'none'"; - - $this->policy->addAllowedFullScreenDomain('www.nextcloud.com'); - $this->policy->addAllowedFullScreenDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyGeoLocationDomainValid(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com;microphone 'none';payment 'none'"; - - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyGeoLocationDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation www.nextcloud.com www.nextcloud.org;microphone 'none';payment 'none'"; - - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.com'); - $this->policy->addAllowedGeoLocationDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyMicrophoneDomainValid(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com;payment 'none'"; - - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyMicrophoneDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone www.nextcloud.com www.nextcloud.org;payment 'none'"; - - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.com'); - $this->policy->addAllowedMicrophoneDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyPaymentDomainValid(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com"; - - $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } - - public function testGetPolicyPaymentDomainValidMultiple(): void { - $expectedPolicy = "autoplay 'self';camera 'none';fullscreen 'self';geolocation 'none';microphone 'none';payment www.nextcloud.com www.nextcloud.org"; - - $this->policy->addAllowedPaymentDomain('www.nextcloud.com'); - $this->policy->addAllowedPaymentDomain('www.nextcloud.org'); - $this->assertSame($expectedPolicy, $this->policy->buildPolicy()); - } -} --- a/tests/lib/AppFramework/Http/FileDisplayResponseTest.php +++ /dev/null @@ -1,195 +0,0 @@ -file = $this->createMock(File::class); - $this->file->expects($this->once()) - ->method('getETag') - ->willReturn('myETag'); - $this->file->expects($this->once()) - ->method('getName') - ->willReturn('myFileName'); - $this->file->expects($this->once()) - ->method('getMTime') - ->willReturn(1464825600); - - $this->response = new FileDisplayResponse($this->file); - } - - public function testHeader(): void { - $headers = $this->response->getHeaders(); - $this->assertArrayHasKey('Content-Disposition', $headers); - $this->assertSame('inline; filename="myFileName"', $headers['Content-Disposition']); - } - - public function testETag(): void { - $this->assertSame('myETag', $this->response->getETag()); - } - - public function testLastModified(): void { - $lastModified = $this->response->getLastModified(); - $this->assertNotNull($lastModified); - $this->assertSame(1464825600, $lastModified->getTimestamp()); - } - - public function test304(): void { - $output = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - - $output->expects($this->any()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_NOT_MODIFIED); - $output->expects($this->never()) - ->method('setOutput'); - $this->file->expects($this->never()) - ->method('getContent'); - - $this->response->callback($output); - } - - public function testNon304(): void { - $resource = fopen('php://memory', 'w+b'); - fwrite($resource, 'my data'); - rewind($resource); - - $this->file->expects($this->once()) - ->method('fopen') - ->willReturn($resource); - $this->file->expects($this->any()) - ->method('getSize') - ->willReturn(7); - - $output = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - $output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $output->expects($this->once()) - ->method('setReadFile') - ->with($this->equalTo($resource)); - $output->expects($this->once()) - ->method('setHeader') - ->with($this->equalTo('Content-Length: 7')); - - $this->response->callback($output); - } - - public function testFileNotFound(): void { - $this->file->expects($this->once()) - ->method('fopen') - ->willReturn(false); - - $output = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - $output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $output->expects($this->once()) - ->method('setHttpResponseCode') - ->with($this->equalTo(Http::STATUS_NOT_FOUND)); - $output->expects($this->once()) - ->method('setOutput') - ->with($this->equalTo('')); - - $this->response->callback($output); - } - - public function testSimpleFileNotFound(): void { - $file = $this->createMock(ISimpleFile::class); - $file->expects($this->once()) - ->method('getETag') - ->willReturn('myETag'); - $file->expects($this->once()) - ->method('getName') - ->willReturn('myFileName'); - $file->expects($this->once()) - ->method('getMTime') - ->willReturn(1464825600); - $file->expects($this->once()) - ->method('read') - ->willReturn(false); - - $response = new FileDisplayResponse($file); - - $output = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - $output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $output->expects($this->once()) - ->method('setHttpResponseCode') - ->with($this->equalTo(Http::STATUS_NOT_FOUND)); - $output->expects($this->once()) - ->method('setOutput') - ->with($this->equalTo('')); - - $response->callback($output); - } - - public function testSimpleFile(): void { - $file = $this->createMock(ISimpleFile::class); - $file->expects($this->once()) - ->method('getETag') - ->willReturn('myETag'); - $file->expects($this->once()) - ->method('getName') - ->willReturn('myFileName'); - $file->expects($this->once()) - ->method('getMTime') - ->willReturn(1464825600); - - $resource = fopen('php://memory', 'w+b'); - fwrite($resource, 'my data'); - rewind($resource); - - $file->expects($this->once()) - ->method('read') - ->willReturn($resource); - $file->expects($this->any()) - ->method('getSize') - ->willReturn(7); - - $response = new FileDisplayResponse($file); - - $output = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - $output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $output->expects($this->once()) - ->method('setReadFile') - ->with($this->equalTo($resource)); - $output->expects($this->once()) - ->method('setHeader') - ->with($this->equalTo('Content-Length: 7')); - - $response->callback($output); - } -} --- a/tests/lib/AppFramework/Http/HttpTest.php +++ /dev/null @@ -1,47 +0,0 @@ -server = []; - $this->http = new Http($this->server); - } - - public function testProtocol(): void { - $header = $this->http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT); - $this->assertEquals('HTTP/1.1 307 Temporary Redirect', $header); - } - - public function testProtocol10(): void { - $this->http = new Http($this->server, 'HTTP/1.0'); - $header = $this->http->getStatusHeader(Http::STATUS_OK); - $this->assertEquals('HTTP/1.0 200 OK', $header); - } - - public function testTempRedirectBecomesFoundInHttp10(): void { - $http = new Http([], 'HTTP/1.0'); - - $header = $http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT); - $this->assertEquals('HTTP/1.0 302 Found', $header); - } - // TODO: write unittests for http codes -} --- a/tests/lib/AppFramework/Http/JSONResponseTest.php +++ /dev/null @@ -1,95 +0,0 @@ -json = new JSONResponse(); - } - - public function testHeader(): void { - $headers = $this->json->getHeaders(); - $this->assertEquals('application/json; charset=utf-8', $headers['Content-Type']); - } - - public function testSetData(): void { - $params = ['hi', 'yo']; - $this->json->setData($params); - - $this->assertEquals(['hi', 'yo'], $this->json->getData()); - } - - public function testSetRender(): void { - $params = ['test' => 'hi']; - $this->json->setData($params); - - $expected = '{"test":"hi"}'; - - $this->assertEquals($expected, $this->json->render()); - } - - public static function renderDataProvider(): array { - return [ - [ - ['test' => 'hi'], '{"test":"hi"}', - ], - [ - ['

test' => '

hi'], '{"\u003Ch1\u003Etest":"\u003Ch1\u003Ehi"}', - ], - ]; - } - - /** - * @param array $input - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('renderDataProvider')] - public function testRender(array $input, $expected): void { - $this->json->setData($input); - $this->assertEquals($expected, $this->json->render()); - } - - public function testRenderWithNonUtf8Encoding(): void { - $this->expectException(\JsonException::class); - $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); - - $params = ['test' => hex2bin('e9')]; - $this->json->setData($params); - $this->json->render(); - } - - public function testConstructorAllowsToSetData(): void { - $data = ['hi']; - $code = 300; - $response = new JSONResponse($data, $code); - - $expected = '["hi"]'; - $this->assertEquals($expected, $response->render()); - $this->assertEquals($code, $response->getStatus()); - } - - public function testChainability(): void { - $params = ['hi', 'yo']; - $this->json->setData($params) - ->setStatus(Http::STATUS_NOT_FOUND); - - $this->assertEquals(Http::STATUS_NOT_FOUND, $this->json->getStatus()); - $this->assertEquals(['hi', 'yo'], $this->json->getData()); - } -} --- a/tests/lib/AppFramework/Http/OutputTest.php +++ /dev/null @@ -1,30 +0,0 @@ -expectOutputString('foo'); - $output = new Output(''); - $output->setOutput('foo'); - } - - public function testSetReadfile(): void { - $this->expectOutputString(file_get_contents(__FILE__)); - $output = new Output(''); - $output->setReadfile(__FILE__); - } - - public function testSetReadfileStream(): void { - $this->expectOutputString(file_get_contents(__FILE__)); - $output = new Output(''); - $output->setReadfile(fopen(__FILE__, 'r')); - } -} --- a/tests/lib/AppFramework/Http/PublicTemplateResponseTest.php +++ /dev/null @@ -1,60 +0,0 @@ - 'value']); - $this->assertEquals(['key' => 'value'], $template->getParams()); - } - - public function testAdditionalElements(): void { - $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); - $template->setHeaderTitle('Header'); - $template->setHeaderDetails('Details'); - $this->assertEquals(['key' => 'value'], $template->getParams()); - $this->assertEquals('Header', $template->getHeaderTitle()); - $this->assertEquals('Details', $template->getHeaderDetails()); - } - - public function testActionSingle(): void { - $actions = [ - new SimpleMenuAction('link', 'Download', 'download', 'downloadLink', 0) - ]; - $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); - $template->setHeaderActions($actions); - $this->assertEquals(['key' => 'value'], $template->getParams()); - $this->assertEquals($actions[0], $template->getPrimaryAction()); - $this->assertEquals(1, $template->getActionCount()); - $this->assertEquals([], $template->getOtherActions()); - } - - public function testActionMultiple(): void { - $actions = [ - new SimpleMenuAction('link1', 'Download1', 'download1', 'downloadLink1', 100), - new SimpleMenuAction('link2', 'Download2', 'download2', 'downloadLink2', 20), - new SimpleMenuAction('link3', 'Download3', 'download3', 'downloadLink3', 0) - ]; - $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); - $template->setHeaderActions($actions); - $this->assertEquals(['key' => 'value'], $template->getParams()); - $this->assertEquals($actions[2], $template->getPrimaryAction()); - $this->assertEquals(3, $template->getActionCount()); - $this->assertEquals([$actions[1], $actions[0]], $template->getOtherActions()); - } - - public function testGetRenderAs(): void { - $template = new PublicTemplateResponse('app', 'home', ['key' => 'value']); - $this->assertEquals(['key' => 'value'], $template->getParams()); - $this->assertEquals('public', $template->getRenderAs()); - } -} --- a/tests/lib/AppFramework/Http/RedirectResponseTest.php +++ /dev/null @@ -1,36 +0,0 @@ -response = new RedirectResponse('/url'); - } - - public function testHeaders(): void { - $headers = $this->response->getHeaders(); - $this->assertEquals('/url', $headers['Location']); - $this->assertEquals(Http::STATUS_SEE_OTHER, - $this->response->getStatus()); - } - - public function testGetRedirectUrl(): void { - $this->assertEquals('/url', $this->response->getRedirectUrl()); - } -} --- a/tests/lib/AppFramework/Http/RequestIdTest.php +++ /dev/null @@ -1,58 +0,0 @@ -secureRandom = $this->createMock(ISecureRandom::class); - } - - public function testGetIdWithModUnique(): void { - $requestId = new RequestId( - 'GeneratedUniqueIdByModUnique', - $this->secureRandom - ); - - $this->secureRandom->expects($this->never()) - ->method('generate'); - - $this->assertSame('GeneratedUniqueIdByModUnique', $requestId->getId()); - $this->assertSame('GeneratedUniqueIdByModUnique', $requestId->getId()); - } - - public function testGetIdWithoutModUnique(): void { - $requestId = new RequestId( - '', - $this->secureRandom - ); - - $this->secureRandom->expects($this->once()) - ->method('generate') - ->with('20') - ->willReturn('GeneratedByNextcloudItself1'); - - $this->assertSame('GeneratedByNextcloudItself1', $requestId->getId()); - $this->assertSame('GeneratedByNextcloudItself1', $requestId->getId()); - } -} --- a/tests/lib/AppFramework/Http/RequestStream.php +++ /dev/null @@ -1,117 +0,0 @@ -varname = $url['host'] ?? ''; - $this->position = 0; - - return true; - } - - public function stream_read(int $count): string { - $ret = substr($GLOBALS[$this->varname], $this->position, $count); - $this->position += strlen($ret); - return $ret; - } - - public function stream_write(string $data): int { - $left = substr($GLOBALS[$this->varname], 0, $this->position); - $right = substr($GLOBALS[$this->varname], $this->position + strlen($data)); - $GLOBALS[$this->varname] = $left . $data . $right; - $this->position += strlen($data); - return strlen($data); - } - - public function stream_tell(): int { - return $this->position; - } - - public function stream_eof(): bool { - return $this->position >= strlen($GLOBALS[$this->varname]); - } - - public function stream_seek(int $offset, int $whence = SEEK_SET): bool { - switch ($whence) { - case SEEK_SET: - if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) { - $this->position = $offset; - return true; - } else { - return false; - } - break; - - case SEEK_CUR: - if ($offset >= 0) { - $this->position += $offset; - return true; - } else { - return false; - } - break; - - case SEEK_END: - if (strlen($GLOBALS[$this->varname]) + $offset >= 0) { - $this->position = strlen($GLOBALS[$this->varname]) + $offset; - return true; - } else { - return false; - } - break; - - default: - return false; - } - } - - public function stream_stat(): array { - $size = strlen($GLOBALS[$this->varname]); - $time = time(); - $data = [ - 'dev' => 0, - 'ino' => 0, - 'mode' => 0777, - 'nlink' => 1, - 'uid' => 0, - 'gid' => 0, - 'rdev' => '', - 'size' => $size, - 'atime' => $time, - 'mtime' => $time, - 'ctime' => $time, - 'blksize' => -1, - 'blocks' => -1, - ]; - return array_values($data) + $data; - //return false; - } - - public function stream_metadata(string $path, int $option, $var): bool { - if ($option === STREAM_META_TOUCH) { - $url = parse_url($path); - $varname = $url['host'] ?? ''; - if (!isset($GLOBALS[$varname])) { - $GLOBALS[$varname] = ''; - } - return true; - } - return false; - } -} --- a/tests/lib/AppFramework/Http/RequestTest.php +++ /dev/null @@ -1,2318 +0,0 @@ -requestId = $this->createMock(IRequestId::class); - $this->config = $this->createMock(IConfig::class); - $this->csrfTokenManager = $this->getMockBuilder(CsrfTokenManager::class) - ->disableOriginalConstructor() - ->getMock(); - } - - #[\Override] - protected function tearDown(): void { - stream_wrapper_unregister('fakeinput'); - parent::tearDown(); - } - - public function testRequestAccessors(): void { - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'method' => 'GET', - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - // Countable - $this->assertSame(2, count($request)); - // Array access - $this->assertSame('Joey', $request['nickname']); - // "Magic" accessors - $this->assertSame('Joey', $request->{'nickname'}); - $this->assertTrue(isset($request['nickname'])); - $this->assertTrue(isset($request->{'nickname'})); - $this->assertFalse(isset($request->{'flickname'})); - // Only testing 'get', but same approach for post, files etc. - $this->assertSame('Joey', $request->get['nickname']); - // Always returns null if variable not set. - $this->assertSame(null, $request->{'flickname'}); - } - - // urlParams has precedence over POST which has precedence over GET - public function testPrecedence(): void { - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'post' => ['name' => 'Jane Doe', 'nickname' => 'Janey'], - 'urlParams' => ['user' => 'jw', 'name' => 'Johnny Weissmüller'], - 'method' => 'GET' - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame(3, count($request)); - $this->assertSame('Janey', $request->{'nickname'}); - $this->assertSame('Johnny Weissmüller', $request->{'name'}); - } - - public function testImmutableArrayAccess(): void { - $this->expectException(\RuntimeException::class); - - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'method' => 'GET' - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $request['nickname'] = 'Janey'; - } - - public function testImmutableMagicAccess(): void { - $this->expectException(\RuntimeException::class); - - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'method' => 'GET' - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $request->{'nickname'} = 'Janey'; - } - - public function testGetTheMethodRight(): void { - $this->expectException(\LogicException::class); - - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'method' => 'GET', - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $request->post; - } - - public function testTheMethodIsRight(): void { - $vars = [ - 'get' => ['name' => 'John Q. Public', 'nickname' => 'Joey'], - 'method' => 'GET', - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('GET', $request->method); - $result = $request->get; - $this->assertSame('John Q. Public', $result['name']); - $this->assertSame('Joey', $result['nickname']); - } - - public function testJsonPost(): void { - global $data; - $data = '{"name": "John Q. Public", "nickname": "Joey"}'; - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('POST', $request->method); - $result = $request->post; - $this->assertSame('John Q. Public', $result['name']); - $this->assertSame('Joey', $result['nickname']); - $this->assertSame('Joey', $request->params['nickname']); - $this->assertSame('Joey', $request['nickname']); - } - - public function testScimJsonPost(): void { - global $data; - $data = '{"userName":"testusername", "displayName":"Example User"}'; - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('POST', $request->method); - $result = $request->post; - $this->assertSame('testusername', $result['userName']); - $this->assertSame('Example User', $result['displayName']); - $this->assertSame('Example User', $request->params['displayName']); - $this->assertSame('Example User', $request['displayName']); - } - - public function testCustomJsonPost(): void { - global $data; - $data = '{"propertyA":"sometestvalue", "propertyB":"someothertestvalue"}'; - - // Note: the content type used here is fictional and intended to check if the regex for JSON content types works fine - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('POST', $request->method); - $result = $request->post; - $this->assertSame('sometestvalue', $result['propertyA']); - $this->assertSame('someothertestvalue', $result['propertyB']); - } - - public static function dataNotJsonData(): array { - return [ - ['this is not valid json'], - ['"just a string"'], - ['{"just a string"}'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNotJsonData')] - public function testNotJsonPost(string $testData): void { - global $data; - $data = $testData; - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertEquals('POST', $request->method); - $result = $request->post; - // ensure there's no error attempting to decode the content - } - - public function testNotScimJsonPost(): void { - global $data; - $data = 'this is not valid scim json'; - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertEquals('POST', $request->method); - $result = $request->post; - // ensure there's no error attempting to decode the content - } - - public function testNotCustomJsonPost(): void { - global $data; - $data = 'this is not valid json'; - $vars = [ - 'method' => 'POST', - 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'] - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertEquals('POST', $request->method); - $result = $request->post; - // ensure there's no error attempting to decode the content - } - - public function testPatch(): void { - global $data; - $data = http_build_query(['name' => 'John Q. Public', 'nickname' => 'Joey'], '', '&'); - - $vars = [ - 'method' => 'PATCH', - 'server' => ['CONTENT_TYPE' => 'application/x-www-form-urlencoded'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PATCH', $request->method); - $result = $request->patch; - - $this->assertSame('John Q. Public', $result['name']); - $this->assertSame('Joey', $result['nickname']); - } - - public function testJsonPatchAndPut(): void { - global $data; - - // PUT content - $data = '{"name": "John Q. Public", "nickname": "Joey"}'; - $vars = [ - 'method' => 'PUT', - 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PUT', $request->method); - $result = $request->put; - - $this->assertSame('John Q. Public', $result['name']); - $this->assertSame('Joey', $result['nickname']); - - // PATCH content - $data = '{"name": "John Q. Public", "nickname": null}'; - $vars = [ - 'method' => 'PATCH', - 'server' => ['CONTENT_TYPE' => 'application/json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PATCH', $request->method); - $result = $request->patch; - - $this->assertSame('John Q. Public', $result['name']); - $this->assertSame(null, $result['nickname']); - } - - public function testScimJsonPatchAndPut(): void { - global $data; - - // PUT content - $data = '{"userName": "sometestusername", "displayName": "Example User"}'; - $vars = [ - 'method' => 'PUT', - 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PUT', $request->method); - $result = $request->put; - - $this->assertSame('sometestusername', $result['userName']); - $this->assertSame('Example User', $result['displayName']); - - // PATCH content - $data = '{"userName": "sometestusername", "displayName": null}'; - $vars = [ - 'method' => 'PATCH', - 'server' => ['CONTENT_TYPE' => 'application/scim+json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PATCH', $request->method); - $result = $request->patch; - - $this->assertSame('sometestusername', $result['userName']); - $this->assertSame(null, $result['displayName']); - } - - public function testCustomJsonPatchAndPut(): void { - global $data; - - // PUT content - $data = '{"propertyA": "sometestvalue", "propertyB": "someothertestvalue"}'; - $vars = [ - 'method' => 'PUT', - 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PUT', $request->method); - $result = $request->put; - - $this->assertSame('sometestvalue', $result['propertyA']); - $this->assertSame('someothertestvalue', $result['propertyB']); - - // PATCH content - $data = '{"propertyA": "sometestvalue", "propertyB": null}'; - $vars = [ - 'method' => 'PATCH', - 'server' => ['CONTENT_TYPE' => 'application/custom-type+json; utf-8'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PATCH', $request->method); - $result = $request->patch; - - $this->assertSame('sometestvalue', $result['propertyA']); - $this->assertSame(null, $result['propertyB']); - } - - public function testPutStream(): void { - global $data; - $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); - - $vars = [ - 'put' => $data, - 'method' => 'PUT', - 'server' => [ - 'CONTENT_TYPE' => 'image/png', - 'CONTENT_LENGTH' => (string)strlen($data) - ], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('PUT', $request->method); - $resource = $request->put; - $contents = stream_get_contents($resource); - $this->assertSame($data, $contents); - - try { - $resource = $request->put; - } catch (\LogicException $e) { - return; - } - $this->fail('Expected LogicException.'); - } - - public function testSetUrlParameters(): void { - $vars = [ - 'post' => [], - 'method' => 'POST', - 'urlParams' => ['id' => '2'], - ]; - - $request = new Request( - $vars, - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $newParams = ['id' => '3', 'test' => 'test2']; - $request->setUrlParameters($newParams); - $this->assertSame('test2', $request->getParam('test')); - $this->assertEquals('3', $request->getParam('id')); - $this->assertEquals('3', $request->getParams()['id']); - } - - public static function dataGetRemoteAddress(): array { - return [ - 'IPv4 without trusted remote' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - [], - [], - '10.0.0.2', - ], - 'IPv4 without trusted headers' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['10.0.0.2'], - [], - '10.0.0.2', - ], - 'IPv4 with single trusted remote' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['10.0.0.2'], - ['HTTP_X_FORWARDED'], - '10.4.0.4', - ], - 'IPv6 with single trusted remote' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['2001:db8:85a3:8d3:1319:8a2e:370:7348'], - ['HTTP_X_FORWARDED'], - '10.4.0.4', - ], - 'IPv4 with multiple trusted remotes' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4, ::1', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['10.0.0.2', '::1'], - ['HTTP_X_FORWARDED'], - '10.4.0.4', - ], - 'IPv4 order of forwarded-for headers' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['10.0.0.2'], - [ - 'HTTP_X_FORWARDED', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_CLIENT_IP', - ], - '192.168.0.233', - ], - 'IPv4 order of forwarded-for headers (reversed)' => [ - [ - 'REMOTE_ADDR' => '10.0.0.2', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['10.0.0.2'], - [ - 'HTTP_CLIENT_IP', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_FORWARDED', - ], - '10.4.0.4', - ], - 'IPv6 order of forwarded-for headers' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['2001:db8:85a3:8d3:1319:8a2e:370:7348'], - [ - 'HTTP_X_FORWARDED', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_CLIENT_IP', - ], - '192.168.0.233', - ], - 'IPv4 matching CIDR of trusted proxy' => [ - [ - 'REMOTE_ADDR' => '192.168.3.99', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['192.168.2.0/24'], - ['HTTP_X_FORWARDED_FOR'], - '192.168.3.99', - ], - 'IPv6 matching CIDR of trusted proxy' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a21:370:7348', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['2001:db8:85a3:8d3:1319:8a20::/95'], - ['HTTP_X_FORWARDED_FOR'], - '192.168.0.233', - ], - 'IPv6 not matching CIDR of trusted proxy' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['fd::/8'], - [], - '2001:db8:85a3:8d3:1319:8a2e:370:7348', - ], - 'IPv6 with invalid trusted proxy' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', - 'HTTP_X_FORWARDED' => '10.4.0.5, 10.4.0.4', - 'HTTP_X_FORWARDED_FOR' => '192.168.0.233', - ], - ['fx::/8'], - [], - '2001:db8:85a3:8d3:1319:8a2e:370:7348', - ], - 'IPv4 forwarded for IPv6' => [ - [ - 'REMOTE_ADDR' => '192.168.2.99', - 'HTTP_X_FORWARDED_FOR' => '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - ], - ['192.168.2.0/24'], - ['HTTP_X_FORWARDED_FOR'], - '2001:db8:85a3:8d3:1319:8a2e:370:7348', - ], - 'IPv4 with port' => [ - [ - 'REMOTE_ADDR' => '2001:db8:85a3:8d3:1319:8a2e:370:7348', - 'HTTP_X_FORWARDED_FOR' => '192.168.2.99:8080', - ], - ['2001:db8::/8'], - ['HTTP_X_FORWARDED_FOR'], - '192.168.2.99', - ], - 'IPv6 with port' => [ - [ - 'REMOTE_ADDR' => '192.168.2.99', - 'HTTP_X_FORWARDED_FOR' => '[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080', - ], - ['192.168.2.0/24'], - ['HTTP_X_FORWARDED_FOR'], - '2001:db8:85a3:8d3:1319:8a2e:370:7348', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetRemoteAddress')] - public function testGetRemoteAddress(array $headers, array $trustedProxies, array $forwardedForHeaders, string $expected): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['trusted_proxies', [], $trustedProxies], - ['forwarded_for_headers', ['HTTP_X_FORWARDED_FOR'], $forwardedForHeaders], - ]); - - $request = new Request( - [ - 'server' => $headers, - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getRemoteAddress()); - } - - public static function dataHttpProtocol(): array { - return [ - // Valid HTTP 1.0 - ['HTTP/1.0', 'HTTP/1.0'], - ['http/1.0', 'HTTP/1.0'], - ['HTTp/1.0', 'HTTP/1.0'], - - // Valid HTTP 1.1 - ['HTTP/1.1', 'HTTP/1.1'], - ['http/1.1', 'HTTP/1.1'], - ['HTTp/1.1', 'HTTP/1.1'], - - // Valid HTTP 2.0 - ['HTTP/2', 'HTTP/2'], - ['http/2', 'HTTP/2'], - ['HTTp/2', 'HTTP/2'], - - // Invalid - ['HTTp/394', 'HTTP/1.1'], - ['InvalidProvider/1.1', 'HTTP/1.1'], - [null, 'HTTP/1.1'], - ['', 'HTTP/1.1'], - ]; - } - - /** - * - * @param mixed $input - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataHttpProtocol')] - public function testGetHttpProtocol($input, $expected): void { - $request = new Request( - [ - 'server' => [ - 'SERVER_PROTOCOL' => $input, - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getHttpProtocol()); - } - - public function testGetServerProtocolWithOverrideValid(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueString') - ->willReturnMap([ - ['overwriteprotocol', '', 'HTTPS'], // should be automatically lowercased - ['overwritecondaddr', '', ''], - ]); - - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('https', $request->getServerProtocol()); - } - - public function testGetServerProtocolWithOverrideInValid(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueString') - ->willReturnMap([ - ['overwriteprotocol', '', 'bogusProtocol'], // should trigger fallback to http - ['overwritecondaddr', '', ''], - ]); - - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('http', $request->getServerProtocol()); - } - - public function testGetServerProtocolWithProtoValid(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - - return $default; - }); - - $requestHttps = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_PROTO' => 'HtTpS', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - $requestHttp = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_PROTO' => 'HTTp', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('https', $requestHttps->getServerProtocol()); - $this->assertSame('http', $requestHttp->getServerProtocol()); - } - - public function testGetServerProtocolWithHttpsServerValueOn(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTPS' => 'on' - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - $this->assertSame('https', $request->getServerProtocol()); - } - - public function testGetServerProtocolWithHttpsServerValueOff(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTPS' => 'off' - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - $this->assertSame('http', $request->getServerProtocol()); - } - - public function testGetServerProtocolWithHttpsServerValueEmpty(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTPS' => '' - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - $this->assertSame('http', $request->getServerProtocol()); - } - - public function testGetServerProtocolDefault(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - $this->assertSame('http', $request->getServerProtocol()); - } - - public function testGetServerProtocolBehindLoadBalancers(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_PROTO' => 'https,http,http', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('https', $request->getServerProtocol()); - } - - /** - * @param string $testAgent - * @param array $userAgent - * @param bool $matches - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataUserAgent')] - public function testUserAgent($testAgent, $userAgent, $matches): void { - $request = new Request( - [ - 'server' => [ - 'HTTP_USER_AGENT' => $testAgent, - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($matches, $request->isUserAgent($userAgent)); - } - - /** - * @param string $testAgent - * @param array $userAgent - * @param bool $matches - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataUserAgent')] - public function testUndefinedUserAgent($testAgent, $userAgent, $matches): void { - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertFalse($request->isUserAgent($userAgent)); - } - - public static function dataUserAgent(): array { - return [ - [ - 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', - [ - Request::USER_AGENT_IE - ], - true, - ], - [ - 'Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0', - [ - Request::USER_AGENT_IE - ], - false, - ], - [ - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', - [ - Request::USER_AGENT_CHROME - ], - true, - ], - [ - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36', - [ - Request::USER_AGENT_CHROME - ], - true, - ], - [ - 'Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16S) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36', - [ - Request::USER_AGENT_ANDROID_MOBILE_CHROME - ], - true, - ], - [ - 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', - [ - Request::USER_AGENT_ANDROID_MOBILE_CHROME - ], - false, - ], - [ - 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)', - [ - Request::USER_AGENT_IE, - Request::USER_AGENT_ANDROID_MOBILE_CHROME, - ], - true, - ], - [ - 'Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16S) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36', - [ - Request::USER_AGENT_IE, - Request::USER_AGENT_ANDROID_MOBILE_CHROME, - ], - true, - ], - [ - 'Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20100101 Firefox/24.0', - [ - Request::USER_AGENT_FREEBOX - ], - false, - ], - [ - 'Mozilla/5.0', - [ - Request::USER_AGENT_FREEBOX - ], - true, - ], - [ - 'Fake Mozilla/5.0', - [ - Request::USER_AGENT_FREEBOX - ], - false, - ], - [ - 'Mozilla/5.0 (Android) ownCloud-android/2.0.0', - [ - Request::USER_AGENT_CLIENT_ANDROID - ], - true, - ], - [ - 'Mozilla/5.0 (Android) Nextcloud-android/2.0.0', - [ - Request::USER_AGENT_CLIENT_ANDROID - ], - true, - ], - [ - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.99 Safari/537.36 Vivaldi/2.9.1705.41', - [ - Request::USER_AGENT_CHROME - ], - true - ], - [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.38 Safari/537.36 Brave/75', - [ - Request::USER_AGENT_CHROME - ], - true - ], - [ - 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 OPR/50.0.2762.67', - [ - Request::USER_AGENT_CHROME - ], - true - ] - ]; - } - - public static function dataMatchClientVersion(): array { - return [ - [ - 'Mozilla/5.0 (Android) Nextcloud-android/3.24.1', - Request::USER_AGENT_CLIENT_ANDROID, - '3.24.1', - ], - [ - 'Mozilla/5.0 (iOS) Nextcloud-iOS/4.8.2', - Request::USER_AGENT_CLIENT_IOS, - '4.8.2', - ], - [ - 'Mozilla/5.0 (Windows) mirall/3.8.1', - Request::USER_AGENT_CLIENT_DESKTOP, - '3.8.1', - ], - [ - 'Mozilla/5.0 (Android) Nextcloud-Talk v17.10.0', - Request::USER_AGENT_TALK_ANDROID, - '17.10.0', - ], - [ - 'Mozilla/5.0 (iOS) Nextcloud-Talk v17.0.1', - Request::USER_AGENT_TALK_IOS, - '17.0.1', - ], - [ - 'Mozilla/5.0 (Windows) Nextcloud-Talk v0.6.0', - Request::USER_AGENT_TALK_DESKTOP, - '0.6.0', - ], - [ - 'Mozilla/5.0 (Windows) Nextcloud-Outlook v1.0.0', - Request::USER_AGENT_OUTLOOK_ADDON, - '1.0.0', - ], - [ - 'Filelink for *cloud/1.0.0', - Request::USER_AGENT_THUNDERBIRD_ADDON, - '1.0.0', - ], - ]; - } - - /** - * @param string $testAgent - * @param string $userAgent - * @param string $version - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataMatchClientVersion')] - public function testMatchClientVersion(string $testAgent, string $userAgent, string $version): void { - preg_match($userAgent, $testAgent, $matches); - - $this->assertSame($version, $matches[1]); - } - - public function testInsecureServerHostServerNameHeader(): void { - $request = new Request( - [ - 'server' => [ - 'SERVER_NAME' => 'from.server.name:8080', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('from.server.name:8080', $request->getInsecureServerHost()); - } - - public function testInsecureServerHostHttpHostHeader(): void { - $request = new Request( - [ - 'server' => [ - 'SERVER_NAME' => 'from.server.name:8080', - 'HTTP_HOST' => 'from.host.header:8080', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('from.host.header:8080', $request->getInsecureServerHost()); - } - - public function testInsecureServerHostHttpFromForwardedHeaderSingle(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'SERVER_NAME' => 'from.server.name:8080', - 'HTTP_HOST' => 'from.host.header:8080', - 'HTTP_X_FORWARDED_HOST' => 'from.forwarded.host:8080', - 'REMOTE_ADDR' => '1.2.3.4', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('from.forwarded.host:8080', $request->getInsecureServerHost()); - } - - public function testInsecureServerHostHttpFromForwardedHeaderStacked(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'SERVER_NAME' => 'from.server.name:8080', - 'HTTP_HOST' => 'from.host.header:8080', - 'HTTP_X_FORWARDED_HOST' => 'from.forwarded.host2:8080,another.one:9000', - 'REMOTE_ADDR' => '1.2.3.4', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('from.forwarded.host2:8080', $request->getInsecureServerHost()); - } - - public function testGetServerHostWithOverwriteHost(): void { - $this->config - ->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - if ($key === 'overwritecondaddr') { - return ''; - } elseif ($key === 'overwritehost') { - return 'my.overwritten.host'; - } - - return $default; - }); - - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('my.overwritten.host', $request->getServerHost()); - } - - public function testGetServerHostWithTrustedDomain(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } elseif ($key === 'trusted_domains') { - return ['my.trusted.host']; - } - - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_HOST' => 'my.trusted.host', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('my.trusted.host', $request->getServerHost()); - } - - public function testGetServerHostWithUntrustedDomain(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } elseif ($key === 'trusted_domains') { - return ['my.trusted.host']; - } - - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('my.trusted.host', $request->getServerHost()); - } - - public function testGetServerHostWithNoTrustedDomain(): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('', $request->getServerHost()); - } - - public static function dataGetServerHostTrustedDomain(): array { - return [ - 'is array' => ['my.trusted.host', ['my.trusted.host']], - 'is array but undefined index 0' => ['my.trusted.host', [2 => 'my.trusted.host']], - 'is string' => ['my.trusted.host', 'my.trusted.host'], - 'is null' => ['', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetServerHostTrustedDomain')] - public function testGetServerHostTrustedDomain(string $expected, $trustedDomain): void { - $this->config - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) use ($trustedDomain) { - if ($key === 'trusted_proxies') { - return ['1.2.3.4']; - } - if ($key === 'trusted_domains') { - return $trustedDomain; - } - return $default; - }); - - $request = new Request( - [ - 'server' => [ - 'HTTP_X_FORWARDED_HOST' => 'my.untrusted.host', - 'REMOTE_ADDR' => '1.2.3.4', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getServerHost()); - } - - public function testGetOverwriteHostDefaultNull(): void { - $this->config - ->expects($this->once()) - ->method('getSystemValueString') - ->with('overwritehost') - ->willReturn(''); - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertNull(self::invokePrivate($request, 'getOverwriteHost')); - } - - public function testGetOverwriteHostWithOverwrite(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueString') - ->willReturnMap([ - ['overwritehost', '', 'www.owncloud.org'], - ['overwritecondaddr', '', ''], - ]); - - $request = new Request( - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('www.owncloud.org', self::invokePrivate($request, 'getOverwriteHost')); - } - - public function testGetPathInfoNotProcessible(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('The requested uri(/foo.php) cannot be processed by the script \'/var/www/index.php\')'); - - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/foo.php', - 'SCRIPT_NAME' => '/var/www/index.php', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $request->getPathInfo(); - } - - public function testGetRawPathInfoNotProcessible(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('The requested uri(/foo.php) cannot be processed by the script \'/var/www/index.php\')'); - - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/foo.php', - 'SCRIPT_NAME' => '/var/www/index.php', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $request->getRawPathInfo(); - } - - /** - * @param string $requestUri - * @param string $scriptName - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGenericPathInfo')] - public function testGetPathInfoWithoutSetEnvGeneric($requestUri, $scriptName, $expected): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => $requestUri, - 'SCRIPT_NAME' => $scriptName, - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getPathInfo()); - } - - /** - * @param string $requestUri - * @param string $scriptName - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGenericPathInfo')] - public function testGetRawPathInfoWithoutSetEnvGeneric($requestUri, $scriptName, $expected): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => $requestUri, - 'SCRIPT_NAME' => $scriptName, - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getRawPathInfo()); - } - - /** - * @param string $requestUri - * @param string $scriptName - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataRawPathInfo')] - public function testGetRawPathInfoWithoutSetEnv($requestUri, $scriptName, $expected): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => $requestUri, - 'SCRIPT_NAME' => $scriptName, - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getRawPathInfo()); - } - - /** - * @param string $requestUri - * @param string $scriptName - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPathInfo')] - public function testGetPathInfoWithoutSetEnv($requestUri, $scriptName, $expected): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => $requestUri, - 'SCRIPT_NAME' => $scriptName, - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame($expected, $request->getPathInfo()); - } - - public static function dataGenericPathInfo(): array { - return [ - ['/core/index.php?XDEBUG_SESSION_START=14600', '/core/index.php', ''], - ['/index.php/apps/files/', 'index.php', '/apps/files/'], - ['/index.php/apps/files/../&/&?someQueryParameter=QueryParam', 'index.php', '/apps/files/../&/&'], - ['/remote.php/漢字編碼方法 / 汉字编码方法', 'remote.php', '/漢字編碼方法 / 汉字编码方法'], - ['///removeTrailin//gSlashes///', 'remote.php', '/removeTrailin/gSlashes/'], - ['/', '/', ''], - ['', '', ''], - ]; - } - - public static function dataRawPathInfo(): array { - return [ - ['/foo%2Fbar/subfolder', '', 'foo%2Fbar/subfolder'], - ]; - } - - public static function dataPathInfo(): array { - return [ - ['/foo%2Fbar/subfolder', '', 'foo/bar/subfolder'], - ]; - } - - public function testGetRawPathInfoWithoutScriptName(): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/index.php/apps/files/', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('index.php/apps/files/', $request->getRawPathInfo()); - } - - public function testGetPathInfoWithoutScriptName(): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/index.php/apps/files/', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('index.php/apps/files/', $request->getPathInfo()); - } - - public function testGetRawPathInfoWithoutScriptNameRoot(): void { - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/', - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('', $request->getRawPathInfo()); - } - - public function testGetRawPathInfoWithoutScriptNameOrRequestUri(): void { - $request = new Request( - [ - 'server' => [] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('', $request->getRawPathInfo()); - } - - public function testGetRequestUriWithoutOverwrite(): void { - $this->config - ->expects($this->once()) - ->method('getSystemValueString') - ->with('overwritewebroot') - ->willReturn(''); - - $request = new Request( - [ - 'server' => [ - 'REQUEST_URI' => '/test.php' - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ); - - $this->assertSame('/test.php', $request->getRequestUri()); - } - - public static function dataGetRequestUriWithOverwrite(): array { - return [ - ['/scriptname.php/some/PathInfo', '/owncloud/', ''], - ['/scriptname.php/some/PathInfo', '/owncloud/', '123', '123.123.123.123'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetRequestUriWithOverwrite')] - public function testGetRequestUriWithOverwrite($expectedUri, $overwriteWebRoot, $overwriteCondAddr, $remoteAddr = ''): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['overwritewebroot', '', $overwriteWebRoot], - ['overwritecondaddr', '', $overwriteCondAddr], - ]); - - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'REQUEST_URI' => '/test.php/some/PathInfo', - 'SCRIPT_NAME' => '/test.php', - 'REMOTE_ADDR' => $remoteAddr - ] - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $request - ->expects($this->once()) - ->method('getScriptName') - ->willReturn('/scriptname.php'); - - $this->assertSame($expectedUri, $request->getRequestUri()); - } - - public function testPassesCSRFCheckWithGet(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'get' => [ - 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - 'nc_sameSiteCookiestrict' => 'true', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->with($token) - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithPost(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'post' => [ - 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - 'nc_sameSiteCookiestrict' => 'true', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->with($token) - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithHeader(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - 'nc_sameSiteCookiestrict' => 'true', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds'); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->with($token) - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithGetAndWithoutCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'get' => [ - 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithPostAndWithoutCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'post' => [ - 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithHeaderAndWithoutCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $this->csrfTokenManager - ->expects($this->once()) - ->method('isTokenValid') - ->willReturn(true); - - $this->assertTrue($request->passesCSRFCheck()); - } - - public function testFailsCSRFCheckWithHeaderAndNotAllChecksPassing(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $this->csrfTokenManager - ->expects($this->never()) - ->method('isTokenValid'); - - $this->assertFalse($request->passesCSRFCheck()); - } - - public function testPassesStrictCookieCheckWithAllCookiesAndStrict(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName', 'getCookieParams']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - '__Host-nc_sameSiteCookiestrict' => 'true', - '__Host-nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $request - ->expects($this->any()) - ->method('getCookieParams') - ->willReturn([ - 'secure' => true, - 'path' => '/', - ]); - - $this->assertTrue($request->passesStrictCookieCheck()); - } - - public function testFailsStrictCookieCheckWithAllCookiesAndMissingStrict(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName', 'getCookieParams']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'true', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $request - ->expects($this->any()) - ->method('getCookieParams') - ->willReturn([ - 'secure' => true, - 'path' => '/', - ]); - - $this->assertFalse($request->passesStrictCookieCheck()); - } - - public function testGetCookieParams(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $actual = $request->getCookieParams(); - $this->assertSame(session_get_cookie_params(), $actual); - } - - public function testPassesStrictCookieCheckWithAllCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'true', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertTrue($request->passesStrictCookieCheck()); - } - - public function testPassesStrictCookieCheckWithRandomCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - 'RandomCookie' => 'asdf', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertTrue($request->passesStrictCookieCheck()); - } - - public function testFailsStrictCookieCheckWithSessionCookie(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesStrictCookieCheck()); - } - - public function testFailsStrictCookieCheckWithRememberMeCookie(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - 'nc_token' => 'asdf', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesStrictCookieCheck()); - } - - public function testFailsCSRFCheckWithPostAndWithCookies(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'post' => [ - 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'foo' => 'bar', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - $this->csrfTokenManager - ->expects($this->never()) - ->method('isTokenValid'); - - $this->assertFalse($request->passesCSRFCheck()); - } - - public function testFailStrictCookieCheckWithOnlyLaxCookie(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesStrictCookieCheck()); - } - - public function testFailStrictCookieCheckWithOnlyStrictCookie(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesStrictCookieCheck()); - } - - public function testPassesLaxCookieCheck(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookielax' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertTrue($request->passesLaxCookieCheck()); - } - - public function testFailsLaxCookieCheckWithOnlyStrictCookie(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesLaxCookieCheck()); - } - - public function testSkipCookieCheckForOCSRequests(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds', - 'HTTP_OCS_APIREQUEST' => 'true', - ], - 'cookies' => [ - session_name() => 'asdf', - 'nc_sameSiteCookiestrict' => 'false', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertTrue($request->passesStrictCookieCheck()); - } - - public static function dataInvalidToken(): array { - return [ - ['InvalidSentToken'], - ['InvalidSentToken:InvalidSecret'], - [''], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataInvalidToken')] - public function testPassesCSRFCheckWithInvalidToken(string $invalidToken): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_REQUESTTOKEN' => $invalidToken, - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $token = new CsrfToken($invalidToken); - $this->csrfTokenManager - ->expects($this->any()) - ->method('isTokenValid') - ->with($token) - ->willReturn(false); - - $this->assertFalse($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithoutTokenFail(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertFalse($request->passesCSRFCheck()); - } - - public function testPassesCSRFCheckWithOCSAPIRequestHeader(): void { - /** @var Request $request */ - $request = $this->getMockBuilder(Request::class) - ->onlyMethods(['getScriptName']) - ->setConstructorArgs([ - [ - 'server' => [ - 'HTTP_OCS_APIREQUEST' => 'true', - ], - ], - $this->requestId, - $this->config, - $this->csrfTokenManager, - $this->stream - ]) - ->getMock(); - - $this->assertTrue($request->passesCSRFCheck()); - } -} --- a/tests/lib/AppFramework/Http/ResponseTest.php +++ /dev/null @@ -1,262 +0,0 @@ -childResponse = new Response(); - } - - public function testAddHeader(): void { - $this->childResponse->addHeader(' hello ', 'world'); - $headers = $this->childResponse->getHeaders(); - $this->assertEquals('world', $headers['hello']); - } - - public function testSetHeaders(): void { - $expected = [ - 'Last-Modified' => 1, - 'ETag' => 3, - 'Something-Else' => 'hi', - 'X-Robots-Tag' => 'noindex, nofollow', - 'Cache-Control' => 'no-cache, no-store, must-revalidate', - ]; - - $this->childResponse->setHeaders($expected); - $expected['Content-Security-Policy'] = "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"; - $expected['Feature-Policy'] = "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'"; - - $headers = $this->childResponse->getHeaders(); - unset($headers['X-Request-Id']); - - $this->assertEquals($expected, $headers); - } - - public function testOverwriteCsp(): void { - $expected = [ - 'Content-Security-Policy' => "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-inline';style-src 'self' 'unsafe-inline';img-src 'self';font-src 'self' data:;connect-src 'self';media-src 'self'", - ]; - $policy = new ContentSecurityPolicy(); - - $this->childResponse->setContentSecurityPolicy($policy); - $headers = $this->childResponse->getHeaders(); - - $this->assertEquals(array_merge($expected, $headers), $headers); - } - - public function testGetCsp(): void { - $policy = new ContentSecurityPolicy(); - - $this->childResponse->setContentSecurityPolicy($policy); - $this->assertEquals($policy, $this->childResponse->getContentSecurityPolicy()); - } - - public function testGetCspEmpty(): void { - $this->assertEquals(new EmptyContentSecurityPolicy(), $this->childResponse->getContentSecurityPolicy()); - } - - public function testAddHeaderValueNullDeletesIt(): void { - $this->childResponse->addHeader('hello', 'world'); - $this->childResponse->addHeader('hello', null); - $this->assertEquals(5, count($this->childResponse->getHeaders())); - } - - public function testCacheHeadersAreDisabledByDefault(): void { - $headers = $this->childResponse->getHeaders(); - $this->assertEquals('no-cache, no-store, must-revalidate', $headers['Cache-Control']); - } - - public function testAddCookie(): void { - $this->childResponse->addCookie('foo', 'bar'); - $this->childResponse->addCookie('bar', 'foo', new \DateTime('1970-01-01')); - - $expectedResponse = [ - 'foo' => [ - 'value' => 'bar', - 'expireDate' => null, - 'sameSite' => 'Lax', - ], - 'bar' => [ - 'value' => 'foo', - 'expireDate' => new \DateTime('1970-01-01'), - 'sameSite' => 'Lax', - ] - ]; - $this->assertEquals($expectedResponse, $this->childResponse->getCookies()); - } - - public function testSetCookies(): void { - $expected = [ - 'foo' => [ - 'value' => 'bar', - 'expireDate' => null, - ], - 'bar' => [ - 'value' => 'foo', - 'expireDate' => new \DateTime('1970-01-01') - ] - ]; - - $this->childResponse->setCookies($expected); - $cookies = $this->childResponse->getCookies(); - - $this->assertEquals($expected, $cookies); - } - - public function testInvalidateCookie(): void { - $this->childResponse->addCookie('foo', 'bar'); - $this->childResponse->invalidateCookie('foo'); - $expected = [ - 'foo' => [ - 'value' => 'expired', - 'expireDate' => new \DateTime('1971-01-01'), - 'sameSite' => 'Lax', - ] - ]; - - $cookies = $this->childResponse->getCookies(); - - $this->assertEquals($expected, $cookies); - } - - public function testInvalidateCookies(): void { - $this->childResponse->addCookie('foo', 'bar'); - $this->childResponse->addCookie('bar', 'foo'); - $expected = [ - 'foo' => [ - 'value' => 'bar', - 'expireDate' => null, - 'sameSite' => 'Lax', - ], - 'bar' => [ - 'value' => 'foo', - 'expireDate' => null, - 'sameSite' => 'Lax', - ] - ]; - $cookies = $this->childResponse->getCookies(); - $this->assertEquals($expected, $cookies); - - $this->childResponse->invalidateCookies(['foo', 'bar']); - $expected = [ - 'foo' => [ - 'value' => 'expired', - 'expireDate' => new \DateTime('1971-01-01'), - 'sameSite' => 'Lax', - ], - 'bar' => [ - 'value' => 'expired', - 'expireDate' => new \DateTime('1971-01-01'), - 'sameSite' => 'Lax', - ] - ]; - - $cookies = $this->childResponse->getCookies(); - $this->assertEquals($expected, $cookies); - } - - public function testRenderReturnNullByDefault(): void { - $this->assertEquals(null, $this->childResponse->render()); - } - - public function testGetStatus(): void { - $default = $this->childResponse->getStatus(); - - $this->childResponse->setStatus(Http::STATUS_NOT_FOUND); - - $this->assertEquals(Http::STATUS_OK, $default); - $this->assertEquals(Http::STATUS_NOT_FOUND, $this->childResponse->getStatus()); - } - - public function testGetEtag(): void { - $this->childResponse->setEtag('hi'); - $this->assertSame('hi', $this->childResponse->getEtag()); - } - - public function testGetLastModified(): void { - $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); - $lastModified->setTimestamp(1); - $this->childResponse->setLastModified($lastModified); - $this->assertEquals($lastModified, $this->childResponse->getLastModified()); - } - - public function testCacheSecondsZero(): void { - $this->childResponse->cacheFor(0); - - $headers = $this->childResponse->getHeaders(); - $this->assertEquals('no-cache, no-store, must-revalidate', $headers['Cache-Control']); - $this->assertFalse(isset($headers['Expires'])); - } - - public function testCacheSeconds(): void { - $time = $this->createMock(ITimeFactory::class); - $time->method('getTime') - ->willReturn(1234567); - - $this->overwriteService(ITimeFactory::class, $time); - - $this->childResponse->cacheFor(33); - - $headers = $this->childResponse->getHeaders(); - $this->assertEquals('private, max-age=33, must-revalidate', $headers['Cache-Control']); - $this->assertEquals('Thu, 15 Jan 1970 06:56:40 GMT', $headers['Expires']); - } - - public function testEtagLastModifiedHeaders(): void { - $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); - $lastModified->setTimestamp(1); - $this->childResponse->setLastModified($lastModified); - $headers = $this->childResponse->getHeaders(); - $this->assertEquals('Thu, 01 Jan 1970 00:00:01 GMT', $headers['Last-Modified']); - } - - public function testChainability(): void { - $lastModified = new \DateTime('now', new \DateTimeZone('GMT')); - $lastModified->setTimestamp(1); - - $this->childResponse->setEtag('hi') - ->setStatus(Http::STATUS_NOT_FOUND) - ->setLastModified($lastModified) - ->cacheFor(33) - ->addHeader('hello', 'world'); - - $headers = $this->childResponse->getHeaders(); - - $this->assertEquals('world', $headers['hello']); - $this->assertEquals(Http::STATUS_NOT_FOUND, $this->childResponse->getStatus()); - $this->assertEquals('hi', $this->childResponse->getEtag()); - $this->assertEquals('Thu, 01 Jan 1970 00:00:01 GMT', $headers['Last-Modified']); - $this->assertEquals('private, max-age=33, must-revalidate', - $headers['Cache-Control']); - } - - public function testThrottle(): void { - $this->assertFalse($this->childResponse->isThrottled()); - $this->childResponse->throttle(); - $this->assertTrue($this->childResponse->isThrottled()); - } - - public function testGetThrottleMetadata(): void { - $this->childResponse->throttle(['foo' => 'bar']); - $this->assertSame(['foo' => 'bar'], $this->childResponse->getThrottleMetadata()); - } -} --- a/tests/lib/AppFramework/Http/StreamResponseTest.php +++ /dev/null @@ -1,83 +0,0 @@ -output = $this->getMockBuilder('OCP\\AppFramework\\Http\\IOutput') - ->disableOriginalConstructor() - ->getMock(); - } - - public function testOutputNotModified(): void { - $path = __FILE__; - $this->output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_NOT_MODIFIED); - $this->output->expects($this->never()) - ->method('setReadfile'); - $response = new StreamResponse($path); - - $response->callback($this->output); - } - - public function testOutputOk(): void { - $path = __FILE__; - $this->output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $this->output->expects($this->once()) - ->method('setReadfile') - ->with($this->equalTo($path)) - ->willReturn(true); - $response = new StreamResponse($path); - - $response->callback($this->output); - } - - public function testOutputNotFound(): void { - $path = __FILE__ . 'test'; - $this->output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $this->output->expects($this->never()) - ->method('setReadfile'); - $this->output->expects($this->once()) - ->method('setHttpResponseCode') - ->with($this->equalTo(Http::STATUS_NOT_FOUND)); - $response = new StreamResponse($path); - - $response->callback($this->output); - } - - public function testOutputReadFileError(): void { - $path = __FILE__; - $this->output->expects($this->once()) - ->method('getHttpResponseCode') - ->willReturn(Http::STATUS_OK); - $this->output->expects($this->once()) - ->method('setReadfile') - ->willReturn(false); - $this->output->expects($this->once()) - ->method('setHttpResponseCode') - ->with($this->equalTo(Http::STATUS_BAD_REQUEST)); - $response = new StreamResponse($path); - - $response->callback($this->output); - } -} --- a/tests/lib/AppFramework/Http/StreamTraversableResponseTest.php +++ /dev/null @@ -1,52 +0,0 @@ - 'text/plain']); - - $headers = $response->getHeaders(); - $this->assertEquals('text/plain', $headers['Content-Type']); - $this->assertEquals(200, $response->getStatus()); - } - - public function testCallback() { - $count = 0; - $generator = function () use (&$count) { - $count++; - yield 'chunk1'; - $count++; - yield 'chunk2'; - }; - $response = new StreamTraversableResponse($generator(), 200, ['Content-Type' => 'text/plain']); - $output = $this->createMock(IOutput::class); - $output->expects($this->exactly(2)) - ->method('setOutput') - ->with($this->callback(function ($chunk) { - return in_array($chunk, ['chunk1', 'chunk2'], true); - })); - - $response->callback($output); - $this->assertEquals($count, 2); - } - -} --- a/tests/lib/AppFramework/Http/TemplateResponseTest.php +++ /dev/null @@ -1,66 +0,0 @@ -tpl = new TemplateResponse('app', 'home'); - } - - public function testSetParamsConstructor(): void { - $params = ['hi' => 'yo']; - $this->tpl = new TemplateResponse('app', 'home', $params); - - $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); - } - - public function testSetRenderAsConstructor(): void { - $renderAs = 'myrender'; - $this->tpl = new TemplateResponse('app', 'home', [], $renderAs); - - $this->assertEquals($renderAs, $this->tpl->getRenderAs()); - } - - public function testSetParams(): void { - $params = ['hi' => 'yo']; - $this->tpl->setParams($params); - - $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); - } - - public function testGetTemplateName(): void { - $this->assertEquals('home', $this->tpl->getTemplateName()); - } - - public function testGetRenderAs(): void { - $render = 'myrender'; - $this->tpl->renderAs($render); - $this->assertEquals($render, $this->tpl->getRenderAs()); - } - - public function testChainability(): void { - $params = ['hi' => 'yo']; - $this->tpl->setParams($params) - ->setStatus(Http::STATUS_NOT_FOUND); - - $this->assertEquals(Http::STATUS_NOT_FOUND, $this->tpl->getStatus()); - $this->assertEquals(['hi' => 'yo'], $this->tpl->getParams()); - } -} --- a/tests/lib/AppFramework/Middleware/AdditionalScriptsMiddlewareTest.php +++ /dev/null @@ -1,115 +0,0 @@ -userSession = $this->createMock(IUserSession::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->middleWare = new AdditionalScriptsMiddleware( - $this->userSession, - $this->dispatcher - ); - - $this->controller = $this->createMock(Controller::class); - } - - public function testNoTemplateResponse(): void { - $this->userSession->expects($this->never()) - ->method($this->anything()); - $this->dispatcher->expects($this->never()) - ->method($this->anything()); - - $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(Response::class)); - } - - public function testPublicShareController(): void { - $this->userSession->expects($this->never()) - ->method($this->anything()); - $this->dispatcher->expects($this->never()) - ->method($this->anything()); - - $this->middleWare->afterController($this->createMock(PublicShareController::class), 'myMethod', $this->createMock(Response::class)); - } - - public function testStandaloneTemplateResponse(): void { - $this->userSession->expects($this->never()) - ->method($this->anything()); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->willReturnCallback(function ($event): void { - if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === false) { - return; - } - - $this->fail('Wrong event dispatched'); - }); - - $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(StandaloneTemplateResponse::class)); - } - - public function testTemplateResponseNotLoggedIn(): void { - $this->userSession->method('isLoggedIn') - ->willReturn(false); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->willReturnCallback(function ($event): void { - if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === false) { - return; - } - - $this->fail('Wrong event dispatched'); - }); - - $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(TemplateResponse::class)); - } - - public function testTemplateResponseLoggedIn(): void { - $events = []; - - $this->userSession->method('isLoggedIn') - ->willReturn(true); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->willReturnCallback(function ($event): void { - if ($event instanceof BeforeTemplateRenderedEvent && $event->isLoggedIn() === true) { - return; - } - - $this->fail('Wrong event dispatched'); - }); - - $this->middleWare->afterController($this->controller, 'myMethod', $this->createMock(TemplateResponse::class)); - } -} --- a/tests/lib/AppFramework/Middleware/CompressionMiddlewareTest.php +++ /dev/null @@ -1,146 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->middleWare = new CompressionMiddleware( - $this->request - ); - - $this->controller = $this->createMock(Controller::class); - } - - public function testGzipOCSV1(): void { - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip'); - - $response = $this->createMock(V1Response::class); - $response->expects($this->once()) - ->method('addHeader') - ->with('Content-Encoding', 'gzip'); - - $response->method('getStatus') - ->willReturn(Http::STATUS_OK); - - $this->middleWare->beforeController($this->controller, 'myMethod'); - $this->middleWare->afterController($this->controller, 'myMethod', $response); - - $output = 'myoutput'; - $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); - - $this->assertSame($output, gzdecode($result)); - } - - public function testGzipOCSV2(): void { - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip'); - - $response = $this->createMock(V2Response::class); - $response->expects($this->once()) - ->method('addHeader') - ->with('Content-Encoding', 'gzip'); - - $response->method('getStatus') - ->willReturn(Http::STATUS_OK); - - $this->middleWare->beforeController($this->controller, 'myMethod'); - $this->middleWare->afterController($this->controller, 'myMethod', $response); - - $output = 'myoutput'; - $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); - - $this->assertSame($output, gzdecode($result)); - } - - public function testGzipJSONResponse(): void { - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip'); - - $response = $this->createMock(JSONResponse::class); - $response->expects($this->once()) - ->method('addHeader') - ->with('Content-Encoding', 'gzip'); - - $response->method('getStatus') - ->willReturn(Http::STATUS_OK); - - $this->middleWare->beforeController($this->controller, 'myMethod'); - $this->middleWare->afterController($this->controller, 'myMethod', $response); - - $output = 'myoutput'; - $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); - - $this->assertSame($output, gzdecode($result)); - } - - public function testNoGzipDataResponse(): void { - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip'); - - $response = $this->createMock(DataResponse::class); - $response->expects($this->never()) - ->method('addHeader'); - - $response->method('getStatus') - ->willReturn(Http::STATUS_OK); - $this->middleWare->beforeController($this->controller, 'myMethod'); - $this->middleWare->afterController($this->controller, 'myMethod', $response); - - $output = 'myoutput'; - $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); - - $this->assertSame($output, $result); - } - - public function testNoGzipNo200(): void { - $this->request->method('getHeader') - ->with('Accept-Encoding') - ->willReturn('gzip'); - - $response = $this->createMock(JSONResponse::class); - $response->expects($this->never()) - ->method('addHeader'); - - $response->method('getStatus') - ->willReturn(Http::STATUS_NOT_FOUND); - - $this->middleWare->beforeController($this->controller, 'myMethod'); - $this->middleWare->afterController($this->controller, 'myMethod', $response); - - $output = 'myoutput'; - $result = $this->middleWare->beforeOutput($this->controller, 'myMethod', $output); - - $this->assertSame($output, $result); - } -} --- a/tests/lib/AppFramework/Middleware/MiddlewareDispatcherTest.php +++ /dev/null @@ -1,272 +0,0 @@ -beforeControllerOrder = self::$beforeControllerCalled; - $this->controller = $controller; - $this->methodName = $methodName; - if ($this->beforeControllerThrowsEx) { - throw new \Exception(); - } - } - - #[\Override] - public function afterException(Controller $controller, string $methodName, \Exception $exception): void { - self::$afterExceptionCalled++; - $this->afterExceptionOrder = self::$afterExceptionCalled; - $this->controller = $controller; - $this->methodName = $methodName; - $this->exception = $exception; - parent::afterException($controller, $methodName, $exception); - } - - #[\Override] - public function afterController(Controller $controller, string $methodName, Response $response): Response { - self::$afterControllerCalled++; - $this->afterControllerOrder = self::$afterControllerCalled; - $this->controller = $controller; - $this->methodName = $methodName; - $this->response = $response; - return parent::afterController($controller, $methodName, $response); - } - - #[\Override] - public function beforeOutput(Controller $controller, string $methodName, string $output): string { - self::$beforeOutputCalled++; - $this->beforeOutputOrder = self::$beforeOutputCalled; - $this->controller = $controller; - $this->methodName = $methodName; - $this->output = $output; - return parent::beforeOutput($controller, $methodName, $output); - } -} - -class TestController extends Controller { - public function controllerMethod(): void { - } -} - -class MiddlewareDispatcherTest extends \Test\TestCase { - public $exception; - public $response; - private $out; - private $method; - private $controller; - - /** - * @var MiddlewareDispatcher - */ - private $dispatcher; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->dispatcher = new MiddlewareDispatcher(); - $this->controller = $this->getControllerMock(); - $this->method = 'method'; - $this->response = new Response(); - $this->out = 'hi'; - $this->exception = new \Exception(); - } - - private function getControllerMock() { - return $this->getMockBuilder(TestController::class) - ->onlyMethods(['controllerMethod']) - ->setConstructorArgs(['app', - new Request( - ['controllerMethod' => 'GET'], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ) - ])->getMock(); - } - - private function getMiddleware($beforeControllerThrowsEx = false) { - $m1 = new TestMiddleware($beforeControllerThrowsEx); - $this->dispatcher->registerMiddleware($m1); - return $m1; - } - - public function testAfterExceptionShouldReturnResponseOfMiddleware(): void { - $response = new Response(); - $m1 = $this->getMockBuilder(Middleware::class) - ->onlyMethods(['afterException', 'beforeController']) - ->getMock(); - $m1->expects($this->never()) - ->method('afterException'); - - $m2 = $this->getMockBuilder(Middleware::class) - ->onlyMethods(['afterException', 'beforeController']) - ->getMock(); - $m2->expects($this->once()) - ->method('afterException') - ->willReturn($response); - - $this->dispatcher->registerMiddleware($m1); - $this->dispatcher->registerMiddleware($m2); - - $this->dispatcher->beforeController($this->controller, $this->method); - $this->assertEquals($response, $this->dispatcher->afterException($this->controller, $this->method, $this->exception)); - } - - public function testAfterExceptionShouldThrowAgainWhenNotHandled(): void { - $m1 = new TestMiddleware(false); - $m2 = new TestMiddleware(true); - - $this->dispatcher->registerMiddleware($m1); - $this->dispatcher->registerMiddleware($m2); - - $this->expectException(\Exception::class); - $this->dispatcher->beforeController($this->controller, $this->method); - $this->dispatcher->afterException($this->controller, $this->method, $this->exception); - } - - public function testBeforeControllerCorrectArguments(): void { - $m1 = $this->getMiddleware(); - $this->dispatcher->beforeController($this->controller, $this->method); - - $this->assertEquals($this->controller, $m1->controller); - $this->assertEquals($this->method, $m1->methodName); - } - - public function testAfterControllerCorrectArguments(): void { - $m1 = $this->getMiddleware(); - - $this->dispatcher->afterController($this->controller, $this->method, $this->response); - - $this->assertEquals($this->controller, $m1->controller); - $this->assertEquals($this->method, $m1->methodName); - $this->assertEquals($this->response, $m1->response); - } - - public function testAfterExceptionCorrectArguments(): void { - $m1 = $this->getMiddleware(); - - $this->expectException(\Exception::class); - - $this->dispatcher->beforeController($this->controller, $this->method); - $this->dispatcher->afterException($this->controller, $this->method, $this->exception); - - $this->assertEquals($this->controller, $m1->controller); - $this->assertEquals($this->method, $m1->methodName); - $this->assertEquals($this->exception, $m1->exception); - } - - public function testBeforeOutputCorrectArguments(): void { - $m1 = $this->getMiddleware(); - - $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); - - $this->assertEquals($this->controller, $m1->controller); - $this->assertEquals($this->method, $m1->methodName); - $this->assertEquals($this->out, $m1->output); - } - - public function testBeforeControllerOrder(): void { - $m1 = $this->getMiddleware(); - $m2 = $this->getMiddleware(); - - $this->dispatcher->beforeController($this->controller, $this->method); - - $this->assertEquals(1, $m1->beforeControllerOrder); - $this->assertEquals(2, $m2->beforeControllerOrder); - } - - public function testAfterControllerOrder(): void { - $m1 = $this->getMiddleware(); - $m2 = $this->getMiddleware(); - - $this->dispatcher->afterController($this->controller, $this->method, $this->response); - - $this->assertEquals(2, $m1->afterControllerOrder); - $this->assertEquals(1, $m2->afterControllerOrder); - } - - public function testAfterExceptionOrder(): void { - $m1 = $this->getMiddleware(); - $m2 = $this->getMiddleware(); - - $this->expectException(\Exception::class); - $this->dispatcher->beforeController($this->controller, $this->method); - $this->dispatcher->afterException($this->controller, $this->method, $this->exception); - - $this->assertEquals(1, $m1->afterExceptionOrder); - $this->assertEquals(1, $m2->afterExceptionOrder); - } - - public function testBeforeOutputOrder(): void { - $m1 = $this->getMiddleware(); - $m2 = $this->getMiddleware(); - - $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); - - $this->assertEquals(2, $m1->beforeOutputOrder); - $this->assertEquals(1, $m2->beforeOutputOrder); - } - - public function testExceptionShouldRunAfterExceptionOfOnlyPreviouslyExecutedMiddlewares(): void { - $m1 = $this->getMiddleware(); - $m2 = $this->getMiddleware(true); - $m3 = $this->createMock(Middleware::class); - $m3->expects($this->never()) - ->method('afterException'); - $m3->expects($this->never()) - ->method('beforeController'); - $m3->expects($this->never()) - ->method('afterController'); - $m3->method('beforeOutput') - ->willReturnArgument(2); - - $this->dispatcher->registerMiddleware($m3); - - $this->dispatcher->beforeOutput($this->controller, $this->method, $this->out); - - $this->assertEquals(2, $m1->beforeOutputOrder); - $this->assertEquals(1, $m2->beforeOutputOrder); - } -} --- a/tests/lib/AppFramework/Middleware/MiddlewareTest.php +++ /dev/null @@ -1,74 +0,0 @@ -middleware = new ChildMiddleware(); - - $this->api = $this->createMock(DIContainer::class); - - $this->controller = $this->getMockBuilder(Controller::class) - ->setConstructorArgs([ - $this->api, - new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ) - ])->getMock(); - $this->exception = new \Exception(); - $this->response = $this->createMock(Response::class); - } - - public function testBeforeController(): void { - $this->middleware->beforeController($this->controller, ''); - $this->assertNull(null); - } - - public function testAfterExceptionRaiseAgainWhenUnhandled(): void { - $this->expectException(\Exception::class); - $this->middleware->afterException($this->controller, '', $this->exception); - } - - public function testAfterControllerReturnResponseWhenUnhandled(): void { - $response = $this->middleware->afterController($this->controller, '', $this->response); - - $this->assertEquals($this->response, $response); - } - - public function testBeforeOutputReturnOutputhenUnhandled(): void { - $output = $this->middleware->beforeOutput($this->controller, '', 'test'); - - $this->assertEquals('test', $output); - } -} --- a/tests/lib/AppFramework/Middleware/Mock/UseSessionController.php +++ /dev/null @@ -1,19 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->middleWare = new NotModifiedMiddleware( - $this->request - ); - - $this->controller = $this->createMock(Controller::class); - } - - public static function dataModified(): array { - $now = new \DateTime(); - - return [ - [null, '', null, '', false], - ['etag', 'etag', null, '', false], - ['etag', '"wrongetag"', null, '', false], - ['etag', '', null, '', false], - [null, '"etag"', null, '', false], - ['etag', '"etag"', null, '', true], - - [null, '', $now, $now->format(Constants::DATE_RFC7231), true], - [null, '', $now, $now->format(\DateTimeInterface::ATOM), false], - [null, '', null, $now->format(Constants::DATE_RFC7231), false], - [null, '', $now, '', false], - - ['etag', '"etag"', $now, $now->format(\DateTimeInterface::ATOM), true], - ['etag', '"etag"', $now, $now->format(Constants::DATE_RFC7231), true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataModified')] - public function testMiddleware(?string $etag, string $etagHeader, ?\DateTime $lastModified, string $lastModifiedHeader, bool $notModifiedSet): void { - $this->request->method('getHeader') - ->willReturnCallback(function (string $name) use ($etagHeader, $lastModifiedHeader) { - if ($name === 'IF_NONE_MATCH') { - return $etagHeader; - } - if ($name === 'IF_MODIFIED_SINCE') { - return $lastModifiedHeader; - } - return ''; - }); - - $response = new Response(); - if ($etag !== null) { - $response->setETag($etag); - } - if ($lastModified !== null) { - $response->setLastModified($lastModified); - } - $response->setStatus(Http::STATUS_OK); - - $result = $this->middleWare->afterController($this->controller, 'myfunction', $response); - - if ($notModifiedSet) { - $this->assertSame(Http::STATUS_NOT_MODIFIED, $result->getStatus()); - } else { - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - } - } -} --- a/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php +++ /dev/null @@ -1,184 +0,0 @@ -request = $this->getMockBuilder(IRequest::class) - ->getMock(); - } - - public static function dataAfterException(): array { - return [ - [OCSController::class, new \Exception(), true], - [OCSController::class, new OCSException(), false, '', Http::STATUS_INTERNAL_SERVER_ERROR], - [OCSController::class, new OCSException('foo'), false, 'foo', Http::STATUS_INTERNAL_SERVER_ERROR], - [OCSController::class, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), false, 'foo', Http::STATUS_IM_A_TEAPOT], - [OCSController::class, new OCSBadRequestException(), false, '', Http::STATUS_BAD_REQUEST], - [OCSController::class, new OCSBadRequestException('foo'), false, 'foo', Http::STATUS_BAD_REQUEST], - [OCSController::class, new OCSForbiddenException(), false, '', Http::STATUS_FORBIDDEN], - [OCSController::class, new OCSForbiddenException('foo'), false, 'foo', Http::STATUS_FORBIDDEN], - [OCSController::class, new OCSNotFoundException(), false, '', Http::STATUS_NOT_FOUND], - [OCSController::class, new OCSNotFoundException('foo'), false, 'foo', Http::STATUS_NOT_FOUND], - - [Controller::class, new \Exception(), true], - [Controller::class, new OCSException(), true], - [Controller::class, new OCSException('foo'), true], - [Controller::class, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), true], - [Controller::class, new OCSBadRequestException(), true], - [Controller::class, new OCSBadRequestException('foo'), true], - [Controller::class, new OCSForbiddenException(), true], - [Controller::class, new OCSForbiddenException('foo'), true], - [Controller::class, new OCSNotFoundException(), true], - [Controller::class, new OCSNotFoundException('foo'), true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] - public function testAfterExceptionOCSv1(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { - $controller = $this->createMock($controller); - $this->request - ->method('getScriptName') - ->willReturn('/ocs/v1.php'); - $OCSMiddleware = new OCSMiddleware($this->request); - $OCSMiddleware->beforeController($controller, 'method'); - - if ($forward) { - $this->expectException(get_class($exception)); - $this->expectExceptionMessage($exception->getMessage()); - } - - $result = $OCSMiddleware->afterException($controller, 'method', $exception); - - $this->assertInstanceOf(V1Response::class, $result); - - $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); - - if ($exception->getCode() === 0) { - $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); - } else { - $this->assertSame($code, $result->getOCSStatus()); - } - - $this->assertSame(Http::STATUS_OK, $result->getStatus()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] - public function testAfterExceptionOCSv2(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { - $controller = $this->createMock($controller); - $this->request - ->method('getScriptName') - ->willReturn('/ocs/v2.php'); - $OCSMiddleware = new OCSMiddleware($this->request); - $OCSMiddleware->beforeController($controller, 'method'); - - if ($forward) { - $this->expectException(get_class($exception)); - $this->expectExceptionMessage($exception->getMessage()); - } - - $result = $OCSMiddleware->afterException($controller, 'method', $exception); - - $this->assertInstanceOf(V2Response::class, $result); - - $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); - if ($exception->getCode() === 0) { - $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); - } else { - $this->assertSame($code, $result->getOCSStatus()); - } - $this->assertSame($code, $result->getStatus()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterException')] - public function testAfterExceptionOCSv2SubFolder(string $controller, \Exception $exception, bool $forward, string $message = '', int $code = 0): void { - $controller = $this->createMock($controller); - $this->request - ->method('getScriptName') - ->willReturn('/mysubfolder/ocs/v2.php'); - $OCSMiddleware = new OCSMiddleware($this->request); - $OCSMiddleware->beforeController($controller, 'method'); - - if ($forward) { - $this->expectException($exception::class); - $this->expectExceptionMessage($exception->getMessage()); - } - - $result = $OCSMiddleware->afterException($controller, 'method', $exception); - - $this->assertInstanceOf(V2Response::class, $result); - - $this->assertSame($message, $this->invokePrivate($result, 'statusMessage')); - if ($exception->getCode() === 0) { - $this->assertSame(OCSController::RESPOND_UNKNOWN_ERROR, $result->getOCSStatus()); - } else { - $this->assertSame($code, $result->getOCSStatus()); - } - $this->assertSame($code, $result->getStatus()); - } - - public static function dataAfterController(): array { - return [ - [OCSController::class, new Response(), false], - [OCSController::class, new JSONResponse(), false], - [OCSController::class, new JSONResponse(['message' => 'foo']), false], - [OCSController::class, new JSONResponse(['message' => 'foo'], Http::STATUS_UNAUTHORIZED), true, OCSController::RESPOND_UNAUTHORISED], - [OCSController::class, new JSONResponse(['message' => 'foo'], Http::STATUS_FORBIDDEN), true], - - [Controller::class, new Response(), false], - [Controller::class, new JSONResponse(), false], - [Controller::class, new JSONResponse(['message' => 'foo']), false], - [Controller::class, new JSONResponse(['message' => 'foo'], Http::STATUS_UNAUTHORIZED), false], - [Controller::class, new JSONResponse(['message' => 'foo'], Http::STATUS_FORBIDDEN), false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataAfterController')] - public function testAfterController(string $controller, Response $response, bool $converted, int $convertedOCSStatus = 0): void { - $controller = $this->createMock($controller); - $OCSMiddleware = new OCSMiddleware($this->request); - $newResponse = $OCSMiddleware->afterController($controller, 'foo', $response); - - if ($converted === false) { - $this->assertSame($response, $newResponse); - } else { - $this->assertInstanceOf(BaseResponse::class, $newResponse); - $this->assertSame($response->getData()['message'], $this->invokePrivate($newResponse, 'statusMessage')); - - if ($convertedOCSStatus) { - $this->assertSame($convertedOCSStatus, $newResponse->getOCSStatus()); - } else { - $this->assertSame($response->getStatus(), $newResponse->getOCSStatus()); - } - $this->assertSame($response->getStatus(), $newResponse->getStatus()); - } - } -} --- a/tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php +++ /dev/null @@ -1,264 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->session = $this->createMock(ISession::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->throttler = $this->createMock(IThrottler::class); - - $this->middleware = new PublicShareMiddleware( - $this->request, - $this->session, - $this->appConfig, - $this->throttler - ); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testBeforeControllerNoPublicShareController(): void { - $controller = $this->createMock(Controller::class); - - $this->middleware->beforeController($controller, 'method'); - } - - public static function dataShareApi(): array { - return [ - [false, false], - [false, true], - [true, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataShareApi')] - public function testBeforeControllerShareApiDisabled(bool $shareApi, bool $shareLinks): void { - $controller = $this->createMock(PublicShareController::class); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, $shareApi], - ['core', 'shareapi_allow_links', true, $shareLinks], - ]); - - $this->expectException(NotFoundException::class); - $this->middleware->beforeController($controller, 'mehod'); - } - - public function testBeforeControllerNoTokenParam(): void { - $controller = $this->createMock(PublicShareController::class); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->expectException(NotFoundException::class); - $this->middleware->beforeController($controller, 'mehod'); - } - - public function testBeforeControllerInvalidToken(): void { - $controller = $this->createMock(PublicShareController::class); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->request->method('getParam') - ->with('token', null) - ->willReturn('myToken'); - - $controller->method('isValidToken') - ->willReturn(false); - $controller->expects($this->once()) - ->method('shareNotFound'); - - $this->expectException(NotFoundException::class); - $this->middleware->beforeController($controller, 'mehod'); - } - - public function testBeforeControllerValidTokenNotAuthenticated(): void { - $controller = $this->getMockBuilder(PublicShareController::class) - ->setConstructorArgs(['app', $this->request, $this->session]) - ->getMock(); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->request->method('getParam') - ->with('token', null) - ->willReturn('myToken'); - - $controller->method('isValidToken') - ->willReturn(true); - - $controller->method('isPasswordProtected') - ->willReturn(true); - - $this->expectException(NotFoundException::class); - $this->middleware->beforeController($controller, 'mehod'); - } - - public function testBeforeControllerValidTokenAuthenticateMethod(): void { - $controller = $this->getMockBuilder(PublicShareController::class) - ->setConstructorArgs(['app', $this->request, $this->session]) - ->getMock(); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->request->method('getParam') - ->with('token', null) - ->willReturn('myToken'); - - $controller->expects($this->once()) - ->method('isValidToken') - ->willReturn(true); - - $this->middleware->beforeController($controller, 'authenticate'); - } - - public function testBeforeControllerValidTokenShowAuthenticateMethod(): void { - $controller = $this->getMockBuilder(PublicShareController::class) - ->setConstructorArgs(['app', $this->request, $this->session]) - ->getMock(); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->request->method('getParam') - ->with('token', null) - ->willReturn('myToken'); - - $controller->expects($this->once()) - ->method('isValidToken') - ->willReturn(true); - - $this->middleware->beforeController($controller, 'showAuthenticate'); - } - - public function testBeforeControllerAuthPublicShareController(): void { - $controller = $this->getMockBuilder(AuthPublicShareController::class) - ->setConstructorArgs(['app', $this->request, $this->session, $this->createMock(IURLGenerator::class)]) - ->getMock(); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_enabled', true, true], - ['core', 'shareapi_allow_links', true, true], - ]); - - $this->request->method('getParam') - ->with('token', null) - ->willReturn('myToken'); - - $controller->method('isValidToken') - ->willReturn(true); - - $controller->method('isPasswordProtected') - ->willReturn(true); - - $this->session->expects($this->once()) - ->method('set') - ->with('public_link_authenticate_redirect', '[]'); - - $this->expectException(NeedAuthenticationException::class); - $this->middleware->beforeController($controller, 'method'); - } - - public function testAfterExceptionNoPublicShareController(): void { - $controller = $this->createMock(Controller::class); - $exception = new \Exception(); - - try { - $this->middleware->afterException($controller, 'method', $exception); - } catch (\Exception $e) { - $this->assertEquals($exception, $e); - } - } - - public function testAfterExceptionPublicShareControllerNotFoundException(): void { - $controller = $this->createMock(PublicShareController::class); - $exception = new NotFoundException(); - - $result = $this->middleware->afterException($controller, 'method', $exception); - $this->assertInstanceOf(TemplateResponse::class, $result); - $this->assertEquals($result->getStatus(), Http::STATUS_NOT_FOUND); - } - - public function testAfterExceptionPublicShareController(): void { - $controller = $this->createMock(PublicShareController::class); - $exception = new \Exception(); - - try { - $this->middleware->afterException($controller, 'method', $exception); - } catch (\Exception $e) { - $this->assertEquals($exception, $e); - } - } - - public function testAfterExceptionAuthPublicShareController(): void { - $controller = $this->getMockBuilder(AuthPublicShareController::class) - ->setConstructorArgs([ - 'app', - $this->request, - $this->session, - $this->createMock(IURLGenerator::class), - ])->getMock(); - $controller->setToken('token'); - - $exception = new NeedAuthenticationException(); - - $this->request->method('getParam') - ->with('_route') - ->willReturn('my.route'); - - $result = $this->middleware->afterException($controller, 'method', $exception); - $this->assertInstanceOf(RedirectResponse::class, $result); - } -} --- a/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php +++ /dev/null @@ -1,309 +0,0 @@ -reflector = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $this->throttler = $this->createMock(IThrottler::class); - $this->request = $this->createMock(IRequest::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->bruteForceMiddleware = new BruteForceMiddleware( - $this->reflector, - $this->throttler, - $this->request, - $this->logger, - ); - } - - public function testBeforeControllerWithAnnotation(): void { - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - $this->throttler - ->expects($this->once()) - ->method('sleepDelayOrThrowOnMax') - ->with('127.0.0.1', 'login'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithAnnotation'); - $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithAnnotation'); - } - - public function testBeforeControllerWithSingleAttribute(): void { - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('::1'); - $this->throttler - ->expects($this->once()) - ->method('sleepDelayOrThrowOnMax') - ->with('::1', 'single'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'singleAttribute'); - $this->bruteForceMiddleware->beforeController($controller, 'singleAttribute'); - } - - public function testBeforeControllerWithMultipleAttributes(): void { - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('::1'); - - $calls = [ - ['::1', 'first'], - ['::1', 'second'], - ]; - $this->throttler - ->expects($this->exactly(2)) - ->method('sleepDelayOrThrowOnMax') - ->willReturnCallback(function () use (&$calls) { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - return 0; - }); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'multipleAttributes'); - $this->bruteForceMiddleware->beforeController($controller, 'multipleAttributes'); - } - - public function testBeforeControllerWithoutAnnotation(): void { - $this->request - ->expects($this->never()) - ->method('getRemoteAddress'); - $this->throttler - ->expects($this->never()) - ->method('sleepDelayOrThrowOnMax'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); - $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); - } - - public function testAfterControllerWithAnnotationAndThrottledRequest(): void { - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response - ->expects($this->once()) - ->method('isThrottled') - ->willReturn(true); - $response - ->expects($this->once()) - ->method('getThrottleMetadata') - ->willReturn([]); - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - $this->throttler - ->expects($this->once()) - ->method('sleepDelayOrThrowOnMax') - ->with('127.0.0.1', 'login'); - $this->throttler - ->expects($this->once()) - ->method('registerAttempt') - ->with('login', '127.0.0.1'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithAnnotation'); - $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); - } - - public function testAfterControllerWithAnnotationAndNotThrottledRequest(): void { - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response - ->expects($this->once()) - ->method('isThrottled') - ->willReturn(false); - $this->request - ->expects($this->never()) - ->method('getRemoteAddress'); - $this->throttler - ->expects($this->never()) - ->method('sleepDelayOrThrowOnMax'); - $this->throttler - ->expects($this->never()) - ->method('registerAttempt'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithAnnotation'); - $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response); - } - - public function testAfterControllerWithSingleAttribute(): void { - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response - ->expects($this->once()) - ->method('isThrottled') - ->willReturn(true); - $response - ->expects($this->once()) - ->method('getThrottleMetadata') - ->willReturn([]); - - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('::1'); - $this->throttler - ->expects($this->once()) - ->method('sleepDelayOrThrowOnMax') - ->with('::1', 'single'); - $this->throttler - ->expects($this->once()) - ->method('registerAttempt') - ->with('single', '::1'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'singleAttribute'); - $this->bruteForceMiddleware->afterController($controller, 'singleAttribute', $response); - } - - public function testAfterControllerWithMultipleAttributesGeneralMatch(): void { - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response - ->expects($this->once()) - ->method('isThrottled') - ->willReturn(true); - $response - ->expects($this->once()) - ->method('getThrottleMetadata') - ->willReturn([]); - - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('::1'); - - $sleepCalls = [ - ['::1', 'first'], - ['::1', 'second'], - ]; - $this->throttler - ->expects($this->exactly(2)) - ->method('sleepDelayOrThrowOnMax') - ->willReturnCallback(function () use (&$sleepCalls) { - $expected = array_shift($sleepCalls); - $this->assertEquals($expected, func_get_args()); - return 0; - }); - - $attemptCalls = [ - ['first', '::1', []], - ['second', '::1', []], - ]; - $this->throttler - ->expects($this->exactly(2)) - ->method('registerAttempt') - ->willReturnCallback(function () use (&$attemptCalls): void { - $expected = array_shift($attemptCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'multipleAttributes'); - $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); - } - - public function testAfterControllerWithMultipleAttributesSpecificMatch(): void { - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response - ->expects($this->once()) - ->method('isThrottled') - ->willReturn(true); - $response - ->expects($this->once()) - ->method('getThrottleMetadata') - ->willReturn(['action' => 'second']); - - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('::1'); - $this->throttler - ->expects($this->once()) - ->method('sleepDelayOrThrowOnMax') - ->with('::1', 'second'); - $this->throttler - ->expects($this->once()) - ->method('registerAttempt') - ->with('second', '::1'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'multipleAttributes'); - $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response); - } - - public function testAfterControllerWithoutAnnotation(): void { - $this->request - ->expects($this->never()) - ->method('getRemoteAddress'); - $this->throttler - ->expects($this->never()) - ->method('sleepDelayOrThrowOnMax'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); - } - - public function testAfterControllerWithThrottledResponseButUnhandled(): void { - $this->request - ->expects($this->never()) - ->method('getRemoteAddress'); - $this->throttler - ->expects($this->never()) - ->method('sleepDelayOrThrowOnMax'); - - $controller = new BruteForceMiddlewareController('test', $this->request); - $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); - /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */ - $response = $this->createMock(Response::class); - $response->method('isThrottled') - ->willReturn(true); - - $this->logger->expects($this->once()) - ->method('debug') - ->with('Response for Test\AppFramework\Middleware\Security\Mock\BruteForceMiddlewareController::testMethodWithoutAnnotation got bruteforce throttled but has no annotation nor attribute defined.'); - - $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response); - } -} --- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php +++ /dev/null @@ -1,347 +0,0 @@ -reflector = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $this->session = $this->createMock(Session::class); - $this->throttler = $this->createMock(IThrottler::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->controller = new CORSMiddlewareController( - 'test', - $this->createMock(IRequest::class) - ); - } - - public static function dataSetCORSAPIHeader(): array { - return [ - ['testSetCORSAPIHeader'], - ['testSetCORSAPIHeaderAttribute'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')] - public function testSetCORSAPIHeader(string $method): void { - $request = new Request( - [ - 'server' => [ - 'HTTP_ORIGIN' => 'test' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $response = $middleware->afterController($this->controller, $method, new Response()); - $headers = $response->getHeaders(); - $this->assertEquals('test', $headers['Access-Control-Allow-Origin']); - } - - public function testNoAnnotationNoCORSHEADER(): void { - $request = new Request( - [ - 'server' => [ - 'HTTP_ORIGIN' => 'test' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $response = $middleware->afterController($this->controller, __FUNCTION__, new Response()); - $headers = $response->getHeaders(); - $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); - } - - public static function dataNoOriginHeaderNoCORSHEADER(): array { - return [ - ['testNoOriginHeaderNoCORSHEADER'], - ['testNoOriginHeaderNoCORSHEADERAttribute'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')] - public function testNoOriginHeaderNoCORSHEADER(string $method): void { - $request = new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $response = $middleware->afterController($this->controller, $method, new Response()); - $headers = $response->getHeaders(); - $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers)); - } - - public static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array { - return [ - ['testCorsIgnoredIfWithCredentialsHeaderPresent'], - ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')] - public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void { - $this->expectException(SecurityException::class); - - $request = new Request( - [ - 'server' => [ - 'HTTP_ORIGIN' => 'test' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - - $response = new Response(); - $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE'); - $middleware->afterController($this->controller, $method, $response); - } - - public static function dataNoCORSOnAnonymousPublicPage(): array { - return [ - ['testNoCORSOnAnonymousPublicPage'], - ['testNoCORSOnAnonymousPublicPageAttribute'], - ['testNoCORSAttributeOnAnonymousPublicPage'], - ['testNoCORSAttributeOnAnonymousPublicPageAttribute'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')] - public function testNoCORSOnAnonymousPublicPage(string $method): void { - $request = new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger); - $this->session->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - $this->session->expects($this->never()) - ->method('logout'); - $this->session->expects($this->never()) - ->method('logClientIn') - ->with($this->equalTo('user'), $this->equalTo('pass')) - ->willReturn(true); - $this->reflector->reflect($this->controller, $method); - - $middleware->beforeController($this->controller, $method); - } - - public static function dataCORSShouldNeverAllowCookieAuth(): array { - return [ - ['testCORSShouldNeverAllowCookieAuth'], - ['testCORSShouldNeverAllowCookieAuthAttribute'], - ['testCORSAttributeShouldNeverAllowCookieAuth'], - ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')] - public function testCORSShouldNeverAllowCookieAuth(string $method): void { - $request = new Request( - [], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - $this->session->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->session->expects($this->once()) - ->method('logout'); - $this->session->expects($this->never()) - ->method('logClientIn') - ->with($this->equalTo('user'), $this->equalTo('pass')) - ->willReturn(true); - - $this->expectException(SecurityException::class); - $middleware->beforeController($this->controller, $method); - } - - public static function dataCORSShouldRelogin(): array { - return [ - ['testCORSShouldRelogin'], - ['testCORSAttributeShouldRelogin'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')] - public function testCORSShouldRelogin(string $method): void { - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->session->expects($this->once()) - ->method('logout'); - $this->session->expects($this->once()) - ->method('logClientIn') - ->with($this->equalTo('user'), $this->equalTo('pass')) - ->willReturn(true); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $middleware->beforeController($this->controller, $method); - } - - public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array { - return [ - ['testCORSShouldFailIfPasswordLoginIsForbidden'], - ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')] - public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void { - $this->expectException(SecurityException::class); - - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->session->expects($this->once()) - ->method('logout'); - $this->session->expects($this->once()) - ->method('logClientIn') - ->with($this->equalTo('user'), $this->equalTo('pass')) - ->willThrowException(new PasswordLoginForbiddenException); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $middleware->beforeController($this->controller, $method); - } - - public static function dataCORSShouldNotAllowCookieAuth(): array { - return [ - ['testCORSShouldNotAllowCookieAuth'], - ['testCORSAttributeShouldNotAllowCookieAuth'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')] - public function testCORSShouldNotAllowCookieAuth(string $method): void { - $this->expectException(SecurityException::class); - - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->session->expects($this->once()) - ->method('logout'); - $this->session->expects($this->once()) - ->method('logClientIn') - ->with($this->equalTo('user'), $this->equalTo('pass')) - ->willReturn(false); - $this->reflector->reflect($this->controller, $method); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - - $middleware->beforeController($this->controller, $method); - } - - public function testAfterExceptionWithSecurityExceptionNoStatus(): void { - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception')); - - $expected = new JSONResponse(['message' => 'A security exception'], 500); - $this->assertEquals($expected, $response); - } - - public function testAfterExceptionWithSecurityExceptionWithStatus(): void { - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501)); - - $expected = new JSONResponse(['message' => 'A security exception'], 501); - $this->assertEquals($expected, $response); - } - - public function testAfterExceptionWithRegularException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('A regular exception'); - - $request = new Request( - ['server' => [ - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'pass' - ]], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->reflector->reflect($this->controller, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); - $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception')); - } -} --- a/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php +++ /dev/null @@ -1,123 +0,0 @@ -controller = $this->createMock(Controller::class); - $this->contentSecurityPolicyManager = $this->createMock(ContentSecurityPolicyManager::class); - $this->cspNonceManager = $this->createMock(ContentSecurityPolicyNonceManager::class); - $this->middleware = new CSPMiddleware( - $this->contentSecurityPolicyManager, - $this->cspNonceManager, - ); - } - - public function testAfterController(): void { - $this->cspNonceManager - ->expects($this->once()) - ->method('browserSupportsCspV3') - ->willReturn(false); - $response = $this->createMock(Response::class); - $defaultPolicy = new ContentSecurityPolicy(); - $defaultPolicy->addAllowedImageDomain('defaultpolicy'); - $currentPolicy = new ContentSecurityPolicy(); - $currentPolicy->addAllowedConnectDomain('currentPolicy'); - $mergedPolicy = new ContentSecurityPolicy(); - $mergedPolicy->addAllowedMediaDomain('mergedPolicy'); - $response - ->expects($this->exactly(2)) - ->method('getContentSecurityPolicy') - ->willReturn($currentPolicy); - $this->contentSecurityPolicyManager - ->expects($this->once()) - ->method('getDefaultPolicy') - ->willReturn($defaultPolicy); - $this->contentSecurityPolicyManager - ->expects($this->once()) - ->method('mergePolicies') - ->with($defaultPolicy, $currentPolicy) - ->willReturn($mergedPolicy); - $response->expects($this->once()) - ->method('setContentSecurityPolicy') - ->with($mergedPolicy); - - $this->middleware->afterController($this->controller, 'test', $response); - } - - public function testAfterControllerEmptyCSP(): void { - $response = $this->createMock(Response::class); - $emptyPolicy = new EmptyContentSecurityPolicy(); - $response->expects($this->any()) - ->method('getContentSecurityPolicy') - ->willReturn($emptyPolicy); - $response->expects($this->never()) - ->method('setContentSecurityPolicy'); - - $this->middleware->afterController($this->controller, 'test', $response); - } - - public function testAfterControllerWithContentSecurityPolicy3Support(): void { - $this->cspNonceManager - ->expects($this->once()) - ->method('browserSupportsCspV3') - ->willReturn(true); - $token = base64_encode('the-nonce'); - $this->cspNonceManager - ->expects($this->once()) - ->method('getNonce') - ->willReturn($token); - $response = $this->createMock(Response::class); - $defaultPolicy = new ContentSecurityPolicy(); - $defaultPolicy->addAllowedImageDomain('defaultpolicy'); - $currentPolicy = new ContentSecurityPolicy(); - $currentPolicy->addAllowedConnectDomain('currentPolicy'); - $mergedPolicy = new ContentSecurityPolicy(); - $mergedPolicy->addAllowedMediaDomain('mergedPolicy'); - $response - ->expects($this->exactly(2)) - ->method('getContentSecurityPolicy') - ->willReturn($currentPolicy); - $this->contentSecurityPolicyManager - ->expects($this->once()) - ->method('getDefaultPolicy') - ->willReturn($defaultPolicy); - $this->contentSecurityPolicyManager - ->expects($this->once()) - ->method('mergePolicies') - ->with($defaultPolicy, $currentPolicy) - ->willReturn($mergedPolicy); - $response->expects($this->once()) - ->method('setContentSecurityPolicy') - ->with($mergedPolicy); - - $this->assertEquals($response, $this->middleware->afterController($this->controller, 'test', $response)); - } -} --- a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php +++ /dev/null @@ -1,70 +0,0 @@ -controller = $this->createMock(Controller::class); - $this->manager = $this->createMock(FeaturePolicyManager::class); - $this->middleware = new FeaturePolicyMiddleware( - $this->manager - ); - } - - public function testAfterController(): void { - $response = $this->createMock(Response::class); - $defaultPolicy = new FeaturePolicy(); - $defaultPolicy->addAllowedCameraDomain('defaultpolicy'); - $currentPolicy = new FeaturePolicy(); - $currentPolicy->addAllowedAutoplayDomain('currentPolicy'); - $mergedPolicy = new FeaturePolicy(); - $mergedPolicy->addAllowedGeoLocationDomain('mergedPolicy'); - $response->method('getFeaturePolicy') - ->willReturn($currentPolicy); - $this->manager->method('getDefaultPolicy') - ->willReturn($defaultPolicy); - $this->manager->method('mergePolicies') - ->with($defaultPolicy, $currentPolicy) - ->willReturn($mergedPolicy); - $response->expects($this->once()) - ->method('setFeaturePolicy') - ->with($mergedPolicy); - - $this->middleware->afterController($this->controller, 'test', $response); - } - - public function testAfterControllerEmptyCSP(): void { - $response = $this->createMock(Response::class); - $emptyPolicy = new EmptyFeaturePolicy(); - $response->method('getFeaturePolicy') - ->willReturn($emptyPolicy); - $response->expects($this->never()) - ->method('setFeaturePolicy'); - - $this->middleware->afterController($this->controller, 'test', $response); - } -} --- a/tests/lib/AppFramework/Middleware/Security/Mock/BruteForceMiddlewareController.php +++ /dev/null @@ -1,26 +0,0 @@ -reflector = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $this->session = $this->createMock(ISession::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->user = $this->createMock(IUser::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->request = $this->createMock(IRequest::class); - $this->userManager = $this->createMock(Manager::class); - $this->controller = new PasswordConfirmationMiddlewareController( - 'test', - $this->createMock(IRequest::class) - ); - $this->remoteAddress = $this->createMock(IRemoteAddress::class); - - $this->middleware = new PasswordConfirmationMiddleware( - $this->reflector, - $this->session, - $this->userSession, - $this->timeFactory, - $this->tokenProvider, - $this->request, - $this->userManager, - $this->remoteAddress, - ); - } - - public function testNoAnnotationNorAttribute(): void { - $this->reflector->reflect($this->controller, __FUNCTION__); - $this->session->expects($this->never()) - ->method($this->anything()); - $this->userSession->expects($this->never()) - ->method($this->anything()); - - $this->middleware->beforeController($this->controller, __FUNCTION__); - } - - public function testDifferentAnnotation(): void { - $this->reflector->reflect($this->controller, __FUNCTION__); - $this->session->expects($this->never()) - ->method($this->anything()); - $this->userSession->expects($this->never()) - ->method($this->anything()); - - $this->middleware->beforeController($this->controller, __FUNCTION__); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] - public function testAnnotation($backend, $lastConfirm, $currentTime, $exception): void { - $this->reflector->reflect($this->controller, __FUNCTION__); - - $this->user->method('getBackendClassName') - ->willReturn($backend); - $this->userSession->method('getUser') - ->willReturn($this->user); - - $this->session->method('get') - ->with('last-password-confirm') - ->willReturn($lastConfirm); - - $this->timeFactory->method('getTime') - ->willReturn($currentTime); - - $token = $this->createMock(IToken::class); - $token->method('getScopeAsArray') - ->willReturn([]); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->willReturn($token); - - $thrown = false; - try { - $this->middleware->beforeController($this->controller, __FUNCTION__); - } catch (NotConfirmedException $e) { - $thrown = true; - } - - $this->assertSame($exception, $thrown); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] - public function testAttribute($backend, $lastConfirm, $currentTime, $exception): void { - $this->reflector->reflect($this->controller, __FUNCTION__); - - $this->user->method('getBackendClassName') - ->willReturn($backend); - $this->userSession->method('getUser') - ->willReturn($this->user); - - $this->session->method('get') - ->with('last-password-confirm') - ->willReturn($lastConfirm); - - $this->timeFactory->method('getTime') - ->willReturn($currentTime); - - $token = $this->createMock(IToken::class); - $token->method('getScopeAsArray') - ->willReturn([]); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->willReturn($token); - - $thrown = false; - try { - $this->middleware->beforeController($this->controller, __FUNCTION__); - } catch (NotConfirmedException $e) { - $thrown = true; - } - - $this->assertSame($exception, $thrown); - } - - public static function dataProvider(): array { - return [ - ['foo', 2000, 4000, true], - ['foo', 2000, 3000, false], - ['user_saml', 2000, 4000, false], - ['user_saml', 2000, 3000, false], - ['foo', 2000, 3815, false], - ['foo', 2000, 3816, true], - ]; - } - - public function testSSO(): void { - static $sessionId = 'mySession1d'; - - $this->reflector->reflect($this->controller, __FUNCTION__); - - $this->user->method('getBackendClassName') - ->willReturn('fictional_backend'); - $this->userSession->method('getUser') - ->willReturn($this->user); - - $this->session->method('get') - ->with('last-password-confirm') - ->willReturn(0); - $this->session->method('getId') - ->willReturn($sessionId); - - $this->timeFactory->method('getTime') - ->willReturn(9876); - - $token = $this->createMock(IToken::class); - $token->method('getScopeAsArray') - ->willReturn([IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true]); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with($sessionId) - ->willReturn($token); - - $thrown = false; - try { - $this->middleware->beforeController($this->controller, __FUNCTION__); - } catch (NotConfirmedException) { - $thrown = true; - } - - $this->assertSame(false, $thrown); - } - - public function testAuthHeader(): void { - $this->reflector->reflect($this->controller, __FUNCTION__); - - $this->user->method('getBackendClassName') - ->willReturn('fictional_backend'); - $this->userSession->method('getUser') - ->willReturn($this->user); - - $this->session->method('get') - ->with('loginname') - ->willReturn('user'); - - $this->request->method('getHeader') - ->with('PHP_AUTH_PW') - ->willReturn('password'); - - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with('user', 'password'); - - $thrown = false; - try { - $this->middleware->beforeController($this->controller, __FUNCTION__); - } catch (NotConfirmedException) { - $thrown = true; - } - - $this->assertSame(false, $thrown); - } -} --- a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php +++ /dev/null @@ -1,297 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->reflector = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $this->limiter = $this->createMock(Limiter::class); - $this->session = $this->createMock(ISession::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->serverConfig = $this->createMock(IConfig::class); - $this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->rateLimitingMiddleware = new RateLimitingMiddleware( - $this->request, - $this->userSession, - $this->reflector, - $this->limiter, - $this->session, - $this->appConfig, - $this->serverConfig, - $this->bruteForceAllowList, - $this->logger - ); - } - - public function testBeforeControllerWithoutAnnotationForAnon(): void { - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->never()) - ->method('registerAnonRequest'); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - - /** @var RateLimitingMiddlewareController|MockObject $controller */ - $controller = $this->createMock(RateLimitingMiddlewareController::class); - $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); - } - - public function testBeforeControllerWithoutAnnotationForLoggedIn(): void { - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->never()) - ->method('registerAnonRequest'); - - $this->userSession->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - - /** @var RateLimitingMiddlewareController|MockObject $controller */ - $controller = $this->createMock(RateLimitingMiddlewareController::class); - $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); - } - - public function testBeforeControllerForAnon(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - - $this->request - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethodWithAnnotation', '10', '100', '127.0.0.1'); - - $this->reflector->reflect($controller, 'testMethodWithAnnotation'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); - } - - public function testBeforeControllerForLoggedIn(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - - $this->limiter - ->expects($this->never()) - ->method('registerAnonRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerUserRequest') - ->with(get_class($controller) . '::testMethodWithAnnotation', '20', '200', $user); - - $this->reflector->reflect($controller, 'testMethodWithAnnotation'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); - } - - public function testBeforeControllerAnonWithFallback(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethodWithAnnotationFallback', '10', '100', '127.0.0.1'); - - $this->reflector->reflect($controller, 'testMethodWithAnnotationFallback'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotationFallback'); - } - - public function testBeforeControllerAttributesForAnon(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - - $this->request - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(false); - - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethodWithAttributes', '10', '100', '127.0.0.1'); - - $this->reflector->reflect($controller, 'testMethodWithAttributes'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); - } - - public function testBeforeControllerAttributesForLoggedIn(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - - $this->limiter - ->expects($this->never()) - ->method('registerAnonRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerUserRequest') - ->with(get_class($controller) . '::testMethodWithAttributes', '20', '200', $user); - - $this->reflector->reflect($controller, 'testMethodWithAttributes'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); - } - - public function testBeforeControllerAttributesAnonWithFallback(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - $this->request - ->expects($this->once()) - ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); - - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - - $this->limiter - ->expects($this->never()) - ->method('registerUserRequest'); - $this->limiter - ->expects($this->once()) - ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethodWithAttributesFallback', '10', '100', '127.0.0.1'); - - $this->reflector->reflect($controller, 'testMethodWithAttributesFallback'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributesFallback'); - } - - public function testAfterExceptionWithOtherException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('My test exception'); - - $controller = new RateLimitingMiddlewareController('test', $this->request); - - $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception')); - } - - public function testAfterExceptionWithJsonBody(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - $this->request - ->expects($this->once()) - ->method('getHeader') - ->with('Accept') - ->willReturn('JSON'); - - $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); - $expected = new DataResponse([], 429 - ); - $this->assertEquals($expected, $result); - } - - public function testAfterExceptionWithHtmlBody(): void { - $controller = new RateLimitingMiddlewareController('test', $this->request); - $this->request - ->expects($this->once()) - ->method('getHeader') - ->with('Accept') - ->willReturn('html'); - - $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException()); - $expected = new TemplateResponse( - 'core', - '429', - [], - TemplateResponse::RENDER_AS_GUEST - ); - $expected->setStatus(429); - $this->assertEquals($expected, $result); - $this->assertIsString($result->render()); - } -} --- a/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php +++ /dev/null @@ -1,136 +0,0 @@ -request = $this->createMock(Request::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->reflector = $this->createMock(ControllerMethodReflector::class); - $this->middleware = new SameSiteCookieMiddleware($this->request, $this->reflector); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testBeforeControllerNoIndex(): void { - $this->request->method('getScriptName') - ->willReturn('/ocs/v2.php'); - - $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo'); - } - - public function testBeforeControllerIndexHasAnnotation(): void { - $this->request->method('getScriptName') - ->willReturn('/index.php'); - - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('NoSameSiteCookieRequired', NoSameSiteCookieRequired::class) - ->willReturn(true); - - $this->middleware->beforeController(new HasAnnotationController('foo', $this->request), 'foo'); - } - - public function testBeforeControllerIndexNoAnnotationPassingCheck(): void { - $this->request->method('getScriptName') - ->willReturn('/index.php'); - - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('NoSameSiteCookieRequired', NoSameSiteCookieRequired::class) - ->willReturn(false); - - $this->request->method('passesLaxCookieCheck') - ->willReturn(true); - - $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo'); - } - - public function testBeforeControllerIndexNoAnnotationFailingCheck(): void { - $this->expectException(LaxSameSiteCookieFailedException::class); - - $this->request->method('getScriptName') - ->willReturn('/index.php'); - - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('NoSameSiteCookieRequired', NoSameSiteCookieRequired::class) - ->willReturn(false); - - $this->request->method('passesLaxCookieCheck') - ->willReturn(false); - - $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo'); - } - - public function testAfterExceptionNoLaxCookie(): void { - $ex = new SecurityException(); - - try { - $this->middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex); - $this->fail(); - } catch (\Exception $e) { - $this->assertSame($ex, $e); - } - } - - public function testAfterExceptionLaxCookie(): void { - $ex = new LaxSameSiteCookieFailedException(); - - $this->request->method('getRequestUri') - ->willReturn('/myrequri'); - - $middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class) - ->setConstructorArgs([$this->request, $this->reflector]) - ->onlyMethods(['setSameSiteCookie']) - ->getMock(); - - $middleware->expects($this->once()) - ->method('setSameSiteCookie'); - - $resp = $middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex); - - $this->assertSame(Http::STATUS_FOUND, $resp->getStatus()); - - $headers = $resp->getHeaders(); - $this->assertSame('/myrequri', $headers['Location']); - } -} --- a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php +++ /dev/null @@ -1,684 +0,0 @@ -authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class); - $this->userSession = $this->createMock(Session::class); - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('test'); - $this->userSession->method('getUser')->willReturn($user); - $this->request = $this->createMock(IRequest::class); - $this->controller = new SecurityMiddlewareController( - 'test', - $this->request - ); - $this->reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $this->logger = $this->createMock(LoggerInterface::class); - $this->navigationManager = $this->createMock(INavigationManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->l10n = $this->createMock(IL10N::class); - $this->middleware = $this->getMiddleware(true, true, false); - $this->secAjaxException = new SecurityException('hey', true); - } - - private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware { - $this->appManager = $this->createMock(IAppManager::class); - $this->appManager->expects($this->any()) - ->method('isEnabledForUser') - ->willReturn($isAppEnabledForUser); - $remoteIpAddress = $this->createMock(IRemoteAddress::class); - $remoteIpAddress->method('allowsAdminActions')->willReturn(true); - - $groupManager = $this->createMock(IGroupManager::class); - $groupManager->method('isAdmin') - ->willReturn($isAdminUser); - $subAdminManager = $this->createMock(ISubAdmin::class); - $subAdminManager->method('isSubAdmin') - ->willReturn($isSubAdmin); - - return new SecurityMiddleware( - $this->request, - $this->reader, - $this->navigationManager, - $this->urlGenerator, - $this->logger, - 'files', - $isLoggedIn, - $groupManager, - $subAdminManager, - $this->appManager, - $this->l10n, - $this->authorizedGroupMapper, - $this->userSession, - $remoteIpAddress - ); - } - - public static function dataNoCSRFRequiredPublicPage(): array { - return [ - ['testAnnotationNoCSRFRequiredPublicPage'], - ['testAnnotationNoCSRFRequiredAttributePublicPage'], - ['testAnnotationPublicPageAttributeNoCSRFRequired'], - ['testAttributeNoCSRFRequiredPublicPage'], - ]; - } - - public static function dataPublicPage(): array { - return [ - ['testAnnotationPublicPage'], - ['testAttributePublicPage'], - ]; - } - - public static function dataNoCSRFRequired(): array { - return [ - ['testAnnotationNoCSRFRequired'], - ['testAttributeNoCSRFRequired'], - ]; - } - - public static function dataPublicPageStrictCookieRequired(): array { - return [ - ['testAnnotationPublicPageStrictCookieRequired'], - ['testAnnotationStrictCookieRequiredAttributePublicPage'], - ['testAnnotationPublicPageAttributeStrictCookiesRequired'], - ['testAttributePublicPageStrictCookiesRequired'], - ]; - } - - public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array { - return [ - ['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'], - ['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'], - ]; - } - - public static function dataNoAdminRequiredNoCSRFRequired(): array { - return [ - ['testAnnotationNoAdminRequiredNoCSRFRequired'], - ['testAttributeNoAdminRequiredNoCSRFRequired'], - ]; - } - - public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array { - return [ - ['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'], - ['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'], - ]; - } - - public static function dataNoCSRFRequiredSubAdminRequired(): array { - return [ - ['testAnnotationNoCSRFRequiredSubAdminRequired'], - ['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'], - ['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'], - ['testAttributeNoCSRFRequiredSubAdminRequired'], - ]; - } - - public static function dataExAppRequired(): array { - return [ - ['testAnnotationExAppRequired'], - ['testAttributeExAppRequired'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] - public function testSetNavigationEntry(string $method): void { - $this->navigationManager->expects($this->once()) - ->method('setActiveEntry') - ->with($this->equalTo('files')); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - /** - * @param string $method - * @param string $test - */ - private function ajaxExceptionStatus($method, $test, $status) { - $isLoggedIn = false; - $isAdminUser = false; - - // isAdminUser requires isLoggedIn call to return true - if ($test === 'isAdminUser') { - $isLoggedIn = true; - } - - $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false); - - try { - $this->reader->reflect($this->controller, $method); - $sec->beforeController($this->controller, $method); - } catch (SecurityException $ex) { - $this->assertEquals($status, $ex->getCode()); - } - - // add assertion if everything should work fine otherwise phpunit will - // complain - if ($status === 0) { - $this->addToAssertionCount(1); - } - } - - public function testAjaxStatusLoggedInCheck(): void { - $this->ajaxExceptionStatus( - 'testNoAnnotationNorAttribute', - 'isLoggedIn', - Http::STATUS_UNAUTHORIZED - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] - public function testAjaxNotAdminCheck(string $method): void { - $this->ajaxExceptionStatus( - $method, - 'isAdminUser', - Http::STATUS_FORBIDDEN - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] - public function testAjaxStatusCSRFCheck(string $method): void { - $this->ajaxExceptionStatus( - $method, - 'passesCSRFCheck', - Http::STATUS_PRECONDITION_FAILED - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] - public function testAjaxStatusAllGood(string $method): void { - $this->ajaxExceptionStatus( - $method, - 'isLoggedIn', - 0 - ); - $this->ajaxExceptionStatus( - $method, - 'isAdminUser', - 0 - ); - $this->ajaxExceptionStatus( - $method, - 'passesCSRFCheck', - 0 - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] - public function testNoChecks(string $method): void { - $this->request->expects($this->never()) - ->method('passesCSRFCheck') - ->willReturn(false); - - $sec = $this->getMiddleware(false, false, false); - - $this->reader->reflect($this->controller, $method); - $sec->beforeController($this->controller, $method); - } - - /** - * @param string $method - * @param string $expects - */ - private function securityCheck($method, $expects, $shouldFail = false) { - // admin check requires login - if ($expects === 'isAdminUser') { - $isLoggedIn = true; - $isAdminUser = !$shouldFail; - } else { - $isLoggedIn = !$shouldFail; - $isAdminUser = false; - } - - $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false); - - if ($shouldFail) { - $this->expectException(SecurityException::class); - } else { - $this->addToAssertionCount(1); - } - - $this->reader->reflect($this->controller, $method); - $sec->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] - public function testCsrfCheck(string $method): void { - $this->expectException(CrossSiteRequestForgeryException::class); - - $this->request->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] - public function testNoCsrfCheck(string $method): void { - $this->request->expects($this->never()) - ->method('passesCSRFCheck') - ->willReturn(false); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] - public function testPassesCsrfCheck(string $method): void { - $this->request->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')] - public function testFailCsrfCheck(string $method): void { - $this->expectException(CrossSiteRequestForgeryException::class); - - $this->request->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')] - public function testStrictCookieRequiredCheck(string $method): void { - $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class); - - $this->request->expects($this->never()) - ->method('passesCSRFCheck'); - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(false); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')] - public function testNoStrictCookieRequiredCheck(string $method): void { - $this->request->expects($this->never()) - ->method('passesStrictCookieCheck') - ->willReturn(false); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')] - public function testPassesStrictCookieRequiredCheck(string $method): void { - $this->request - ->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - - $this->reader->reflect($this->controller, $method); - $this->middleware->beforeController($this->controller, $method); - } - - public static function dataCsrfOcsController(): array { - return [ - [NormalController::class, false, false, true], - [NormalController::class, false, true, true], - [NormalController::class, true, false, true], - [NormalController::class, true, true, true], - - [OCSController::class, false, false, true], - [OCSController::class, false, true, false], - [OCSController::class, true, false, false], - [OCSController::class, true, true, false], - ]; - } - - /** - * @param string $controllerClass - * @param bool $hasOcsApiHeader - * @param bool $hasBearerAuth - * @param bool $exception - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')] - public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void { - $this->request - ->method('getHeader') - ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) { - if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) { - return 'true'; - } - if ($header === 'Authorization' && $hasBearerAuth) { - return 'Bearer TOKEN!'; - } - return ''; - }); - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - - $controller = new $controllerClass('test', $this->request); - $this->reader->reflect($controller, 'foo'); - - try { - $this->middleware->beforeController($controller, 'foo'); - $this->assertFalse($exception); - } catch (CrossSiteRequestForgeryException $e) { - $this->assertTrue($exception); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] - public function testLoggedInCheck(string $method): void { - $this->securityCheck($method, 'isLoggedIn'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] - public function testFailLoggedInCheck(string $method): void { - $this->securityCheck($method, 'isLoggedIn', true); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] - public function testIsAdminCheck(string $method): void { - $this->securityCheck($method, 'isAdminUser'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] - public function testIsNotSubAdminCheck(string $method): void { - $this->reader->reflect($this->controller, $method); - $sec = $this->getMiddleware(true, false, false); - - $this->expectException(SecurityException::class); - $sec->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testIsSubAdminCheck(string $method): void { - $this->reader->reflect($this->controller, $method); - $sec = $this->getMiddleware(true, false, true); - - $sec->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')] - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testIsSubAdminAndAdminCheck(string $method): void { - $this->reader->reflect($this->controller, $method); - $sec = $this->getMiddleware(true, true, true); - - $sec->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')] - public function testFailIsAdminCheck(string $method): void { - $this->securityCheck($method, 'isAdminUser', true); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')] - public function testRestrictedAppLoggedInPublicPage(string $method): void { - $middleware = $this->getMiddleware(true, false, false); - $this->reader->reflect($this->controller, $method); - - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); - - $this->appManager->method('isEnabledForUser') - ->with('files') - ->willReturn(false); - - $middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')] - public function testRestrictedAppNotLoggedInPublicPage(string $method): void { - $middleware = $this->getMiddleware(false, false, false); - $this->reader->reflect($this->controller, $method); - - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); - - $this->appManager->method('isEnabledForUser') - ->with('files') - ->willReturn(false); - - $middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')] - public function testRestrictedAppLoggedIn(string $method): void { - $middleware = $this->getMiddleware(true, false, false, false); - $this->reader->reflect($this->controller, $method); - - $this->appManager->method('getAppPath') - ->with('files') - ->willReturn('foo'); - - $this->expectException(AppNotEnabledException::class); - $middleware->beforeController($this->controller, $method); - } - - public function testAfterExceptionNotCaughtThrowsItAgain(): void { - $ex = new \Exception(); - $this->expectException(\Exception::class); - $this->middleware->afterException($this->controller, 'test', $ex); - } - - public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void { - $this->request = new Request( - [ - 'server' - => [ - 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->middleware = $this->getMiddleware(false, false, false); - $this->urlGenerator - ->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.login.showLoginForm', - [ - 'redirect_url' => 'nextcloud/index.php/apps/specialapp', - ] - ) - ->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp'); - $this->logger - ->expects($this->once()) - ->method('debug'); - $response = $this->middleware->afterException( - $this->controller, - 'test', - new NotLoggedInException() - ); - $expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp'); - $this->assertEquals($expected, $response); - } - - public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void { - $this->request = new Request( - [ - 'server' => [ - 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp', - ], - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - - $this->middleware = $this->getMiddleware(false, false, false); - $response = $this->middleware->afterException( - $this->controller, - 'test', - new StrictCookieMissingException() - ); - - $expected = new RedirectResponse(\OC::$WEBROOT . '/'); - $this->assertEquals($expected, $response); - } - - /** - * @return array - */ - public static function exceptionProvider(): array { - return [ - [ - new AppNotEnabledException(), - ], - [ - new CrossSiteRequestForgeryException(), - ], - [ - new NotAdminException(''), - ], - ]; - } - - /** - * @param SecurityException $exception - */ - #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')] - public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void { - $this->request = new Request( - [ - 'server' - => [ - 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp' - ] - ], - $this->createMock(IRequestId::class), - $this->createMock(IConfig::class) - ); - $this->middleware = $this->getMiddleware(false, false, false); - $this->logger - ->expects($this->once()) - ->method('debug'); - $response = $this->middleware->afterException( - $this->controller, - 'test', - $exception - ); - $expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest'); - $expected->setStatus($exception->getCode()); - $this->assertEquals($expected, $response); - } - - public function testAfterAjaxExceptionReturnsJSONError(): void { - $response = $this->middleware->afterException($this->controller, 'test', - $this->secAjaxException); - - $this->assertTrue($response instanceof JSONResponse); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] - public function testExAppRequired(string $method): void { - $middleware = $this->getMiddleware(true, false, false); - $this->reader->reflect($this->controller, $method); - - $session = $this->createMock(ISession::class); - $session->method('get')->with('app_api')->willReturn(true); - $this->userSession->method('getSession')->willReturn($session); - - $this->request->expects($this->once()) - ->method('passesStrictCookieCheck') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - - $middleware->beforeController($this->controller, $method); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')] - public function testExAppRequiredError(string $method): void { - $middleware = $this->getMiddleware(true, false, false, false); - $this->reader->reflect($this->controller, $method); - - $session = $this->createMock(ISession::class); - $session->method('get')->with('app_api')->willReturn(false); - $this->userSession->method('getSession')->willReturn($session); - - $this->expectException(ExAppRequiredException::class); - $middleware->beforeController($this->controller, $method); - } -} --- a/tests/lib/AppFramework/Middleware/SessionMiddlewareTest.php +++ /dev/null @@ -1,99 +0,0 @@ -reflector = $this->createMock(ControllerMethodReflector::class); - $this->session = $this->createMock(ISession::class); - $this->controller = new UseSessionController('app', $this->createMock(IRequest::class)); - $this->middleware = new SessionMiddleware( - $this->reflector, - $this->session, - ); - } - - public function testSessionNotClosedOnBeforeController(): void { - $this->configureSessionMock(0, 1); - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('UseSession', UseSession::class) - ->willReturn(true); - - $this->middleware->beforeController($this->controller, 'withAnnotation'); - } - - public function testSessionClosedOnAfterController(): void { - $this->configureSessionMock(1); - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('UseSession', UseSession::class) - ->willReturn(true); - - $this->middleware->afterController($this->controller, 'withAnnotation', new Response()); - } - - public function testSessionReopenedAndClosedOnBeforeController(): void { - $this->configureSessionMock(1, 1); - $this->reflector->expects(self::exactly(2)) - ->method('hasAnnotationOrAttribute') - ->with('UseSession', UseSession::class) - ->willReturn(true); - - $this->middleware->beforeController($this->controller, 'withAnnotation'); - $this->middleware->afterController($this->controller, 'withAnnotation', new Response()); - } - - public function testSessionClosedOnBeforeController(): void { - $this->configureSessionMock(0); - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('UseSession', UseSession::class) - ->willReturn(false); - - $this->middleware->beforeController($this->controller, 'without'); - } - - public function testSessionNotClosedOnAfterController(): void { - $this->configureSessionMock(0); - $this->reflector->expects(self::once()) - ->method('hasAnnotationOrAttribute') - ->with('UseSession', UseSession::class) - ->willReturn(false); - - $this->middleware->afterController($this->controller, 'without', new Response()); - } - - private function configureSessionMock(int $expectedCloseCount, int $expectedReopenCount = 0): void { - $this->session->expects($this->exactly($expectedCloseCount)) - ->method('close'); - $this->session->expects($this->exactly($expectedReopenCount)) - ->method('reopen'); - } -} --- a/tests/lib/AppFramework/OCS/BaseResponseTest.php +++ /dev/null @@ -1,82 +0,0 @@ -array; - } -} - -class BaseResponseTest extends \Test\TestCase { - public function testToXml(): void { - /** @var BaseResponse $response */ - $response = $this->createMock(BaseResponse::class); - - $writer = new \XMLWriter(); - $writer->openMemory(); - $writer->setIndent(false); - $writer->startDocument(); - - $data = [ - 'hello' => 'hello', - 'information' => [ - '@test' => 'some data', - 'someElement' => 'withAttribute', - ], - 'value without key', - 'object' => new \stdClass(), - ]; - - $this->invokePrivate($response, 'toXml', [$data, $writer]); - $writer->endDocument(); - - $this->assertEquals( - "\nhellowithAttributevalue without key\n", - $writer->outputMemory(true) - ); - } - - public function testToXmlJsonSerializable(): void { - /** @var BaseResponse $response */ - $response = $this->createMock(BaseResponse::class); - - $writer = new \XMLWriter(); - $writer->openMemory(); - $writer->setIndent(false); - $writer->startDocument(); - - $data = [ - 'hello' => 'hello', - 'information' => new ArrayValue([ - '@test' => 'some data', - 'someElement' => 'withAttribute', - ]), - 'value without key', - 'object' => new \stdClass(), - ]; - - $this->invokePrivate($response, 'toXml', [$data, $writer]); - $writer->endDocument(); - - $this->assertEquals( - "\nhellowithAttributevalue without key\n", - $writer->outputMemory(true) - ); - } -} --- a/tests/lib/AppFramework/OCS/V2ResponseTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertEquals($expected, $response->getStatus()); - } - - public static function providesStatusCodes(): array { - return [ - [Http::STATUS_OK, 200], - [Http::STATUS_BAD_REQUEST, 104], - [Http::STATUS_BAD_REQUEST, 1000], - [201, 201], - [Http::STATUS_UNAUTHORIZED, OCSController::RESPOND_UNAUTHORISED], - [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_SERVER_ERROR], - [Http::STATUS_NOT_FOUND, OCSController::RESPOND_NOT_FOUND], - [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_UNKNOWN_ERROR], - ]; - } -} --- a/tests/lib/AppFramework/ORM/RepositoryTest.php +++ /dev/null @@ -1,919 +0,0 @@ - */ - public static array $entitiesClasses = [ - NoPrimaryKey::class, - PrimaryKey::class, - CompositeKey::class, - Customer::class, - Cart::class, - CascadeParent::class, - CascadeChild::class, - Merchant::class, - Order::class, - EnumOrder::class, - ]; - - public static function setUpBeforeClass(): void { - $schema = new SchemaWrapper(Server::get(Connection::class)); - $entityManager = Server::get(EntityManager::class); - foreach (static::$entitiesClasses as $entityClass) { - try { - $entityManager->createTable($entityClass, $schema); - } catch (\RuntimeException) { - self::assertEquals(NoPrimaryKey::class, $entityClass); - } - } - Server::get(Connection::class)->migrateToSchema($schema->getWrappedSchema()); - } - - public static function tearDownAfterClass(): void { - $entityManager = Server::get(EntityManager::class); - $prefix = Server::get(IConfig::class)->getSystemValueString('dbtableprefix', 'oc_'); - foreach (array_reverse(static::$entitiesClasses) as $entityClass) { - try { - $entityManager->dropTable($entityClass, $prefix); - } catch (\RuntimeException) { - self::assertEquals(NoPrimaryKey::class, $entityClass); - } - } - } - - /** - * @template T of object - * @param class-string $entityClass - * @return Repository - */ - private function getRepository(string $entityClass): Repository { - return Server::get(EntityManager::class)->getRepository($entityClass); - } - - public function testMissingPrimaryKey(): void { - $this->expectException(\RuntimeException::class); - - $repo = $this->getRepository(NoPrimaryKey::class); - $_ = $repo->getTableName(); - } - - public function testPrimaryKey(): void { - $repo = $this->getRepository(PrimaryKey::class); - $this->assertEquals('repository_test_test2', $repo->getTableName()); - $entity = new PrimaryKey(); - $entity->name = null; - $entity->notNullable = 'ab'; - $entity->integer = 5; - $entity->bigInt = 5; - $entity->float = 3.0; - $entity->date = new \DateTime('now'); - - $repo->insert($entity); - $this->assertNotNull($entity->id); - - // Confirm only one entry in the DB - $entities = iterator_to_array($repo->yieldAll()); - $this->assertCount(1, $entities); - $savedEntity = $entities[0]; - $this->assertEquals($entity->name, $savedEntity->name); - $this->assertEquals($entity->notNullable, $savedEntity->notNullable); - $this->assertEquals($entity->integer, $savedEntity->integer); - $this->assertEquals($entity->bigInt, $savedEntity->bigInt); - $this->assertEquals($entity->float, $savedEntity->float); - $this->assertEquals($entity->date->getTimestamp(), $savedEntity->date->getTimestamp()); - - // Search by primary key - $savedEntity = $repo->findOneBy(['id' => $entity->id]); - $this->assertEquals($entity->name, $savedEntity->name); - $this->assertEquals($entity->notNullable, $savedEntity->notNullable); - $this->assertEquals($entity->integer, $savedEntity->integer); - $this->assertEquals($entity->bigInt, $savedEntity->bigInt); - $this->assertEquals($entity->float, $savedEntity->float); - $this->assertEquals($entity->date->getTimestamp(), $savedEntity->date->getTimestamp()); - - // Search by null - $savedEntity = $repo->findOneBy(['name' => $entity->name]); - $this->assertEquals($entity->name, $savedEntity->name); - $this->assertEquals($entity->notNullable, $savedEntity->notNullable); - $this->assertEquals($entity->integer, $savedEntity->integer); - $this->assertEquals($entity->bigInt, $savedEntity->bigInt); - $this->assertEquals($entity->float, $savedEntity->float); - $this->assertEquals($entity->date->getTimestamp(), $savedEntity->date->getTimestamp()); - - // Search by other fields - $savedEntity = $repo->findOneBy([ - 'notNullable' => $entity->notNullable, - 'integer' => $entity->integer, - 'bigInt' => $entity->bigInt, - ]); - /** @psalm-assert PrimaryKey $savedEntity */ - $this->assertEquals($entity->name, $savedEntity->name); - $this->assertEquals($entity->notNullable, $savedEntity->notNullable); - $this->assertEquals($entity->integer, $savedEntity->integer); - $this->assertEquals($entity->bigInt, $savedEntity->bigInt); - $this->assertEquals($entity->float, $savedEntity->float); - $this->assertEquals($entity->date->getTimestamp(), $savedEntity->date->getTimestamp()); - - // update - $entity->name = 'not null anymore'; - $repo->update($entity); - - $savedEntity = $repo->findOneBy(['id' => $entity->id]); - $this->assertEquals($entity->name, $savedEntity->name); - - // delete - $repo->delete($savedEntity); - } - - public function testDeleteBy(): void { - $repo = $this->getRepository(PrimaryKey::class); - - $entity = new PrimaryKey(); - $entity->name = null; - $entity->notNullable = 'ab'; - $entity->integer = 5; - $entity->bigInt = 5; - $entity->float = 3.0; - $entity->date = new \DateTime('now'); - - $repo->insert($entity); - $this->assertNotNull($entity->id); - - // Confirm only one entry in the DB - $entities = iterator_to_array($repo->yieldAll()); - $this->assertCount(1, $entities); - - $repo->deleteBy(['id' => $entity->id]); - - $entities = iterator_to_array($repo->yieldAll()); - $this->assertCount(0, $entities); - } - - public function testFindByAfterId(): void { - $repo = $this->getRepository(PrimaryKey::class); - - $entities = []; - for ($i = 0; $i < 5; $i++) { - $entity = new PrimaryKey(); - $entity->name = 'entry' . $i; - $entity->notNullable = 'testFindByAfterId'; - $entity->integer = $i; - $entity->bigInt = $i; - $entity->float = 1.0; - $entity->date = new \DateTime('now'); - $repo->insert($entity); - $entities[] = $entity; - } - - // Walk through every page, seeking from the last id of the previous one, until a page - // comes back smaller than the requested limit. - $seenIds = []; - $lastId = null; - do { - $page = iterator_to_array($repo->findByAfterId(['notNullable' => 'testFindByAfterId'], $lastId, 2)); - foreach ($page as $entity) { - $seenIds[] = $entity->id; - $lastId = $entity->id; - } - } while (count($page) === 2); - - $this->assertEquals(array_map(static fn (PrimaryKey $entity): ?int => $entity->id, $entities), $seenIds); - - foreach ($entities as $entity) { - $repo->delete($entity); - } - } - - public function testFindByAfterIdWithCompositePrimaryKeyThrows(): void { - $repo = $this->getRepository(CompositeKey::class); - - $this->expectException(\LogicException::class); - iterator_to_array($repo->findByAfterId([], null, 10)); - } - - public function testFindByBeforeId(): void { - $repo = $this->getRepository(PrimaryKey::class); - - $entities = []; - for ($i = 0; $i < 5; $i++) { - $entity = new PrimaryKey(); - $entity->name = 'entry' . $i; - $entity->notNullable = 'testFindByBeforeId'; - $entity->integer = $i; - $entity->bigInt = $i; - $entity->float = 1.0; - $entity->date = new \DateTime('now'); - $repo->insert($entity); - $entities[] = $entity; - } - - // Walk through every page, seeking from the last id of the previous one, until a page - // comes back smaller than the requested limit. - $seenIds = []; - $lastId = null; - do { - $page = iterator_to_array($repo->findByBeforeId(['notNullable' => 'testFindByBeforeId'], $lastId, 2)); - foreach ($page as $entity) { - $seenIds[] = $entity->id; - $lastId = $entity->id; - } - } while (count($page) === 2); - - $this->assertEquals(array_reverse(array_map(static fn (PrimaryKey $entity): ?int => $entity->id, $entities)), $seenIds); - - foreach ($entities as $entity) { - $repo->delete($entity); - } - } - - public function testFindByBeforeIdWithCompositePrimaryKeyThrows(): void { - $repo = $this->getRepository(CompositeKey::class); - - $this->expectException(\LogicException::class); - iterator_to_array($repo->findByBeforeId([], null, 10)); - } - - public function testCompositePrimaryKey(): void { - $repo = $this->getRepository(CompositeKey::class); - $this->assertEquals('repository_composite_key', $repo->getTableName()); - - $entity = new CompositeKey(); - $entity->tenantId = 1; - $entity->itemId = 42; - $entity->label = 'first'; - $repo->insert($entity); - - // A second entity sharing the tenantId but not the itemId is a distinct row. - $other = new CompositeKey(); - $other->tenantId = 1; - $other->itemId = 43; - $other->label = 'second'; - $repo->insert($other); - - $savedEntity = $repo->findOneBy(['tenantId' => 1, 'itemId' => 42]); - $this->assertEquals('first', $savedEntity->label); - - $savedOther = $repo->findOneBy(['tenantId' => 1, 'itemId' => 43]); - $this->assertEquals('second', $savedOther->label); - - // update - $entity->label = 'updated'; - $repo->update($entity); - - $savedEntity = $repo->findOneBy(['tenantId' => 1, 'itemId' => 42]); - $this->assertEquals('updated', $savedEntity->label); - // The other row with the same tenantId is untouched. - $savedOther = $repo->findOneBy(['tenantId' => 1, 'itemId' => 43]); - $this->assertEquals('second', $savedOther->label); - - // delete - $repo->delete($entity); - $this->assertCount(0, iterator_to_array($repo->findBy(['tenantId' => 1, 'itemId' => 42]))); - $this->assertCount(1, iterator_to_array($repo->findBy(['tenantId' => 1, 'itemId' => 43]))); - - $repo->delete($savedOther); - } - - public function testCompositePrimaryKeyMissingPartOnInsertThrows(): void { - $repo = $this->getRepository(CompositeKey::class); - - $entity = new CompositeKey(); - $entity->tenantId = 2; - // itemId intentionally left unset: composite keys can't rely on DB autoincrement. - $entity->label = 'incomplete'; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('is part of a composite primary key and must be set before insert()'); - - $repo->insert($entity); - } - - public function testCompositePrimaryKeyMissingPartOnUpdateThrows(): void { - $repo = $this->getRepository(CompositeKey::class); - - $entity = new CompositeKey(); - $entity->tenantId = 3; - $entity->itemId = 1; - $entity->label = 'x'; - $repo->insert($entity); - - $entity->itemId = null; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Trying to update an entity with no primary key set.'); - - try { - $repo->update($entity); - } finally { - $entity->itemId = 1; - $repo->delete($entity); - } - } - - public function testOneToOne(): void { - $cartRepo = $this->getRepository(Cart::class); - $customerRepo = $this->getRepository(Customer::class); - - $customer = new Customer(); - $customer->name = 'foo'; - $customerRepo->insert($customer); - $this->assertNotNull($customer->id); - - $savedCustomer = $customerRepo->findOneBy(['id' => $customer->id]); - $this->assertNull($savedCustomer->cart); - - $cart = new Cart(); - $cart->customer = $customer; - $cartRepo->insert($cart); - $this->assertNotNull($cart->id); - - // Owning side: reading the cart back resolves the customer relation - $savedCart = $cartRepo->findOneBy(['id' => $cart->id]); - $this->assertNotNull($savedCart->customer); - $this->assertEquals($customer->id, $savedCart->customer->id); - $this->assertEquals($customer->name, $savedCart->customer->name); - - // Inverse side: reading the customer back resolves the cart relation - $savedCustomer = $customerRepo->findOneBy(['id' => $customer->id]); - $this->assertNotNull($savedCustomer->cart); - $this->assertEquals($cart->id, $savedCustomer->cart->id); - } - - public function testInsertOnMappedBySideThrows(): void { - $customerRepo = $this->getRepository(Customer::class); - - $cart = new Cart(); - $cart->customer = null; - - $customer = new Customer(); - $customer->name = 'foo'; - $customer->cart = $cart; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('is the mappedBy (inverse) side of a OneToOne relation'); - - $customerRepo->insert($customer); - } - - public function testOneToOneUniqueConstraint(): void { - $cartRepo = $this->getRepository(Cart::class); - $customerRepo = $this->getRepository(Customer::class); - - $customer = new Customer(); - $customer->name = 'shared'; - $customerRepo->insert($customer); - - $cart1 = new Cart(); - $cart1->customer = $customer; - $cartRepo->insert($cart1); - - $cart2 = new Cart(); - $cart2->customer = $customer; - - try { - $cartRepo->insert($cart2); - $this->fail('Expected inserting a second Cart for the same Customer to violate the unique constraint.'); - } catch (\OCP\DB\Exception $e) { - $this->assertEquals(\OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION, $e->getReason()); - } - } - - public function testOneToOneNullRelation(): void { - $customerRepo = $this->getRepository(Customer::class); - - $customer = new Customer(); - $customer->name = 'no cart yet'; - $customerRepo->insert($customer); - - $savedCustomer = $customerRepo->findOneBy(['id' => $customer->id]); - $this->assertNull($savedCustomer->cart); - } - - public function testMappedByOnDeleteThrows(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('sets JoinColumn::$onDelete on the mappedBy (inverse) side'); - - Server::get(EntityManager::class)->getEntityInfo(InvalidMappedByOnDelete::class); - } - - public function testTypoInMappedByThrows(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('has no property named \'ownerTypo\''); - - Server::get(EntityManager::class)->getEntityInfo(TypoTarget::class); - } - - public function testDeleteWithDanglingMappedByThrows(): void { - $cartRepo = $this->getRepository(Cart::class); - $customerRepo = $this->getRepository(Customer::class); - - $customer = new Customer(); - $customer->name = 'has a cart'; - $customerRepo->insert($customer); - - $cart = new Cart(); - $cart->customer = $customer; - $cartRepo->insert($cart); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('still references it'); - - $customerRepo->delete($customer); - } - - public function testDeleteCascadesWhenConfigured(): void { - $parentRepo = $this->getRepository(CascadeParent::class); - $childRepo = $this->getRepository(CascadeChild::class); - - $parent = new CascadeParent(); - $parentRepo->insert($parent); - - $child = new CascadeChild(); - $child->parent = $parent; - $childRepo->insert($child); - - $parentRepo->delete($parent); - - $entities = iterator_to_array($childRepo->yieldAll()); - $this->assertCount(0, $entities); - } - - public function testManyToOne(): void { - $merchantRepo = $this->getRepository(Merchant::class); - $orderRepo = $this->getRepository(Order::class); - - $merchant = new Merchant(); - $merchant->name = 'Acme'; - $merchantRepo->insert($merchant); - - $order1 = new Order(); - $order1->merchant = $merchant; - $orderRepo->insert($order1); - - // Unlike OneToOne, a second Order for the same Merchant must be allowed. - $order2 = new Order(); - $order2->merchant = $merchant; - $orderRepo->insert($order2); - - $savedOrder1 = $orderRepo->findOneBy(['id' => $order1->id]); - $this->assertNotNull($savedOrder1->merchant); - $this->assertEquals($merchant->id, $savedOrder1->merchant->id); - $this->assertEquals($merchant->name, $savedOrder1->merchant->name); - - $savedOrder2 = $orderRepo->findOneBy(['id' => $order2->id]); - $this->assertEquals($merchant->id, $savedOrder2->merchant->id); - } - - public function testManyToOneNullRelation(): void { - $orderRepo = $this->getRepository(Order::class); - - $order = new Order(); - $orderRepo->insert($order); - - $savedOrder = $orderRepo->findOneBy(['id' => $order->id]); - $this->assertNull($savedOrder->merchant); - } - - private function normalizeSql(string $sql): string { - return str_replace(['`', '"'], '', $sql); - } - - public function testOwningSideGeneratesLeftJoin(): void { - $cartRepo = $this->getRepository(Cart::class); - - /** @var array{0: IQueryBuilder, 1: array} $result */ - $result = self::invokePrivate($cartRepo, 'getJoinedSelectQueryBuilder', [['id' => 1], []]); - [$qb, $relations] = $result; - - $this->assertCount(1, $relations); - $this->assertEquals( - 'SELECT e.id AS e_id, r0.id AS r0_id, r0.name AS r0_name ' - . 'FROM *PREFIX*repository_cart e ' - . 'LEFT JOIN *PREFIX*repository_customer r0 ON e.customer_id = r0.id ' - . 'WHERE e.id = :dcValue1', - $this->normalizeSql($qb->getSQL()), - ); - } - - public function testManyToOneGeneratesLeftJoin(): void { - $orderRepo = $this->getRepository(Order::class); - - /** @var array{0: IQueryBuilder, 1: array} $result */ - $result = self::invokePrivate($orderRepo, 'getJoinedSelectQueryBuilder', [['id' => 1], []]); - [$qb, $relations] = $result; - - $this->assertCount(1, $relations); - $this->assertEquals( - 'SELECT e.id AS e_id, r0.id AS r0_id, r0.name AS r0_name ' - . 'FROM *PREFIX*repository_order e ' - . 'LEFT JOIN *PREFIX*repository_merchant r0 ON e.merchant_id = r0.id ' - . 'WHERE e.id = :dcValue1', - $this->normalizeSql($qb->getSQL()), - ); - } - - public function testInverseSideGeneratesLeftJoin(): void { - $customerRepo = $this->getRepository(Customer::class); - - /** @var array{0: IQueryBuilder, 1: array} $result */ - $result = self::invokePrivate($customerRepo, 'getJoinedSelectQueryBuilder', [['id' => 1], []]); - [$qb, $relations] = $result; - - $this->assertCount(1, $relations); - $this->assertEquals( - 'SELECT e.id AS e_id, e.name AS e_name, r0.id AS r0_id ' - . 'FROM *PREFIX*repository_customer e ' - . 'LEFT JOIN *PREFIX*repository_cart r0 ON r0.customer_id = e.id ' - . 'WHERE e.id = :dcValue1', - $this->normalizeSql($qb->getSQL()), - ); - } - - public function testEntityWithoutRelationsGeneratesNoJoin(): void { - $repo = $this->getRepository(PrimaryKey::class); - - /** @var array{0: IQueryBuilder, 1: array} $result */ - $result = self::invokePrivate($repo, 'getJoinedSelectQueryBuilder', [['id' => 1], []]); - [$qb, $relations] = $result; - - $this->assertCount(0, $relations); - $this->assertEquals( - 'SELECT e.id AS e_id, e.name AS e_name, e.not_nullable AS e_not_nullable, ' - . 'e.integer_val AS e_integer_val, e.bigint_val AS e_bigint_val, ' - . 'e.float_val AS e_float_val, e.date_val AS e_date_val ' - . 'FROM *PREFIX*repository_test_test2 e ' - . 'WHERE e.id = :dcValue1', - $this->normalizeSql($qb->getSQL()), - ); - } - - public function testEnumColumnRoundTrip(): void { - $repo = $this->getRepository(EnumOrder::class); - - $order = new EnumOrder(); - $order->status = OrderStatus::Placed; - $order->priority = Priority::High; - $repo->insert($order); - $this->assertNotNull($order->id); - - $saved = $repo->findOneBy(['id' => $order->id]); - $this->assertSame(OrderStatus::Placed, $saved->status); - $this->assertSame(Priority::High, $saved->priority); - $this->assertNull($saved->previousStatus); - - $repo->delete($saved); - } - - public function testEnumColumnDefault(): void { - $repo = $this->getRepository(EnumOrder::class); - - $order = new EnumOrder(); - $order->priority = Priority::Low; - $this->assertSame(OrderStatus::Draft, $order->status); - - $repo->insert($order); - $saved = $repo->findOneBy(['id' => $order->id]); - $this->assertSame(OrderStatus::Draft, $saved->status); - - $repo->delete($saved); - } - - public function testEnumColumnUpdate(): void { - $repo = $this->getRepository(EnumOrder::class); - - $order = new EnumOrder(); - $order->status = OrderStatus::Draft; - $order->priority = Priority::Low; - $repo->insert($order); - - $order->previousStatus = $order->status; - $order->status = OrderStatus::Shipped; - $repo->update($order); - - $saved = $repo->findOneBy(['id' => $order->id]); - $this->assertSame(OrderStatus::Shipped, $saved->status); - $this->assertSame(OrderStatus::Draft, $saved->previousStatus); - - $repo->delete($saved); - } - - public function testFindByEnumCriteria(): void { - $repo = $this->getRepository(EnumOrder::class); - - $placed = new EnumOrder(); - $placed->status = OrderStatus::Placed; - $placed->priority = Priority::High; - $repo->insert($placed); - - $shipped = new EnumOrder(); - $shipped->status = OrderStatus::Shipped; - $shipped->priority = Priority::Low; - $repo->insert($shipped); - - $found = $repo->findOneBy(['status' => OrderStatus::Placed]); - $this->assertSame($placed->id, $found->id); - - // IN (...) form with a list of enum cases - $both = iterator_to_array($repo->findBy(['status' => [OrderStatus::Placed, OrderStatus::Shipped]])); - $this->assertCount(2, $both); - - $repo->delete($placed); - $repo->delete($shipped); - } - - public function testDeleteByEnumCriteria(): void { - $repo = $this->getRepository(EnumOrder::class); - - $order = new EnumOrder(); - $order->status = OrderStatus::Draft; - $order->priority = Priority::Low; - $repo->insert($order); - - $repo->deleteBy(['status' => OrderStatus::Draft]); - - $this->assertCount(0, iterator_to_array($repo->findBy(['id' => $order->id]))); - } - - public function testEnumTypeMustExist(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('that class is not an enum'); - - Server::get(EntityManager::class)->getEntityInfo(EnumUnknownClass::class); - } - - public function testEnumTypeMustBeBacked(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('that enum is not backed'); - - Server::get(EntityManager::class)->getEntityInfo(EnumNotBacked::class); - } - - public function testEnumTypeMustMatchPropertyType(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('the property is typed as string instead'); - - Server::get(EntityManager::class)->getEntityInfo(EnumTypeMismatch::class); - } - - public function testEnumBackingTypeMustMatchColumnType(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("cannot hold a(n) int-backed enum's value"); - - Server::get(EntityManager::class)->getEntityInfo(EnumColumnTypeMismatch::class); - } -} --- a/tests/lib/AppFramework/Routing/RouteParserTest.php +++ /dev/null @@ -1,351 +0,0 @@ -parser = new RouteParser(); - } - - public function testParseRoutes(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open')); - $this->assertArrayHasKey('app1.folders.create', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('app1.folders.create')); - } - - public function testParseRoutesRootApps(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'core'); - $this->assertArrayHasKey('core.folders.open', $collection->all()); - $this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('core.folders.open')); - $this->assertArrayHasKey('core.folders.create', $collection->all()); - $this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('core.folders.create')); - } - - public function testParseRoutesWithResources(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ], 'resources' => [ - 'names' => ['url' => '/names'], - 'folder_names' => ['url' => '/folder/names'], - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open', $collection->all()); - $this->assertSimpleResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection); - $this->assertSimpleResource('/apps/app1/names', 'names', 'NamesController', 'app1', $collection); - } - - public function testParseRoutesWithPostfix(): void { - $routes = ['routes' => [ - ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'], - ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.update', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('app1.folders.update')); - $this->assertArrayHasKey('app1.folders.update-edit', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('app1.folders.update-edit')); - } - - public function testParseRoutesKebabCaseAction(): void { - $routes = ['routes' => [ - ['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open_folder', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('app1.folders.open_folder')); - } - - public function testParseRoutesKebabCaseController(): void { - $routes = ['routes' => [ - ['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.my_folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('app1.my_folders.open')); - } - - public function testParseRoutesLowercaseVerb(): void { - $routes = ['routes' => [ - ['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.delete', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('app1.folders.delete')); - } - - public function testParseRoutesMissingVerb(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open'] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('app1.folders.open')); - } - - public function testParseRoutesWithRequirements(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('app1.folders.open')); - } - - public function testParseRoutesWithDefaults(): void { - $routes = ['routes' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']] - ]]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertArrayHasKey('app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('app1.folders.open')); - } - - public function testParseRoutesInvalidName(): void { - $routes = ['routes' => [ - ['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $this->expectException(\UnexpectedValueException::class); - $this->parser->parseDefaultRoutes($routes, 'app1'); - } - - public function testParseRoutesInvalidName2(): void { - $routes = ['routes' => [ - ['name' => 'folders#open#action', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $this->expectException(\UnexpectedValueException::class); - $this->parser->parseDefaultRoutes($routes, 'app1'); - } - - public function testParseRoutesEmpty(): void { - $routes = ['routes' => []]; - - $collection = $this->parser->parseDefaultRoutes($routes, 'app1'); - $this->assertEquals(0, $collection->count()); - } - - // OCS routes - - public function testParseOcsRoutes(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open')); - $this->assertArrayHasKey('ocs.app1.folders.create', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/create', 'POST', 'FoldersController', 'create', route: $collection->get('ocs.app1.folders.create')); - } - - public function testParseOcsRoutesRootApps(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ['name' => 'folders#create', 'url' => '/{folderId}/create', 'verb' => 'POST'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'core'); - $this->assertArrayHasKey('ocs.core.folders.open', $collection->all()); - $this->assertSimpleRoute('/{folderId}/open', 'GET', 'FoldersController', 'open', app: 'core', route: $collection->get('ocs.core.folders.open')); - $this->assertArrayHasKey('ocs.core.folders.create', $collection->all()); - $this->assertSimpleRoute('/{folderId}/create', 'POST', 'FoldersController', 'create', app: 'core', route: $collection->get('ocs.core.folders.create')); - } - - public function testParseOcsRoutesWithPostfix(): void { - $routes = ['ocs' => [ - ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'POST'], - ['name' => 'folders#update', 'url' => '/{folderId}/update', 'verb' => 'PUT', 'postfix' => '-edit'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.update', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'POST', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update')); - $this->assertArrayHasKey('ocs.app1.folders.update-edit', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/update', 'PUT', 'FoldersController', 'update', route: $collection->get('ocs.app1.folders.update-edit')); - } - - public function testParseOcsRoutesKebabCaseAction(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open_folder', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open_folder', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'openFolder', route: $collection->get('ocs.app1.folders.open_folder')); - } - - public function testParseOcsRoutesKebabCaseController(): void { - $routes = ['ocs' => [ - ['name' => 'my_folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.my_folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'MyFoldersController', 'open', route: $collection->get('ocs.app1.my_folders.open')); - } - - public function testParseOcsRoutesLowercaseVerb(): void { - $routes = ['ocs' => [ - ['name' => 'folders#delete', 'url' => '/{folderId}/delete', 'verb' => 'delete'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.delete', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/delete', 'DELETE', 'FoldersController', 'delete', route: $collection->get('ocs.app1.folders.delete')); - } - - public function testParseOcsRoutesMissingVerb(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open'] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', route: $collection->get('ocs.app1.folders.open')); - } - - public function testParseOcsRoutesWithRequirements(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'requirements' => ['folderId' => '\d+']] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', requirements: ['folderId' => '\d+'], route: $collection->get('ocs.app1.folders.open')); - } - - public function testParseOcsRoutesWithDefaults(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET', 'defaults' => ['hello' => 'world']] - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); - $this->assertSimpleRoute('/apps/app1/{folderId}/open', 'GET', 'FoldersController', 'open', defaults: ['hello' => 'world'], route: $collection->get('ocs.app1.folders.open')); - } - - public function testParseOcsRoutesInvalidName(): void { - $routes = ['ocs' => [ - ['name' => 'folders', 'url' => '/{folderId}/open', 'verb' => 'GET'] - ]]; - - $this->expectException(\UnexpectedValueException::class); - $this->parser->parseOCSRoutes($routes, 'app1'); - } - - public function testParseOcsRoutesEmpty(): void { - $routes = ['ocs' => []]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertEquals(0, $collection->count()); - } - - public function testParseOcsRoutesWithResources(): void { - $routes = ['ocs' => [ - ['name' => 'folders#open', 'url' => '/{folderId}/open', 'verb' => 'GET'], - ], 'ocs-resources' => [ - 'names' => ['url' => '/names', 'root' => '/core/something'], - 'folder_names' => ['url' => '/folder/names'], - ]]; - - $collection = $this->parser->parseOCSRoutes($routes, 'app1'); - $this->assertArrayHasKey('ocs.app1.folders.open', $collection->all()); - $this->assertOcsResource('/apps/app1/folder/names', 'folder_names', 'FolderNamesController', 'app1', $collection); - $this->assertOcsResource('/core/something/names', 'names', 'NamesController', 'app1', $collection); - } - - protected function assertSimpleRoute( - string $path, - string $method, - string $controller, - string $action, - string $app = 'app1', - array $requirements = [], - array $defaults = [], - ?RoutingRoute $route = null, - ): void { - self::assertEquals($path, $route->getPath()); - self::assertEqualsCanonicalizing([$method], $route->getMethods()); - self::assertEqualsCanonicalizing($requirements, $route->getRequirements()); - self::assertEquals([...$defaults, 'action' => null, 'caller' => [$app, $controller, $action]], $route->getDefaults()); - } - - protected function assertSimpleResource( - string $path, - string $resourceName, - string $controller, - string $app, - RouteCollection $collection, - ): void { - self::assertArrayHasKey("$app.$resourceName.index", $collection->all()); - self::assertArrayHasKey("$app.$resourceName.show", $collection->all()); - self::assertArrayHasKey("$app.$resourceName.create", $collection->all()); - self::assertArrayHasKey("$app.$resourceName.update", $collection->all()); - self::assertArrayHasKey("$app.$resourceName.destroy", $collection->all()); - - $this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("$app.$resourceName.index")); - $this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("$app.$resourceName.create")); - $this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("$app.$resourceName.show")); - $this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("$app.$resourceName.update")); - $this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("$app.$resourceName.destroy")); - } - - protected function assertOcsResource( - string $path, - string $resourceName, - string $controller, - string $app, - RouteCollection $collection, - ): void { - self::assertArrayHasKey("ocs.$app.$resourceName.index", $collection->all()); - self::assertArrayHasKey("ocs.$app.$resourceName.show", $collection->all()); - self::assertArrayHasKey("ocs.$app.$resourceName.create", $collection->all()); - self::assertArrayHasKey("ocs.$app.$resourceName.update", $collection->all()); - self::assertArrayHasKey("ocs.$app.$resourceName.destroy", $collection->all()); - - $this->assertSimpleRoute($path, 'GET', $controller, 'index', $app, route: $collection->get("ocs.$app.$resourceName.index")); - $this->assertSimpleRoute($path, 'POST', $controller, 'create', $app, route: $collection->get("ocs.$app.$resourceName.create")); - $this->assertSimpleRoute("$path/{id}", 'GET', $controller, 'show', $app, route: $collection->get("ocs.$app.$resourceName.show")); - $this->assertSimpleRoute("$path/{id}", 'PUT', $controller, 'update', $app, route: $collection->get("ocs.$app.$resourceName.update")); - $this->assertSimpleRoute("$path/{id}", 'DELETE', $controller, 'destroy', $app, route: $collection->get("ocs.$app.$resourceName.destroy")); - } -} --- a/tests/lib/AppFramework/Services/AppConfigTest.php +++ /dev/null @@ -1,667 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->appConfigCore = $this->createMock(AppConfigCore::class); - - $this->appConfig = new AppConfig($this->config, $this->appConfigCore, self::TEST_APPID); - } - - public function testGetAppKeys(): void { - $expected = ['key1', 'key2', 'key3', 'key4', 'key5', 'key6', 'key7', 'test8']; - $this->appConfigCore->expects($this->once()) - ->method('getKeys') - ->with(self::TEST_APPID) - ->willReturn($expected); - $this->assertSame($expected, $this->appConfig->getAppKeys()); - } - - /** - * @return array - * @see testHasAppKey - */ - public static function providerHasAppKey(): array { - return [ - // lazy, expected - [false, true], - [true, true], - [false, false], - [true, false], - ]; - } - - /** - * - * @param bool $lazy - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerHasAppKey')] - public function testHasAppKey(bool $lazy, bool $expected): void { - $key = 'key'; - $this->appConfigCore->expects($this->once()) - ->method('hasKey') - ->with(self::TEST_APPID, $key, $lazy) - ->willReturn($expected); - $this->assertSame($expected, $this->appConfig->hasAppKey($key, $lazy)); - } - - /** - * @return array - * @see testIsSensitive - */ - public static function providerIsSensitive(): array { - return [ - // lazy, expected - [false, true], - [true, true], - [false, false], - [true, false], - ]; - } - - /** - * - * @param bool $lazy - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerIsSensitive')] - public function testIsSensitive(bool $lazy, bool $expected): void { - $key = 'key'; - $this->appConfigCore->expects($this->once()) - ->method('isSensitive') - ->with(self::TEST_APPID, $key, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->isSensitive($key, $lazy)); - } - - /** - * - * @param bool $lazy - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerIsSensitive')] - public function testIsSensitiveException(bool $lazy, bool $expected): void { - $key = 'unknown-key'; - $this->appConfigCore->expects($this->once()) - ->method('isSensitive') - ->with(self::TEST_APPID, $key, $lazy) - ->willThrowException(new AppConfigUnknownKeyException()); - - $this->expectException(AppConfigUnknownKeyException::class); - $this->appConfig->isSensitive($key, $lazy); - } - - /** - * @return array - * @see testIsLazy - */ - public static function providerIsLazy(): array { - return [ - // expected - [true], - [false], - ]; - } - - /** - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerIsLazy')] - public function testIsLazy(bool $expected): void { - $key = 'key'; - $this->appConfigCore->expects($this->once()) - ->method('isLazy') - ->with(self::TEST_APPID, $key) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->isLazy($key)); - } - - public function testIsLazyException(): void { - $key = 'unknown-key'; - $this->appConfigCore->expects($this->once()) - ->method('isLazy') - ->with(self::TEST_APPID, $key) - ->willThrowException(new AppConfigUnknownKeyException()); - - $this->expectException(AppConfigUnknownKeyException::class); - $this->appConfig->isLazy($key); - } - - /** - * @return array - * @see testGetAllAppValues - */ - public static function providerGetAllAppValues(): array { - return [ - // key, filtered - ['', false], - ['', true], - ['key', false], - ['key', true], - ]; - } - - /** - * - * @param string $key - * @param bool $filtered - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAllAppValues')] - public function testGetAllAppValues(string $key, bool $filtered): void { - $expected = [ - 'key1' => 'value1', - 'key2' => 3, - 'key3' => 3.14, - 'key4' => true - ]; - - $this->appConfigCore->expects($this->once()) - ->method('getAllValues') - ->with(self::TEST_APPID, $key, $filtered) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAllAppValues($key, $filtered)); - } - - public function testSetAppValue(): void { - $key = 'key'; - $value = 'value'; - $this->appConfigCore->expects($this->once()) - ->method('setValueMixed') - ->with(self::TEST_APPID, $key, $value); - - $this->appConfig->setAppValue($key, $value); - } - - /** - * @return array - * @see testSetAppValueString - * @see testSetAppValueStringException - * @see testSetAppValueInt - * @see testSetAppValueIntException - * @see testSetAppValueFloat - * @see testSetAppValueFloatException - * @see testSetAppValueArray - * @see testSetAppValueArrayException - */ - public static function providerSetAppValue(): array { - return [ - // lazy, sensitive, expected - [false, false, true], - [false, true, true], - [true, true, true], - [true, false, true], - [false, false, false], - [false, true, false], - [true, true, false], - [true, false, false], - ]; - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueString(bool $lazy, bool $sensitive, bool $expected): void { - $key = 'key'; - $value = 'valueString'; - $this->appConfigCore->expects($this->once()) - ->method('setValueString') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->setAppValueString($key, $value, $lazy, $sensitive)); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueStringException(bool $lazy, bool $sensitive): void { - $key = 'key'; - $value = 'valueString'; - $this->appConfigCore->expects($this->once()) - ->method('setValueString') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setAppValueString($key, $value, $lazy, $sensitive); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueInt(bool $lazy, bool $sensitive, bool $expected): void { - $key = 'key'; - $value = 42; - $this->appConfigCore->expects($this->once()) - ->method('setValueInt') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->setAppValueInt($key, $value, $lazy, $sensitive)); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueIntException(bool $lazy, bool $sensitive): void { - $key = 'key'; - $value = 42; - $this->appConfigCore->expects($this->once()) - ->method('setValueInt') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setAppValueInt($key, $value, $lazy, $sensitive); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueFloat(bool $lazy, bool $sensitive, bool $expected): void { - $key = 'key'; - $value = 3.14; - $this->appConfigCore->expects($this->once()) - ->method('setValueFloat') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->setAppValueFloat($key, $value, $lazy, $sensitive)); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueFloatException(bool $lazy, bool $sensitive): void { - $key = 'key'; - $value = 3.14; - $this->appConfigCore->expects($this->once()) - ->method('setValueFloat') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setAppValueFloat($key, $value, $lazy, $sensitive); - } - - /** - * @return array - * @see testSetAppValueBool - */ - public static function providerSetAppValueBool(): array { - return [ - // lazy, expected - [false, true], - [false, false], - [true, true], - [true, false], - ]; - } - - /** - * - * @param bool $lazy - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValueBool')] - public function testSetAppValueBool(bool $lazy, bool $expected): void { - $key = 'key'; - $value = true; - $this->appConfigCore->expects($this->once()) - ->method('setValueBool') - ->with(self::TEST_APPID, $key, $value, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->setAppValueBool($key, $value, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValueBool')] - public function testSetAppValueBoolException(bool $lazy): void { - $key = 'key'; - $value = true; - $this->appConfigCore->expects($this->once()) - ->method('setValueBool') - ->with(self::TEST_APPID, $key, $value, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setAppValueBool($key, $value, $lazy); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueArray(bool $lazy, bool $sensitive, bool $expected): void { - $key = 'key'; - $value = ['item' => true]; - $this->appConfigCore->expects($this->once()) - ->method('setValueArray') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->setAppValueArray($key, $value, $lazy, $sensitive)); - } - - /** - * - * @param bool $lazy - * @param bool $sensitive - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetAppValue')] - public function testSetAppValueArrayException(bool $lazy, bool $sensitive): void { - $key = 'key'; - $value = ['item' => true]; - $this->appConfigCore->expects($this->once()) - ->method('setValueArray') - ->with(self::TEST_APPID, $key, $value, $lazy, $sensitive) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setAppValueArray($key, $value, $lazy, $sensitive); - } - - public function testGetAppValue(): void { - $key = 'key'; - $value = 'value'; - $default = 'default'; - $this->appConfigCore->expects($this->once()) - ->method('getValueMixed') - ->with(self::TEST_APPID, $key, $default) - ->willReturn($value); - - $this->assertSame($value, $this->appConfig->getAppValue($key, $default)); - } - - public function testGetAppValueDefault(): void { - $key = 'key'; - $default = 'default'; - $this->appConfigCore->expects($this->once()) - ->method('getValueMixed') - ->with(self::TEST_APPID, $key, $default) - ->willReturn($default); - - $this->assertSame($default, $this->appConfig->getAppValue($key, $default)); - } - - /** - * @return array - * @see testGetAppValueString - * @see testGetAppValueStringException - * @see testGetAppValueInt - * @see testGetAppValueIntException - * @see testGetAppValueFloat - * @see testGetAppValueFloatException - * @see testGetAppValueBool - * @see testGetAppValueBoolException - * @see testGetAppValueArray - * @see testGetAppValueArrayException - */ - public static function providerGetAppValue(): array { - return [ - // lazy, exist - [false, false], - [false, true], - [true, true], - [true, false] - ]; - } - - /** - * - * @param bool $lazy - * @param bool $exist - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueString(bool $lazy, bool $exist): void { - $key = 'key'; - $value = 'valueString'; - $default = 'default'; - - $expected = ($exist) ? $value : $default; - $this->appConfigCore->expects($this->once()) - ->method('getValueString') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAppValueString($key, $default, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueStringException(bool $lazy): void { - $key = 'key'; - $default = 'default'; - - $this->appConfigCore->expects($this->once()) - ->method('getValueString') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getAppValueString($key, $default, $lazy); - } - - /** - * - * @param bool $lazy - * @param bool $exist - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueInt(bool $lazy, bool $exist): void { - $key = 'key'; - $value = 42; - $default = 17; - - $expected = ($exist) ? $value : $default; - $this->appConfigCore->expects($this->once()) - ->method('getValueInt') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAppValueInt($key, $default, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueIntException(bool $lazy): void { - $key = 'key'; - $default = 17; - - $this->appConfigCore->expects($this->once()) - ->method('getValueInt') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getAppValueInt($key, $default, $lazy); - } - - /** - * - * @param bool $lazy - * @param bool $exist - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueFloat(bool $lazy, bool $exist): void { - $key = 'key'; - $value = 3.14; - $default = 17.04; - - $expected = ($exist) ? $value : $default; - $this->appConfigCore->expects($this->once()) - ->method('getValueFloat') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAppValueFloat($key, $default, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueFloatException(bool $lazy): void { - $key = 'key'; - $default = 17.04; - - $this->appConfigCore->expects($this->once()) - ->method('getValueFloat') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getAppValueFloat($key, $default, $lazy); - } - - /** - * - * @param bool $lazy - * @param bool $exist - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueBool(bool $lazy, bool $exist): void { - $key = 'key'; - $value = true; - $default = false; - - $expected = ($exist) ? $value : $default; // yes, it can be simplified - $this->appConfigCore->expects($this->once()) - ->method('getValueBool') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAppValueBool($key, $default, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueBoolException(bool $lazy): void { - $key = 'key'; - $default = false; - - $this->appConfigCore->expects($this->once()) - ->method('getValueBool') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getAppValueBool($key, $default, $lazy); - } - - /** - * - * @param bool $lazy - * @param bool $exist - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueArray(bool $lazy, bool $exist): void { - $key = 'key'; - $value = ['item' => true]; - $default = []; - - $expected = ($exist) ? $value : $default; - $this->appConfigCore->expects($this->once()) - ->method('getValueArray') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willReturn($expected); - - $this->assertSame($expected, $this->appConfig->getAppValueArray($key, $default, $lazy)); - } - - /** - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppValue')] - public function testGetAppValueArrayException(bool $lazy): void { - $key = 'key'; - $default = []; - - $this->appConfigCore->expects($this->once()) - ->method('getValueArray') - ->with(self::TEST_APPID, $key, $default, $lazy) - ->willThrowException(new AppConfigTypeConflictException()); - - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getAppValueArray($key, $default, $lazy); - } - - public function testDeleteAppValue(): void { - $key = 'key'; - $this->appConfigCore->expects($this->once()) - ->method('deleteKey') - ->with(self::TEST_APPID, $key); - - $this->appConfig->deleteAppValue($key); - } - - public function testDeleteAppValues(): void { - $this->appConfigCore->expects($this->once()) - ->method('deleteApp') - ->with(self::TEST_APPID); - - $this->appConfig->deleteAppValues(); - } -} --- a/tests/lib/AppFramework/Utility/ControllerMethodReflectorTest.php +++ /dev/null @@ -1,395 +0,0 @@ - $rangedOne - * @psalm-param int $rangedTwo - * @psalm-param int<1, 6>|null $rangedThree - * @psalm-param ?int<-70, -30> $rangedFour - * @return void - */ - public function test4(int $rangedOne, int $rangedTwo, ?int $rangedThree, ?int $rangedFour) { - } - - /** - * @param int<-4, 42> $rangedOne - * @param int $rangedTwo - * @param int<1, 6>|null $rangedThree - * @param ?int<-70, -30> $rangedFour - * @return void - */ - public function test5(int $rangedOne, int $rangedTwo, ?int $rangedThree, ?int $rangedFour) { - } - - /** - * @psalm-param positive-int $positive - * @psalm-param non-negative-int $nonNegative - * @psalm-param negative-int $negative - * @psalm-param non-positive-int $nonPositive - * @psalm-param positive-int|null $positiveOrNull - * @psalm-param ?non-negative-int $nonNegativeOrNull - * @return void - */ - public function test6(int $positive, int $nonNegative, int $negative, int $nonPositive, ?int $positiveOrNull, ?int $nonNegativeOrNull) { - } - - /** - * @psalm-param non-empty-string $nonEmpty - * @psalm-param non-empty-lowercase-string $nonEmptyLowercase - * @psalm-param lowercase-string $lowercase - * @psalm-param non-falsy-string $nonFalsy - * @psalm-param numeric-string $numeric - * @psalm-param non-empty-string|null $nonEmptyOrNull - * @psalm-param ?non-empty-string $nonEmptyOrNullPrefix - * @psalm-param string $plain - * @return void - */ - public function test7( - string $nonEmpty, - string $nonEmptyLowercase, - string $lowercase, - string $nonFalsy, - string $numeric, - ?string $nonEmptyOrNull, - ?string $nonEmptyOrNullPrefix, - string $plain, - ) { - } -} - -class EndController extends MiddleController { -} - -class ControllerMethodReflectorTest extends \Test\TestCase { - /** - * @Annotation - */ - public function testReadAnnotation(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testReadAnnotation' - ); - - $this->assertTrue($reader->hasAnnotation('Annotation')); - } - - /** - * @Annotation(parameter=value) - */ - public function testGetAnnotationParameterSingle(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - self::class, - __FUNCTION__ - ); - - $this->assertSame('value', $reader->getAnnotationParameter('Annotation', 'parameter')); - } - - /** - * @Annotation(parameter1=value1, parameter2=value2,parameter3=value3) - */ - public function testGetAnnotationParameterMultiple(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - self::class, - __FUNCTION__ - ); - - $this->assertSame('value1', $reader->getAnnotationParameter('Annotation', 'parameter1')); - $this->assertSame('value2', $reader->getAnnotationParameter('Annotation', 'parameter2')); - $this->assertSame('value3', $reader->getAnnotationParameter('Annotation', 'parameter3')); - } - - /** - * @Annotation - * @param test - */ - public function testReadAnnotationNoLowercase(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testReadAnnotationNoLowercase' - ); - - $this->assertTrue($reader->hasAnnotation('Annotation')); - $this->assertFalse($reader->hasAnnotation('param')); - } - - /** - * @Annotation - * @param int $test - */ - public function testReadTypeIntAnnotations(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testReadTypeIntAnnotations' - ); - - $this->assertEquals('int', $reader->getType('test')); - } - - /** - * @Annotation - * @param int $a - * @param int $b - */ - public function arguments3($a, float $b, int $c, $d) { - } - - public function testReadTypeIntAnnotationsScalarTypes(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'arguments3' - ); - - $this->assertEquals('int', $reader->getType('a')); - $this->assertEquals('float', $reader->getType('b')); - $this->assertEquals('int', $reader->getType('c')); - $this->assertNull($reader->getType('d')); - } - - /** - * @Annotation - * @param double $test something special - */ - public function testReadTypeDoubleAnnotations(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testReadTypeDoubleAnnotations' - ); - - $this->assertEquals('double', $reader->getType('test')); - } - - /** - * @Annotation - * @param string $foo - */ - public function testReadTypeWhitespaceAnnotations(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'testReadTypeWhitespaceAnnotations' - ); - - $this->assertEquals('string', $reader->getType('foo')); - } - - public function arguments($arg, $arg2 = 'hi') { - } - public function testReflectParameters(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'arguments' - ); - - $this->assertEquals(['arg' => null, 'arg2' => 'hi'], $reader->getParameters()); - } - - public function arguments2($arg) { - } - public function testReflectParameters2(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect( - '\Test\AppFramework\Utility\ControllerMethodReflectorTest', - 'arguments2' - ); - - $this->assertEquals(['arg' => null], $reader->getParameters()); - } - - public function testInheritance(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test'); - - $this->assertTrue($reader->hasAnnotation('Annotation')); - } - - public function testInheritanceOverride(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test2'); - - $this->assertTrue($reader->hasAnnotation('NoAnnotation')); - $this->assertFalse($reader->hasAnnotation('Annotation')); - } - - public function testInheritanceOverrideNoDocblock(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test3'); - - $this->assertFalse($reader->hasAnnotation('Annotation')); - } - - public function testRangeDetectionPsalm(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test4'); - - $rangeInfo1 = $reader->getRange('rangedOne'); - $this->assertSame(-4, $rangeInfo1['min']); - $this->assertSame(42, $rangeInfo1['max']); - - $rangeInfo2 = $reader->getRange('rangedTwo'); - $this->assertSame(PHP_INT_MIN, $rangeInfo2['min']); - $this->assertSame(PHP_INT_MAX, $rangeInfo2['max']); - - $rangeInfo3 = $reader->getRange('rangedThree'); - $this->assertSame(1, $rangeInfo3['min']); - $this->assertSame(6, $rangeInfo3['max']); - - $rangeInfo3 = $reader->getRange('rangedFour'); - $this->assertSame(-70, $rangeInfo3['min']); - $this->assertSame(-30, $rangeInfo3['max']); - } - - public function testRangeDetectionNative(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test5'); - - $rangeInfo1 = $reader->getRange('rangedOne'); - $this->assertSame(-4, $rangeInfo1['min']); - $this->assertSame(42, $rangeInfo1['max']); - - $rangeInfo2 = $reader->getRange('rangedTwo'); - $this->assertSame(PHP_INT_MIN, $rangeInfo2['min']); - $this->assertSame(PHP_INT_MAX, $rangeInfo2['max']); - - $rangeInfo3 = $reader->getRange('rangedThree'); - $this->assertSame(1, $rangeInfo3['min']); - $this->assertSame(6, $rangeInfo3['max']); - - $rangeInfo3 = $reader->getRange('rangedFour'); - $this->assertSame(-70, $rangeInfo3['min']); - $this->assertSame(-30, $rangeInfo3['max']); - } - - public function testRangeDetectionIntAliases(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test6'); - - $positive = $reader->getRange('positive'); - $this->assertSame(1, $positive['min']); - $this->assertSame(PHP_INT_MAX, $positive['max']); - - $nonNegative = $reader->getRange('nonNegative'); - $this->assertSame(0, $nonNegative['min']); - $this->assertSame(PHP_INT_MAX, $nonNegative['max']); - - $negative = $reader->getRange('negative'); - $this->assertSame(PHP_INT_MIN, $negative['min']); - $this->assertSame(-1, $negative['max']); - - $nonPositive = $reader->getRange('nonPositive'); - $this->assertSame(PHP_INT_MIN, $nonPositive['min']); - $this->assertSame(0, $nonPositive['max']); - - $positiveOrNull = $reader->getRange('positiveOrNull'); - $this->assertSame(1, $positiveOrNull['min']); - $this->assertSame(PHP_INT_MAX, $positiveOrNull['max']); - - $nonNegativeOrNull = $reader->getRange('nonNegativeOrNull'); - $this->assertSame(0, $nonNegativeOrNull['min']); - $this->assertSame(PHP_INT_MAX, $nonNegativeOrNull['max']); - } - - public function testStringConstraintDetection(): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test7'); - - $this->assertSame('non-empty-string', $reader->getStringConstraint('nonEmpty')); - $this->assertSame('non-empty-lowercase-string', $reader->getStringConstraint('nonEmptyLowercase')); - $this->assertSame('lowercase-string', $reader->getStringConstraint('lowercase')); - $this->assertSame('non-falsy-string', $reader->getStringConstraint('nonFalsy')); - $this->assertSame('numeric-string', $reader->getStringConstraint('numeric')); - $this->assertSame('non-empty-string', $reader->getStringConstraint('nonEmptyOrNull')); - $this->assertSame('non-empty-string', $reader->getStringConstraint('nonEmptyOrNullPrefix')); - $this->assertNull($reader->getStringConstraint('plain')); - } - - public static function stringConstraintDataProvider(): array { - return [ - ['non-empty-string', '', false], - ['non-empty-string', 'a', true], - ['non-empty-lowercase-string', '', false], - ['non-empty-lowercase-string', 'ABC', false], - ['non-empty-lowercase-string', 'abc', true], - ['lowercase-string', '', true], - ['lowercase-string', 'ABC', false], - ['lowercase-string', 'abc', true], - ['non-falsy-string', '', false], - ['non-falsy-string', '0', false], - ['non-falsy-string', '0.0', true], - ['non-falsy-string', 'a', true], - ['numeric-string', 'abc', false], - ['numeric-string', '42', true], - ['numeric-string', '4.2', true], - [null, '', true], - [null, 'anything', true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('stringConstraintDataProvider')] - public function testSatisfiesStringConstraint(?string $constraint, string $value, bool $expected): void { - $reader = new ControllerMethodReflector(Server::get(LoggerInterface::class)); - $reader->reflect('Test\AppFramework\Utility\EndController', 'test7'); - - $parameter = match ($constraint) { - 'non-empty-string' => 'nonEmpty', - 'non-empty-lowercase-string' => 'nonEmptyLowercase', - 'lowercase-string' => 'lowercase', - 'non-falsy-string' => 'nonFalsy', - 'numeric-string' => 'numeric', - default => 'plain', - }; - - $this->assertSame($expected, $reader->satisfiesStringConstraint($parameter, $value)); - } -} --- a/tests/lib/AppFramework/Utility/SimpleContainerTest.php +++ /dev/null @@ -1,250 +0,0 @@ -container = new SimpleContainer(); - } - - public function testRegister(): void { - $this->container->registerParameter('test', 'abc'); - $this->assertEquals('abc', $this->container->query('test')); - } - - /** - * Test querying a class that is not registered without autoload enabled - */ - public function testNothingRegistered(): void { - try { - $this->container->query('something really hard', false); - $this->fail('Expected `QueryException` exception was not thrown'); - } catch (\Throwable $exception) { - $this->assertInstanceOf(QueryException::class, $exception); - $this->assertInstanceOf(NotFoundExceptionInterface::class, $exception); - } - } - - /** - * Test querying a class that is not registered with autoload enabled - */ - public function testNothingRegistered_autoload(): void { - try { - $this->container->query('something really hard'); - $this->fail('Expected `QueryException` exception was not thrown'); - } catch (\Throwable $exception) { - $this->assertInstanceOf(QueryException::class, $exception); - $this->assertInstanceOf(NotFoundExceptionInterface::class, $exception); - } - } - - public function testNotAClass(): void { - $this->expectException(QueryException::class); - - $this->container->query('Test\AppFramework\Utility\TestInterface'); - } - - public function testNoConstructorClass(): void { - $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); - $this->assertTrue($object instanceof ClassEmptyConstructor); - } - - public function testInstancesOnlyOnce(): void { - $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); - $object2 = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); - $this->assertSame($object, $object2); - } - - public function testConstructorSimple(): void { - $this->container->registerParameter('test', 'abc'); - $object = $this->container->query( - 'Test\AppFramework\Utility\ClassSimpleConstructor' - ); - $this->assertTrue($object instanceof ClassSimpleConstructor); - $this->assertEquals('abc', $object->test); - } - - public function testConstructorComplex(): void { - $this->container->registerParameter('test', 'abc'); - $object = $this->container->query( - 'Test\AppFramework\Utility\ClassComplexConstructor' - ); - $this->assertTrue($object instanceof ClassComplexConstructor); - $this->assertEquals('abc', $object->class->test); - $this->assertEquals('abc', $object->test); - } - - public function testConstructorComplexInterface(): void { - $this->container->registerParameter('test', 'abc'); - $this->container->registerService( - 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { - return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); - }); - $object = $this->container->query( - 'Test\AppFramework\Utility\ClassInterfaceConstructor' - ); - $this->assertTrue($object instanceof ClassInterfaceConstructor); - $this->assertEquals('abc', $object->class->test); - $this->assertEquals('abc', $object->test); - } - - public function testOverrideService(): void { - $this->container->registerService( - 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { - return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); - }); - $this->container->registerService( - 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { - return $c->query('Test\AppFramework\Utility\ClassEmptyConstructor'); - }); - $object = $this->container->query( - 'Test\AppFramework\Utility\IInterfaceConstructor' - ); - $this->assertTrue($object instanceof ClassEmptyConstructor); - } - - public function testRegisterAliasParamter(): void { - $this->container->registerParameter('test', 'abc'); - $this->container->registerAlias('test1', 'test'); - $this->assertEquals('abc', $this->container->query('test1')); - } - - public function testRegisterAliasService(): void { - $this->container->registerService('test', function () { - return new \StdClass; - }, true); - $this->container->registerAlias('test1', 'test'); - $this->assertSame( - $this->container->query('test'), $this->container->query('test')); - $this->assertSame( - $this->container->query('test1'), $this->container->query('test1')); - $this->assertSame( - $this->container->query('test'), $this->container->query('test1')); - } - - public static function sanitizeNameProvider(): array { - return [ - ['ABC\\Foo', 'ABC\\Foo'], - ['\\ABC\\Foo', '\\ABC\\Foo'], - ['\\ABC\\Foo', 'ABC\\Foo'], - ['ABC\\Foo', '\\ABC\\Foo'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('sanitizeNameProvider')] - public function testSanitizeName($register, $query): void { - $this->container->registerService($register, function () { - return 'abc'; - }); - $this->assertEquals('abc', $this->container->query($query)); - } - - public function testConstructorComplexNoTestParameterFound(): void { - $this->expectException(QueryException::class); - - $object = $this->container->query( - 'Test\AppFramework\Utility\ClassComplexConstructor' - ); - /* Use the object to trigger DI on PHP >= 8.4 */ - get_object_vars($object); - } - - public function testRegisterFactory(): void { - $this->container->registerService('test', function () { - return new \StdClass(); - }, false); - $this->assertNotSame( - $this->container->query('test'), $this->container->query('test')); - } - - public function testRegisterAliasFactory(): void { - $this->container->registerService('test', function () { - return new \StdClass(); - }, false); - $this->container->registerAlias('test1', 'test'); - $this->assertNotSame( - $this->container->query('test'), $this->container->query('test')); - $this->assertNotSame( - $this->container->query('test1'), $this->container->query('test1')); - $this->assertNotSame( - $this->container->query('test'), $this->container->query('test1')); - } - - public function testQueryUntypedNullable(): void { - $this->expectException(QueryException::class); - - $object = $this->container->query( - ClassNullableUntypedConstructorArg::class - ); - /* Use the object to trigger DI on PHP >= 8.4 */ - get_object_vars($object); - } - - public function testQueryTypedNullable(): void { - /** @var ClassNullableTypedConstructorArg $service */ - $service = $this->container->query(ClassNullableTypedConstructorArg::class); - - self::assertNull($service->class); - } -} --- a/tests/lib/AppFramework/Utility/TimeFactoryTest.php +++ /dev/null @@ -1,53 +0,0 @@ -timeFactory = new TimeFactory(); - } - - public function testNow(): void { - $now = $this->timeFactory->now(); - self::assertSame('UTC', $now->getTimezone()->getName()); - } - - public function testNowWithTimeZone(): void { - $timezone = new \DateTimeZone('Europe/Berlin'); - $withTimeZone = $this->timeFactory->withTimeZone($timezone); - - $now = $withTimeZone->now(); - self::assertSame('Europe/Berlin', $now->getTimezone()->getName()); - } - - public function testGetTimeZone(): void { - $expected = new \DateTimeZone('Europe/Berlin'); - $actual = $this->timeFactory->getTimeZone('Europe/Berlin'); - self::assertEquals($expected, $actual); - } - - public function testGetTimeZoneUTC(): void { - $expected = new \DateTimeZone('UTC'); - $actual = $this->timeFactory->getTimeZone(); - self::assertEquals($expected, $actual); - } - - public function testGetTimeZoneInvalid(): void { - $this->expectException(\Exception::class); - $this->timeFactory->getTimeZone('blubblub'); - } -} --- a/tests/lib/AppScriptSortTest.php +++ /dev/null @@ -1,125 +0,0 @@ -logger = $this->getMockBuilder(LoggerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - parent::setUp(); - } - - public function testSort(): void { - $scripts = [ - 'first' => ['myFirstJSFile'], - 'core' => [ - 'core/js/myFancyJSFile1', - 'core/js/myFancyJSFile4', - 'core/js/myFancyJSFile5', - 'core/js/myFancyJSFile1', - ], - 'files' => ['files/js/myFancyJSFile2'], - 'myApp5' => ['myApp5/js/myApp5JSFile'], - 'myApp' => ['myApp/js/myFancyJSFile3'], - 'myApp4' => ['myApp4/js/myApp4JSFile'], - 'myApp3' => ['myApp3/js/myApp3JSFile'], - 'myApp2' => ['myApp2/js/myApp2JSFile'], - ]; - $scriptDeps = [ - 'first' => new AppScriptDependency('first', ['core']), - 'core' => new AppScriptDependency('core', ['core']), - 'files' => new AppScriptDependency('files', ['core']), - 'myApp5' => new AppScriptDependency('myApp5', ['myApp2']), - 'myApp' => new AppScriptDependency('myApp', ['core']), - 'myApp4' => new AppScriptDependency('myApp4', ['myApp3']), - 'myApp3' => new AppScriptDependency('myApp3', ['myApp2']), - 'myApp2' => new AppScriptDependency('myApp2', ['myApp']), - ]; - - // No circular dependency is detected and logged as an error - $this->logger->expects(self::never())->method('error'); - - $scriptSort = new AppScriptSort($this->logger); - $sortedScripts = $scriptSort->sort($scripts, $scriptDeps); - - $sortedScriptKeys = array_keys($sortedScripts); - - // Core should appear first - $this->assertEquals( - 0, - array_search('core', $sortedScriptKeys, true) - ); - - // Dependencies should appear before their children - $this->assertLessThan( - array_search('files', $sortedScriptKeys, true), - array_search('core', $sortedScriptKeys, true) - ); - $this->assertLessThan( - array_search('myApp2', $sortedScriptKeys, true), - array_search('myApp', $sortedScriptKeys, true) - ); - $this->assertLessThan( - array_search('myApp3', $sortedScriptKeys, true), - array_search('myApp2', $sortedScriptKeys, true) - ); - $this->assertLessThan( - array_search('myApp4', $sortedScriptKeys, true), - array_search('myApp3', $sortedScriptKeys, true) - ); - $this->assertLessThan( - array_search('myApp5', $sortedScriptKeys, true), - array_search('myApp2', $sortedScriptKeys, true) - ); - - // All apps still there - foreach ($scripts as $app => $_) { - $this->assertContains($app, $sortedScriptKeys); - } - } - - public function testSortCircularDependency(): void { - $scripts = [ - 'circular' => ['circular/js/file1'], - 'dependency' => ['dependency/js/file2'], - ]; - $scriptDeps = [ - 'circular' => new AppScriptDependency('circular', ['dependency']), - 'dependency' => new AppScriptDependency('dependency', ['circular']), - ]; - - // A circular dependency is detected and logged as an error - $this->logger->expects(self::once())->method('error'); - - $scriptSort = new AppScriptSort($this->logger); - $sortedScripts = $scriptSort->sort($scripts, $scriptDeps); - - $sortedScriptKeys = array_keys($sortedScripts); - - // All apps still there - foreach ($scripts as $app => $_) { - $this->assertContains($app, $sortedScriptKeys); - } - } -} --- a/tests/lib/AppTest.php +++ /dev/null @@ -1,333 +0,0 @@ -createUser(self::TEST_USER1, 'NotAnEasyPassword123456+'); - self::$user2 = $userManager->createUser(self::TEST_USER2, 'NotAnEasyPassword123456_'); - self::$user3 = $userManager->createUser(self::TEST_USER3, 'NotAnEasyPassword123456?'); - - self::$group1 = $groupManager->createGroup(self::TEST_GROUP1); - self::$group1->addUser(self::$user1); - self::$group1->addUser(self::$user3); - self::$group2 = $groupManager->createGroup(self::TEST_GROUP2); - self::$group2->addUser(self::$user2); - self::$group2->addUser(self::$user3); - } - - public static function tearDownAfterClass(): void { - self::$user1->delete(); - self::$user2->delete(); - self::$user3->delete(); - - self::$group1->delete(); - self::$group2->delete(); - } - - /** - * Tests that the app order is correct - */ - public function testGetEnabledAppsIsSorted(): void { - $apps = \OC_App::getEnabledApps(); - // copy array - $sortedApps = $apps; - sort($sortedApps); - // 'files' is always on top - unset($sortedApps[array_search('files', $sortedApps)]); - array_unshift($sortedApps, 'files'); - $this->assertEquals($sortedApps, $apps); - } - - /** - * Providers for the app config values - */ - public static function appConfigValuesProvider(): array { - return [ - // logged in user1 - [ - self::TEST_USER1, - [ - 'files', - 'app1', - 'app3', - 'appforgroup1', - 'appforgroup12', - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ], - false - ], - // logged in user2 - [ - self::TEST_USER2, - [ - 'files', - 'app1', - 'app3', - 'appforgroup12', - 'appforgroup2', - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ], - false - ], - // logged in user3 - [ - self::TEST_USER3, - [ - 'files', - 'app1', - 'app3', - 'appforgroup1', - 'appforgroup12', - 'appforgroup2', - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ], - false - ], - // no user, returns all apps - [ - null, - [ - 'files', - 'app1', - 'app3', - 'appforgroup1', - 'appforgroup12', - 'appforgroup2', - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ], - false, - ], - // user given, but ask for all - [ - self::TEST_USER1, - [ - 'files', - 'app1', - 'app3', - 'appforgroup1', - 'appforgroup12', - 'appforgroup2', - 'appstore', - 'cloud_federation_api', - 'dav', - 'federatedfilesharing', - 'lookup_server_connector', - 'oauth2', - 'profile', - 'provisioning_api', - 'settings', - 'sharing', - 'theming', - 'twofactor_backupcodes', - 'viewer', - 'workflowengine', - ], - true, - ], - ]; - } - - /** - * Test enabled apps - */ - #[\PHPUnit\Framework\Attributes\DataProvider('appConfigValuesProvider')] - public function testEnabledApps($userId, $expectedApps, $forceAll): void { - $userSession = Server::get(IUserSession::class); - - $user = match ($userId) { - self::TEST_USER1 => self::$user1, - self::TEST_USER2 => self::$user2, - self::TEST_USER3 => self::$user3, - default => null, - }; - - $userSession->setUser($user); - - $this->setupAppConfigMock()->expects($this->once()) - ->method('searchValues') - ->willReturn( - [ - 'app3' => 'yes', - 'app2' => 'no', - 'app1' => 'yes', - 'appforgroup1' => '["group1"]', - 'appforgroup2' => '["group2"]', - 'appforgroup12' => '["group2","group1"]', - ] - ); - - $apps = \OC_App::getEnabledApps(false, $forceAll); - - $this->restoreAppConfig(); - $userSession->setUser(null); - - $this->assertEquals($expectedApps, $apps); - } - - /** - * Test isEnabledApps() with cache, not re-reading the list of - * enabled apps more than once when a user is set. - */ - public function testEnabledAppsCache(): void { - $userSession = Server::get(IUserSession::class); - $userSession->setUser(self::$user1); - - $this->setupAppConfigMock()->expects($this->once()) - ->method('searchValues') - ->willReturn( - [ - 'app3' => 'yes', - 'app2' => 'no', - ] - ); - - $apps = \OC_App::getEnabledApps(); - $this->assertEquals(['files', 'app3', 'appstore', 'cloud_federation_api', 'dav', 'federatedfilesharing', 'lookup_server_connector', 'oauth2', 'profile', 'provisioning_api', 'settings', 'sharing', 'theming', 'twofactor_backupcodes', 'viewer', 'workflowengine'], $apps); - - // mock should not be called again here - $apps = \OC_App::getEnabledApps(); - $this->assertEquals(['files', 'app3', 'appstore', 'cloud_federation_api', 'dav', 'federatedfilesharing', 'lookup_server_connector', 'oauth2', 'profile', 'provisioning_api', 'settings', 'sharing', 'theming', 'twofactor_backupcodes', 'viewer', 'workflowengine'], $apps); - - $this->restoreAppConfig(); - $userSession->setUser(null); - } - - private function setupAppConfigMock() { - $appConfig = $this->getMockBuilder(AppConfig::class) - ->onlyMethods(['searchValues']) - ->setConstructorArgs([Server::get(IDBConnection::class)]) - ->disableOriginalConstructor() - ->getMock(); - - $this->registerAppConfig($appConfig); - return $appConfig; - } - - /** - * Register an app config mock for testing purposes. - * - * @param IAppConfig $appConfig app config mock - */ - private function registerAppConfig(AppConfig $appConfig) { - $this->overwriteService(AppConfig::class, $appConfig); - $this->overwriteService(AppManager::class, new AppManager( - Server::get(IUserSession::class), - Server::get(IConfig::class), - Server::get(IGroupManager::class), - Server::get(ICacheFactory::class), - Server::get(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(ServerVersion::class), - Server::get(ConfigManager::class), - Server::get(DependencyAnalyzer::class), - )); - } - - /** - * Restore the original app config service. - */ - private function restoreAppConfig() { - $this->restoreService(AppConfig::class); - $this->restoreService(AppManager::class); - - // Remove the cache of the mocked apps list with a forceRefresh - \OC_App::getEnabledApps(); - } -} --- a/tests/lib/Archive/TARTest.php +++ /dev/null @@ -1,26 +0,0 @@ -getTemporaryFile('.tar.gz')); - } -} --- a/tests/lib/Archive/TestBase.php +++ /dev/null @@ -1,147 +0,0 @@ -instance = $this->getExisting(); - $allFiles = $this->instance->getFiles(); - $expected = ['lorem.txt','logo-wide.png','dir/', 'dir/lorem.txt']; - $this->assertEquals(4, count($allFiles), 'only found ' . count($allFiles) . ' out of 4 expected files'); - foreach ($expected as $file) { - $this->assertContains($file, $allFiles, 'cant find ' . $file . ' in archive'); - $this->assertTrue($this->instance->fileExists($file), 'file ' . $file . ' does not exist in archive'); - } - $this->assertFalse($this->instance->fileExists('non/existing/file')); - - $rootContent = $this->instance->getFolder(''); - $expected = ['lorem.txt','logo-wide.png', 'dir/']; - $this->assertEquals(3, count($rootContent)); - foreach ($expected as $file) { - $this->assertContains($file, $rootContent, 'cant find ' . $file . ' in archive'); - } - - $dirContent = $this->instance->getFolder('dir/'); - $expected = ['lorem.txt']; - $this->assertEquals(1, count($dirContent)); - foreach ($expected as $file) { - $this->assertContains($file, $dirContent, 'cant find ' . $file . ' in archive'); - } - } - - public function testContent(): void { - $this->instance = $this->getExisting(); - $dir = \OC::$SERVERROOT . '/tests/data'; - $textFile = $dir . '/lorem.txt'; - $this->assertEquals(file_get_contents($textFile), $this->instance->getFile('lorem.txt')); - - $tmpFile = Server::get(ITempManager::class)->getTemporaryFile('.txt'); - $this->instance->extractFile('lorem.txt', $tmpFile); - $this->assertEquals(file_get_contents($textFile), file_get_contents($tmpFile)); - } - - public function testWrite(): void { - $dir = \OC::$SERVERROOT . '/tests/data'; - $textFile = $dir . '/lorem.txt'; - $this->instance = $this->getNew(); - $this->assertEquals(0, count($this->instance->getFiles())); - $this->instance->addFile('lorem.txt', $textFile); - $this->assertEquals(1, count($this->instance->getFiles())); - $this->assertTrue($this->instance->fileExists('lorem.txt')); - $this->assertFalse($this->instance->fileExists('lorem.txt/')); - - $this->assertEquals(file_get_contents($textFile), $this->instance->getFile('lorem.txt')); - $this->instance->addFile('lorem.txt', 'foobar'); - $this->assertEquals('foobar', $this->instance->getFile('lorem.txt')); - } - - public function testReadStream(): void { - $dir = \OC::$SERVERROOT . '/tests/data'; - $this->instance = $this->getExisting(); - $fh = $this->instance->getStream('lorem.txt', 'r'); - $this->assertTrue((bool)$fh); - $content = fread($fh, $this->instance->filesize('lorem.txt')); - fclose($fh); - $this->assertEquals(file_get_contents($dir . '/lorem.txt'), $content); - } - public function testWriteStream(): void { - $dir = \OC::$SERVERROOT . '/tests/data'; - $this->instance = $this->getNew(); - $fh = $this->instance->getStream('lorem.txt', 'w'); - $source = fopen($dir . '/lorem.txt', 'r'); - $result = stream_copy_to_stream($source, $fh); - $this->assertNotFalse($result); - fclose($source); - fclose($fh); - $this->assertTrue($this->instance->fileExists('lorem.txt')); - $this->assertEquals(file_get_contents($dir . '/lorem.txt'), $this->instance->getFile('lorem.txt')); - } - public function testFolder(): void { - $this->instance = $this->getNew(); - $this->assertFalse($this->instance->fileExists('/test')); - $this->assertFalse($this->instance->fileExists('/test/')); - $this->instance->addFolder('/test'); - $this->assertTrue($this->instance->fileExists('/test')); - $this->assertTrue($this->instance->fileExists('/test/')); - $this->instance->remove('/test'); - $this->assertFalse($this->instance->fileExists('/test')); - $this->assertFalse($this->instance->fileExists('/test/')); - } - public function testExtract(): void { - $dir = \OC::$SERVERROOT . '/tests/data'; - $this->instance = $this->getExisting(); - $tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->instance->extract($tmpDir); - $this->assertEquals(true, file_exists($tmpDir . 'lorem.txt')); - $this->assertEquals(true, file_exists($tmpDir . 'dir/lorem.txt')); - $this->assertEquals(true, file_exists($tmpDir . 'logo-wide.png')); - $this->assertEquals(file_get_contents($dir . '/lorem.txt'), file_get_contents($tmpDir . 'lorem.txt')); - Files::rmdirr($tmpDir); - } - public function testMoveRemove(): void { - $dir = \OC::$SERVERROOT . '/tests/data'; - $textFile = $dir . '/lorem.txt'; - $this->instance = $this->getNew(); - $this->instance->addFile('lorem.txt', $textFile); - $this->assertFalse($this->instance->fileExists('target.txt')); - $this->instance->rename('lorem.txt', 'target.txt'); - $this->assertTrue($this->instance->fileExists('target.txt')); - $this->assertFalse($this->instance->fileExists('lorem.txt')); - $this->assertEquals(file_get_contents($textFile), $this->instance->getFile('target.txt')); - $this->instance->remove('target.txt'); - $this->assertFalse($this->instance->fileExists('target.txt')); - } - public function testRecursive(): void { - $dir = \OC::$SERVERROOT . '/tests/data/themes'; - $this->instance = $this->getNew(); - $this->instance->addRecursive('/dir', $dir); - $this->assertTrue($this->instance->fileExists('/dir/abc/apps/files/l10n/zz.json')); - } -} --- a/tests/lib/Archive/ZIPTest.php +++ /dev/null @@ -1,30 +0,0 @@ -getTempBaseDir() . '/newArchive.zip'; - if (file_exists($newZip)) { - unlink($newZip); - } - return new ZIP($newZip); - } -} --- a/tests/lib/Authentication/Events/RemoteWipeFinishedTest.php +++ /dev/null @@ -1,23 +0,0 @@ -createMock(IToken::class); - $event = new RemoteWipeFinished($token); - - $this->assertSame($token, $event->getToken()); - } -} --- a/tests/lib/Authentication/Events/RemoteWipeStartedTest.php +++ /dev/null @@ -1,23 +0,0 @@ -createMock(IToken::class); - $event = new RemoteWipeStarted($token); - - $this->assertSame($token, $event->getToken()); - } -} --- a/tests/lib/Authentication/Listeners/RemoteWipeActivityListenerTest.php +++ /dev/null @@ -1,138 +0,0 @@ -activityManager = $this->createMock(IActivityManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new RemoteWipeActivityListener( - $this->activityManager, - $this->logger - ); - } - - public function testHandleUnrelated(): void { - $event = new Event(); - - $this->listener->handle($event); - - $this->addToAssertionCount(1); - } - - public function testHandleRemoteWipeStarted(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $activityEvent = $this->createMock(IActivityEvent::class); - $this->activityManager->expects($this->once()) - ->method('generateEvent') - ->willReturn($activityEvent); - $activityEvent->expects($this->once()) - ->method('setApp') - ->with('core') - ->willReturnSelf(); - $activityEvent->expects($this->once()) - ->method('setType') - ->with('security') - ->willReturnSelf(); - $token->method('getUID')->willReturn('user123'); - $activityEvent->expects($this->once()) - ->method('setAuthor') - ->with('user123') - ->willReturnSelf(); - $activityEvent->expects($this->once()) - ->method('setAffectedUser') - ->with('user123') - ->willReturnSelf(); - $token->method('getName')->willReturn('Token 1'); - $activityEvent->expects($this->once()) - ->method('setSubject') - ->with('remote_wipe_start', ['name' => 'Token 1']) - ->willReturnSelf(); - $this->activityManager->expects($this->once()) - ->method('publish'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeStartedCanNotPublish(): void { - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $this->activityManager->expects($this->once()) - ->method('generateEvent'); - $this->activityManager->expects($this->once()) - ->method('publish') - ->willThrowException(new \BadMethodCallException()); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinished(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $activityEvent = $this->createMock(IActivityEvent::class); - $this->activityManager->expects($this->once()) - ->method('generateEvent') - ->willReturn($activityEvent); - $activityEvent->expects($this->once()) - ->method('setApp') - ->with('core') - ->willReturnSelf(); - $activityEvent->expects($this->once()) - ->method('setType') - ->with('security') - ->willReturnSelf(); - $token->method('getUID')->willReturn('user123'); - $activityEvent->expects($this->once()) - ->method('setAuthor') - ->with('user123') - ->willReturnSelf(); - $activityEvent->expects($this->once()) - ->method('setAffectedUser') - ->with('user123') - ->willReturnSelf(); - $token->method('getName')->willReturn('Token 1'); - $activityEvent->expects($this->once()) - ->method('setSubject') - ->with('remote_wipe_finish', ['name' => 'Token 1']) - ->willReturnSelf(); - $this->activityManager->expects($this->once()) - ->method('publish'); - - $this->listener->handle($event); - } -} --- a/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php +++ /dev/null @@ -1,225 +0,0 @@ -mailer = $this->createMock(IMailer::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->l10nFactory = $this->createMock(IFactory::class); - $this->l10n = $this->createMock(IL10N::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->l10nFactory->method('get')->with('core')->willReturn($this->l10n); - $this->l10n->method('t')->willReturnArgument(0); - - $this->listener = new RemoteWipeEmailListener( - $this->mailer, - $this->userManager, - $this->l10nFactory, - $this->logger - ); - } - - public function testHandleUnrelated(): void { - $event = new Event(); - $this->mailer->expects($this->never())->method('send'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeStartedInvalidUser(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $token->method('getUID')->willReturn('nope'); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn(null); - $this->mailer->expects($this->never())->method('send'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeStartedNoEmailSet(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn(null); - $this->mailer->expects($this->never())->method('send'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeStartedTransmissionError(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn('user@domain.org'); - $this->mailer->expects($this->once()) - ->method('send') - ->willThrowException(new Exception()); - $this->logger->expects($this->once()) - ->method('error'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeStarted(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn('user@domain.org'); - $message = $this->createMock(IMessage::class); - $this->mailer->expects($this->once()) - ->method('createMessage') - ->willReturn($message); - $message->expects($this->once()) - ->method('setTo') - ->with($this->equalTo(['user@domain.org'])); - $this->mailer->expects($this->once()) - ->method('send') - ->with($message); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinishedInvalidUser(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $token->method('getUID')->willReturn('nope'); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn(null); - $this->mailer->expects($this->never())->method('send'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinishedNoEmailSet(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn(null); - $this->mailer->expects($this->never())->method('send'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinishedTransmissionError(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn('user@domain.org'); - $this->mailer->expects($this->once()) - ->method('send') - ->willThrowException(new Exception()); - $this->logger->expects($this->once()) - ->method('error'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinished(): void { - /** @var IToken|MockObject $token */ - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $token->method('getUID')->willReturn('nope'); - $user = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('nope') - ->willReturn($user); - $user->method('getEMailAddress')->willReturn('user@domain.org'); - $message = $this->createMock(IMessage::class); - $this->mailer->expects($this->once()) - ->method('createMessage') - ->willReturn($message); - $message->expects($this->once()) - ->method('setTo') - ->with($this->equalTo(['user@domain.org'])); - $this->mailer->expects($this->once()) - ->method('send') - ->with($message); - - $this->listener->handle($event); - } -} --- a/tests/lib/Authentication/Listeners/RemoteWipeNotificationsListenerTest.php +++ /dev/null @@ -1,135 +0,0 @@ -notificationManager = $this->createMock(INotificationManager::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - - $this->listener = new RemoteWipeNotificationsListener( - $this->notificationManager, - $this->timeFactory - ); - } - - public function testHandleUnrelated(): void { - $event = new Event(); - - $this->listener->handle($event); - - $this->addToAssertionCount(1); - } - - public function testHandleRemoteWipeStarted(): void { - $token = $this->createMock(IToken::class); - $event = new RemoteWipeStarted($token); - $notification = $this->createMock(INotification::class); - $this->notificationManager->expects($this->once()) - ->method('createNotification') - ->willReturn($notification); - $notification->expects($this->once()) - ->method('setApp') - ->with('auth') - ->willReturnSelf(); - $token->method('getUID')->willReturn('user123'); - $notification->expects($this->once()) - ->method('setUser') - ->with('user123') - ->willReturnSelf(); - $now = new DateTime(); - $this->timeFactory->method('getDateTime')->willReturn($now); - $notification->expects($this->once()) - ->method('setDateTime') - ->with($now) - ->willReturnSelf(); - $token->method('getId')->willReturn(123); - $notification->expects($this->once()) - ->method('setObject') - ->with('token', '123') - ->willReturnSelf(); - $token->method('getName')->willReturn('Token 1'); - $notification->expects($this->once()) - ->method('setSubject') - ->with('remote_wipe_start', [ - 'name' => 'Token 1' - ]) - ->willReturnSelf(); - $this->notificationManager->expects($this->once()) - ->method('notify'); - - $this->listener->handle($event); - } - - public function testHandleRemoteWipeFinished(): void { - $token = $this->createMock(IToken::class); - $event = new RemoteWipeFinished($token); - $notification = $this->createMock(INotification::class); - $this->notificationManager->expects($this->once()) - ->method('createNotification') - ->willReturn($notification); - $notification->expects($this->once()) - ->method('setApp') - ->with('auth') - ->willReturnSelf(); - $token->method('getUID')->willReturn('user123'); - $notification->expects($this->once()) - ->method('setUser') - ->with('user123') - ->willReturnSelf(); - $now = new DateTime(); - $this->timeFactory->method('getDateTime')->willReturn($now); - $notification->expects($this->once()) - ->method('setDateTime') - ->with($now) - ->willReturnSelf(); - $token->method('getId')->willReturn(123); - $notification->expects($this->once()) - ->method('setObject') - ->with('token', '123') - ->willReturnSelf(); - $token->method('getName')->willReturn('Token 1'); - $notification->expects($this->once()) - ->method('setSubject') - ->with('remote_wipe_finish', [ - 'name' => 'Token 1' - ]) - ->willReturnSelf(); - $this->notificationManager->expects($this->once()) - ->method('notify'); - - $this->listener->handle($event); - } -} --- a/tests/lib/Authentication/Listeners/UserDeletedTokenCleanupListenerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -manager = $this->createMock(Manager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new UserDeletedTokenCleanupListener( - $this->manager, - $this->logger - ); - } - - public function testHandleUnrelated(): void { - $event = new Event(); - $this->manager->expects($this->never())->method('getTokenByUser'); - $this->logger->expects($this->never())->method('error'); - - $this->listener->handle($event); - } - - public function testHandleWithErrors(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $event = new UserDeletedEvent($user); - $exception = new Exception('nope'); - $this->manager->expects($this->once()) - ->method('getTokenByUser') - ->with('user123') - ->willThrowException($exception); - $this->logger->expects($this->once()) - ->method('error'); - - $this->listener->handle($event); - } - - public function testHandle(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $event = new UserDeletedEvent($user); - $token1 = $this->createMock(IToken::class); - $token1->method('getId')->willReturn(1); - $token2 = $this->createMock(IToken::class); - $token2->method('getId')->willReturn(2); - $token3 = $this->createMock(IToken::class); - $token3->method('getId')->willReturn(3); - $this->manager->expects($this->once()) - ->method('getTokenByUser') - ->with('user123') - ->willReturn([ - $token1, - $token2, - $token3, - ]); - - $calls = [ - ['user123', 1], - ['user123', 2], - ['user123', 3], - ]; - $this->manager->expects($this->exactly(3)) - ->method('invalidateTokenById') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - $this->logger->expects($this->never()) - ->method('error'); - - $this->listener->handle($event); - } -} --- a/tests/lib/Authentication/Login/ALoginTestCommand.php +++ /dev/null @@ -1,107 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->user = $this->createMock(IUser::class); - } - - protected function getBasicLoginData(): LoginData { - return new LoginData( - $this->request, - $this->username, - $this->password - ); - } - - protected function getInvalidLoginData(): LoginData { - return new LoginData( - $this->request, - $this->username, - $this->password - ); - } - - protected function getFailedLoginData(): LoginData { - $data = new LoginData( - $this->request, - $this->username, - $this->password - ); - $data->setUser(false); - return $data; - } - - protected function getLoggedInLoginData(): LoginData { - $basic = $this->getBasicLoginData(); - $basic->setUser($this->user); - return $basic; - } - - protected function getLoggedInLoginDataWithRedirectUrl(): LoginData { - $data = new LoginData( - $this->request, - $this->username, - $this->password, - true, - $this->redirectUrl - ); - $data->setUser($this->user); - return $data; - } - - protected function getLoggedInLoginDataWithTimezone(?string $timezone = null): LoginData { - $data = new LoginData( - $this->request, - $this->username, - $this->password, - true, - null, - $timezone ?? $this->timezone, - $this->timeZoneOffset - ); - $data->setUser($this->user); - return $data; - } -} --- a/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php +++ /dev/null @@ -1,48 +0,0 @@ -config = $this->createMock(IConfig::class); - - $this->cmd = new ClearLostPasswordTokensCommand( - $this->config - ); - } - - public function testProcess(): void { - $data = $this->getLoggedInLoginData(); - $this->user->expects($this->once()) - ->method('getUID') - ->willReturn($this->username); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with( - $this->username, - 'core', - 'lostpassword' - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/CompleteLoginCommandTest.php +++ /dev/null @@ -1,49 +0,0 @@ -session = $this->createMock(Session::class); - - $this->cmd = new CompleteLoginCommand( - $this->session - ); - } - - public function testProcess(): void { - $data = $this->getLoggedInLoginData(); - $this->session->expects($this->once()) - ->method('completeLogin') - ->with( - $this->user, - $this->equalTo( - [ - 'loginName' => $this->username, - 'password' => $this->password, - ] - ) - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php +++ /dev/null @@ -1,161 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->userSession = $this->createMock(Session::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - - $this->cmd = new CreateSessionTokenCommand( - $this->config, - $this->userSession, - $this->urlGenerator, - $this->timeFactory, - ); - } - - public function testProcess(): void { - // Just return the route name as path to not return an empty string - $this->urlGenerator->expects(self::once()) - ->method('linkToRoute') - ->willReturnArgument(0); - $data = $this->getLoggedInLoginData(); - $this->config->expects($this->once()) - ->method('getSystemValueInt') - ->with( - 'remember_login_cookie_lifetime', - 60 * 60 * 24 * 15 - ) - ->willReturn(100); - $this->user->expects($this->any()) - ->method('getUID') - ->willReturn($this->username); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with( - $this->request, - $this->username, - $this->username, - $this->password, - IToken::REMEMBER, - null - ); - $this->userSession->expects($this->once()) - ->method('updateTokens') - ->with( - $this->username, - $this->password - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessDoNotRemember(): void { - // Just return the route name as path to not return an empty string - $this->urlGenerator->expects(self::once()) - ->method('linkToRoute') - ->willReturnArgument(0); - $data = $this->getLoggedInLoginData(); - $this->config->expects($this->once()) - ->method('getSystemValueInt') - ->with( - 'remember_login_cookie_lifetime', - 60 * 60 * 24 * 15 - ) - ->willReturn(0); - $this->user->expects($this->any()) - ->method('getUID') - ->willReturn($this->username); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with( - $this->request, - $this->username, - $this->username, - $this->password, - IToken::DO_NOT_REMEMBER, - null - ); - $this->userSession->expects($this->once()) - ->method('updateTokens') - ->with( - $this->username, - $this->password - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertFalse($data->isRememberLogin()); - } - - public function testLoginFlowEphemeral(): void { - $this->redirectUrl = 'EPHEMERAL_ROUTE'; - $this->urlGenerator->expects(self::once()) - ->method('linkToRoute') - ->willReturn($this->redirectUrl); - $this->timeFactory->expects(self::once()) - ->method('getTime') - ->willReturn(1000); - - $data = $this->getLoggedInLoginDataWithRedirectUrl(); - $this->config->expects($this->once()) - ->method('getSystemValueInt') - ->with( - 'remember_login_cookie_lifetime', - 60 * 60 * 24 * 15 - ) - ->willReturn(100); - $this->user->expects($this->any()) - ->method('getUID') - ->willReturn($this->username); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with( - $this->request, - $this->username, - $this->username, - $this->password, - IToken::REMEMBER, - 1000 + 5 * 60 - ); - $this->userSession->expects($this->once()) - ->method('updateTokens') - ->with( - $this->username, - $this->password - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php +++ /dev/null @@ -1,75 +0,0 @@ -userSession = $this->createMock(Session::class); - $this->config = $this->createMock(IConfig::class); - - $this->cmd = new FinishRememberedLoginCommand( - $this->userSession, - $this->config - ); - } - - public function testProcessNotRememberedLogin(): void { - $data = $this->getLoggedInLoginData(); - $data->setRememberLogin(false); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcess(): void { - $data = $this->getLoggedInLoginData(); - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('auto_logout', false) - ->willReturn(false); - $this->userSession->expects($this->once()) - ->method('createRememberMeToken') - ->with($this->user); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessNotRemeberedLoginWithAutologout(): void { - $data = $this->getLoggedInLoginData(); - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('auto_logout', false) - ->willReturn(true); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php +++ /dev/null @@ -1,56 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - - $this->cmd = new LoggedInCheckCommand( - $this->logger, - $this->dispatcher - ); - } - - public function testProcessSuccessfulLogin(): void { - $data = $this->getLoggedInLoginData(); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessFailedLogin(): void { - $data = $this->getFailedLoginData(); - $this->logger->expects($this->once()) - ->method('warning'); - - $result = $this->cmd->process($data); - - $this->assertFalse($result->isSuccess()); - $this->assertSame(LoginController::LOGIN_MSG_INVALIDPASSWORD, $result->getErrorMessage()); - } -} --- a/tests/lib/Authentication/Login/PreLoginHookCommandTest.php +++ /dev/null @@ -1,49 +0,0 @@ -userManager = $this->createMock(Manager::class); - - $this->cmd = new PreLoginHookCommand( - $this->userManager - ); - } - - public function testProcess(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('emit') - ->with( - '\OC\User', - 'preLogin', - [ - $this->username, - $this->password, - ] - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/RecordInteractiveLoginCommandTest.php +++ /dev/null @@ -1,41 +0,0 @@ -lastInteractiveLogin = $this->createMock(LastInteractiveLogin::class); - - $this->cmd = new RecordInteractiveLoginCommand( - $this->lastInteractiveLogin - ); - } - - public function testProcess(): void { - $data = $this->getLoggedInLoginData(); - $this->lastInteractiveLogin->expects($this->once()) - ->method('record') - ->with($this->user); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php +++ /dev/null @@ -1,146 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->session = $this->createMock(ISession::class); - - $this->cmd = new SetUserTimezoneCommand( - $this->config, - $this->session - ); - } - - public function testProcessNoTimezoneSet(): void { - $data = $this->getLoggedInLoginData(); - $this->config->expects($this->never()) - ->method('setUserValue'); - $this->session->expects($this->never()) - ->method('set'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - /** - * Debian and Ubuntu ship the tz database's backward links in a separate - * tzdata-legacy package, so pick an alias this platform actually knows - * instead of hardcoding one. - */ - private static function findBackwardCompatibleTimezone(): ?string { - $aliases = array_diff( - \DateTimeZone::listIdentifiers(\DateTimeZone::ALL_WITH_BC), - \DateTimeZone::listIdentifiers(), - ); - return $aliases === [] ? null : reset($aliases); - } - - public static function dataAcceptedTimezone(): array { - return [ - 'primary identifier' => ['Europe/Vienna'], - 'backward compatible alias' => [self::findBackwardCompatibleTimezone()], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataAcceptedTimezone')] - public function testProcess(?string $timezone): void { - if ($timezone === null) { - $this->markTestSkipped('No backward compatible timezone aliases in this platform\'s tz database'); - } - - $data = $this->getLoggedInLoginDataWithTimezone($timezone); - $this->user->expects($this->once()) - ->method('getUID') - ->willReturn($this->username); - $this->config->expects($this->once()) - ->method('getUserValue') - ->with( - $this->username, - 'core', - 'timezone', - '' - ) - ->willReturn(''); - $this->config->expects($this->once()) - ->method('setUserValue') - ->with( - $this->username, - 'core', - 'timezone', - $timezone - ); - $this->session->expects($this->once()) - ->method('set') - ->with( - 'timezone', - $this->timeZoneOffset - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessUnknownTimezone(): void { - $data = $this->getLoggedInLoginDataWithTimezone('Mars/Olympus_Mons'); - $this->config->expects($this->never()) - ->method('setUserValue'); - $this->session->expects($this->never()) - ->method('set'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessAlreadySet(): void { - $data = $this->getLoggedInLoginDataWithTimezone(); - $this->user->expects($this->once()) - ->method('getUID') - ->willReturn($this->username); - $this->config->expects($this->once()) - ->method('getUserValue') - ->with( - $this->username, - 'core', - 'timezone', - '', - ) - ->willReturn('Europe/Berlin'); - $this->config->expects($this->never()) - ->method('setUserValue'); - $this->session->expects($this->once()) - ->method('set') - ->with( - 'timezone', - $this->timeZoneOffset - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/TwoFactorCommandTest.php +++ /dev/null @@ -1,377 +0,0 @@ -twoFactorManager = $this->createMock(Manager::class); - $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - - $this->cmd = new TwoFactorCommand( - $this->twoFactorManager, - $this->mandatoryTwoFactor, - $this->urlGenerator - ); - } - - public function testNotTwoFactorAuthenticated(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(false); - $this->twoFactorManager->expects($this->never()) - ->method('prepareTwoFactorLogin'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testSkippedForVerifiedWebAuthnLogin(): void { - $data = $this->getLoggedInLoginData(); - $data->setWebAuthnUserVerified(true); - $this->twoFactorManager->expects($this->never()) - ->method('prepareTwoFactorLogin'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertNull($result->getRedirectUrl()); - } - - public function testNotSkippedForWebAuthnLoginWithoutUserVerification(): void { - $data = $this->getLoggedInLoginData(); - $data->setWebAuthnUserVerified(false); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin'); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([], false)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->willReturn([]); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessOneActiveProvider(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $provider = $this->createMock(ITwoFactorAuthProvider::class); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([ - $provider, - ], false)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('test'); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.showChallenge', - [ - 'challengeProviderId' => 'test' - ] - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessMissingProviders(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $provider = $this->createMock(ITwoFactorAuthProvider::class); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('test1'); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([ - $provider, - ], true)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.selectChallenge' - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessTwoActiveProviders(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $provider1 = $this->createMock(ITwoFactorAuthProvider::class); - $provider2 = $this->createMock(ITwoFactorAuthProvider::class); - $provider1->expects($this->once()) - ->method('getId') - ->willReturn('test1'); - $provider2->expects($this->once()) - ->method('getId') - ->willReturn('test2'); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([ - $provider1, - $provider2, - ], false)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.selectChallenge' - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessFailingProviderAndEnforcedButNoSetupProviders(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([], true)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.selectChallenge' - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessFailingProviderAndEnforced(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $provider = $this->createMock(IActivatableAtLogin::class); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([ - $provider, - ], true)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.selectChallenge' - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessNoProvidersButEnforced(): void { - $data = $this->getLoggedInLoginData(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([], false)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(true); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.selectChallenge' - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } - - public function testProcessWithRedirectUrl(): void { - $data = $this->getLoggedInLoginDataWithRedirectUrl(); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->willReturn(true); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with( - $this->user, - $data->isRememberLogin() - ); - $provider = $this->createMock(ITwoFactorAuthProvider::class); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->willReturn(new ProviderSet([ - $provider, - ], false)); - $this->twoFactorManager->expects($this->once()) - ->method('getLoginSetupProviders') - ->with($this->user) - ->willReturn([]); - $this->mandatoryTwoFactor->expects($this->any()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $provider->expects($this->once()) - ->method('getId') - ->willReturn('test'); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with( - 'core.TwoFactorChallenge.showChallenge', - [ - 'challengeProviderId' => 'test', - 'redirect_url' => $this->redirectUrl, - ] - ) - ->willReturn('two/factor/url'); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals('two/factor/url', $result->getRedirectUrl()); - } -} --- a/tests/lib/Authentication/Login/UidLoginCommandTest.php +++ /dev/null @@ -1,62 +0,0 @@ -userManager = $this->createMock(Manager::class); - - $this->cmd = new UidLoginCommand( - $this->userManager - ); - } - - public function testProcessFailingLogin(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->with( - $this->username, - $this->password - ) - ->willReturn(false); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertFalse($data->getUser()); - } - - public function testProcess(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->with( - $this->username, - $this->password - ) - ->willReturn($this->user); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - $this->assertEquals($this->user, $data->getUser()); - } -} --- a/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php +++ /dev/null @@ -1,47 +0,0 @@ -session = $this->createMock(ISession::class); - - $this->cmd = new UpdateLastPasswordConfirmCommand( - $this->session - ); - } - - public function testProcess(): void { - $data = $this->getLoggedInLoginData(); - $this->user->expects($this->once()) - ->method('getLastLogin') - ->willReturn(1234); - $this->session->expects($this->once()) - ->method('set') - ->with( - 'last-password-confirm', - 1234 - ); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php +++ /dev/null @@ -1,80 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->cmd = new UserDisabledCheckCommand( - $this->userManager, - $this->logger - ); - } - - public function testProcessNonExistingUser(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->username) - ->willReturn(null); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } - - public function testProcessDisabledUser(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->username) - ->willReturn($this->user); - $this->user->expects($this->once()) - ->method('isEnabled') - ->willReturn(false); - - $result = $this->cmd->process($data); - - $this->assertFalse($result->isSuccess()); - $this->assertSame(LoginController::LOGIN_MSG_USERDISABLED, $result->getErrorMessage()); - } - - public function testProcess(): void { - $data = $this->getBasicLoginData(); - $this->userManager->expects($this->once()) - ->method('get') - ->with($this->username) - ->willReturn($this->user); - $this->user->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - - $result = $this->cmd->process($data); - - $this->assertTrue($result->isSuccess()); - } -} --- a/tests/lib/Authentication/LoginCredentials/CredentialsTest.php +++ /dev/null @@ -1,48 +0,0 @@ -uid = 'user123'; - $this->user = 'User123'; - $this->password = '123456'; - - $this->credentials = new Credentials($this->uid, $this->user, $this->password); - } - - public function testGetUID(): void { - $this->assertEquals($this->uid, $this->credentials->getUID()); - } - - public function testGetUserName(): void { - $this->assertEquals($this->user, $this->credentials->getLoginName()); - } - - public function testGetPassword(): void { - $this->assertEquals($this->password, $this->credentials->getPassword()); - } -} --- a/tests/lib/Authentication/LoginCredentials/StoreTest.php +++ /dev/null @@ -1,296 +0,0 @@ -session = $this->createMock(ISession::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->crypto = $this->createMock(ICrypto::class); - - $this->store = new Store($this->session, $this->logger, $this->crypto, $this->tokenProvider); - } - - public function testAuthenticate(): void { - $params = [ - 'run' => true, - 'uid' => 'user123', - 'password' => '123456', - ]; - - $this->session->expects($this->once()) - ->method('set') - ->with($this->equalTo('login_credentials'), $this->equalTo(json_encode($params))); - $this->crypto->expects($this->once()) - ->method('encrypt') - ->willReturn('123456'); - - $this->store->authenticate($params); - } - - public function testSetSession(): void { - $session = $this->createMock(ISession::class); - - $this->store->setSession($session); - $this->addToAssertionCount(1); - } - - public function testGetLoginCredentialsNoTokenProvider(): void { - $this->store = new Store($this->session, $this->logger, $this->crypto, null); - - $this->expectException(CredentialsUnavailableException::class); - - $this->store->getLoginCredentials(); - } - - public function testGetLoginCredentials(): void { - $uid = 'uid'; - $user = 'user123'; - $password = 'passme'; - $token = $this->createMock(IToken::class); - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willReturn($token); - $token->expects($this->once()) - ->method('getUID') - ->willReturn($uid); - $token->expects($this->once()) - ->method('getLoginName') - ->willReturn($user); - $this->tokenProvider->expects($this->once()) - ->method('getPassword') - ->with($token, 'sess2233') - ->willReturn($password); - $expected = new Credentials($uid, $user, $password); - - $creds = $this->store->getLoginCredentials(); - - $this->assertEquals($expected, $creds); - } - - public function testGetLoginCredentialsSessionNotAvailable(): void { - $this->session->expects($this->once()) - ->method('getId') - ->willThrowException(new SessionNotAvailableException()); - $this->expectException(CredentialsUnavailableException::class); - - $this->store->getLoginCredentials(); - } - - public function testGetLoginCredentialsInvalidToken(): void { - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new InvalidTokenException()); - $this->expectException(CredentialsUnavailableException::class); - - $this->store->getLoginCredentials(); - } - - public function testGetLoginCredentialsPartialCredentialsAndSessionName(): void { - $uid = 'id987'; - $user = 'user987'; - $password = '7389374'; - - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new InvalidTokenException()); - $this->session->expects($this->once()) - ->method('exists') - ->with($this->equalTo('login_credentials')) - ->willReturn(true); - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willReturn($password); - $this->session->expects($this->exactly(2)) - ->method('get') - ->willReturnMap([ - [ - 'login_credentials', - json_encode([ - 'uid' => $uid, - 'password' => $password, - ]) - ], - [ - 'loginname', - $user, - ], - ]); - $expected = new Credentials($uid, $user, $password); - - $actual = $this->store->getLoginCredentials(); - - $this->assertEquals($expected, $actual); - } - - public function testGetLoginCredentialsPartialCredentials(): void { - $uid = 'id987'; - $password = '7389374'; - - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new InvalidTokenException()); - $this->session->expects($this->once()) - ->method('exists') - ->with($this->equalTo('login_credentials')) - ->willReturn(true); - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willReturn($password); - $this->session->expects($this->exactly(2)) - ->method('get') - ->willReturnMap([ - [ - 'login_credentials', - json_encode([ - 'uid' => $uid, - 'password' => $password, - ]) - ], - [ - 'loginname', - null, - ], - ]); - $expected = new Credentials($uid, $uid, $password); - - $actual = $this->store->getLoginCredentials(); - - $this->assertEquals($expected, $actual); - } - - public function testGetLoginCredentialsInvalidTokenLoginCredentials(): void { - $uid = 'id987'; - $user = 'user987'; - $password = '7389374'; - - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new InvalidTokenException()); - $this->session->expects($this->once()) - ->method('exists') - ->with($this->equalTo('login_credentials')) - ->willReturn(true); - $this->crypto->expects($this->once()) - ->method('decrypt') - ->willReturn($password); - $this->session->expects($this->once()) - ->method('get') - ->with($this->equalTo('login_credentials')) - ->willReturn('{"run":true,"uid":"id987","loginName":"user987","password":"7389374"}'); - $expected = new Credentials($uid, $user, $password); - - $actual = $this->store->getLoginCredentials(); - - $this->assertEquals($expected, $actual); - } - - public function testGetLoginCredentialsPasswordlessToken(): void { - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new PasswordlessTokenException()); - $this->expectException(CredentialsUnavailableException::class); - - $this->store->getLoginCredentials(); - } - - public function testAuthenticatePasswordlessToken(): void { - $user = 'user987'; - $password = null; - - $params = [ - 'run' => true, - 'loginName' => $user, - 'uid' => $user, - 'password' => $password, - ]; - - $this->session->expects($this->once()) - ->method('set') - ->with($this->equalTo('login_credentials'), $this->equalTo(json_encode($params))); - - $this->session->expects($this->once()) - ->method('getId') - ->willReturn('sess2233'); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('sess2233') - ->willThrowException(new PasswordlessTokenException()); - - $this->session->expects($this->once()) - ->method('exists') - ->with($this->equalTo('login_credentials')) - ->willReturn(true); - $this->session->expects($this->once()) - ->method('get') - ->with($this->equalTo('login_credentials')) - ->willReturn(json_encode($params)); - - $this->store->authenticate($params); - $actual = $this->store->getLoginCredentials(); - - $expected = new Credentials($user, $user, $password); - $this->assertEquals($expected, $actual); - } -} --- a/tests/lib/Authentication/Token/ManagerTest.php +++ /dev/null @@ -1,407 +0,0 @@ -publicKeyTokenProvider = $this->createMock(PublicKeyTokenProvider::class); - $this->manager = new Manager( - $this->publicKeyTokenProvider - ); - } - - public function testGenerateToken(): void { - $token = new PublicKeyToken(); - - $this->publicKeyTokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'token', - 'uid', - 'loginName', - 'password', - 'name', - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - )->willReturn($token); - - $actual = $this->manager->generateToken( - 'token', - 'uid', - 'loginName', - 'password', - 'name', - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - ); - - $this->assertSame($token, $actual); - } - - public function testGenerateConflictingToken(): void { - /** @var MockObject|Exception $exception */ - $exception = $this->createMock(Exception::class); - $exception->method('getReason')->willReturn(Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION); - - $token = new PublicKeyToken(); - $token->setUid('uid'); - - $this->publicKeyTokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'token', - 'uid', - 'loginName', - 'password', - 'name', - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - )->willThrowException($exception); - $this->publicKeyTokenProvider->expects($this->once()) - ->method('getToken') - ->with('token') - ->willReturn($token); - - $actual = $this->manager->generateToken( - 'token', - 'uid', - 'loginName', - 'password', - 'name', - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - ); - - $this->assertSame($token, $actual); - } - - public function testGenerateTokenTooLongName(): void { - $token = $this->createMock(IToken::class); - $token->method('getName') - ->willReturn(str_repeat('a', 120) . '…'); - - $this->publicKeyTokenProvider->expects($this->once()) - ->method('generateToken') - ->with( - 'token', - 'uid', - 'loginName', - 'password', - str_repeat('a', 120) . '…', - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - )->willReturn($token); - - $actual = $this->manager->generateToken( - 'token', - 'uid', - 'loginName', - 'password', - str_repeat('a', 200), - IToken::TEMPORARY_TOKEN, - IToken::REMEMBER - ); - - $this->assertSame(121, mb_strlen($actual->getName())); - } - - public static function tokenData(): array { - return [ - [new PublicKeyToken()], - [IToken::class], - ]; - } - - protected function setNoCall(IToken $token) { - if (!($token instanceof PublicKeyToken)) { - $this->publicKeyTokenProvider->expects($this->never()) - ->method($this->anything()); - } - } - - protected function setCall(IToken $token, string $function, $return = null) { - if ($token instanceof PublicKeyToken) { - $this->publicKeyTokenProvider->expects($this->once()) - ->method($function) - ->with($token) - ->willReturn($return); - } - } - - protected function setException(IToken $token) { - if (!($token instanceof PublicKeyToken)) { - $this->expectException(InvalidTokenException::class); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] - public function testUpdateToken(IToken|string $token): void { - if (is_string($token)) { - $token = $this->createMock($token); - } - - $this->setNoCall($token); - $this->setCall($token, 'updateToken'); - $this->setException($token); - - $this->manager->updateToken($token); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] - public function testUpdateTokenActivity(IToken|string $token): void { - if (is_string($token)) { - $token = $this->createMock($token); - } - - $this->setNoCall($token); - $this->setCall($token, 'updateTokenActivity'); - $this->setException($token); - - $this->manager->updateTokenActivity($token); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] - public function testGetPassword(IToken|string $token): void { - if (is_string($token)) { - $token = $this->createMock($token); - } - - $this->setNoCall($token); - $this->setCall($token, 'getPassword', 'password'); - $this->setException($token); - - $result = $this->manager->getPassword($token, 'tokenId', 'password'); - - $this->assertSame('password', $result); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] - public function testSetPassword(IToken|string $token): void { - if (is_string($token)) { - $token = $this->createMock($token); - } - - $this->setNoCall($token); - $this->setCall($token, 'setPassword'); - $this->setException($token); - - $this->manager->setPassword($token, 'tokenId', 'password'); - } - - public function testInvalidateTokens(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('invalidateToken') - ->with('token'); - - $this->manager->invalidateToken('token'); - } - - public function testInvalidateTokenById(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('invalidateTokenById') - ->with('uid', 42); - - $this->manager->invalidateTokenById('uid', 42); - } - - public function testInvalidateOldTokens(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('invalidateOldTokens'); - - $this->manager->invalidateOldTokens(); - } - - public function testInvalidateLastUsedBefore(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('invalidateLastUsedBefore') - ->with('user', 946684800); - - $this->manager->invalidateLastUsedBefore('user', 946684800); - } - - public function testGetTokenByUser(): void { - $t1 = new PublicKeyToken(); - $t2 = new PublicKeyToken(); - - $this->publicKeyTokenProvider - ->method('getTokenByUser') - ->willReturn([$t1, $t2]); - - $result = $this->manager->getTokenByUser('uid'); - - $this->assertEquals([$t1, $t2], $result); - } - - public function testRenewSessionTokenPublicKey(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('renewSessionToken') - ->with('oldId', 'newId'); - - $this->manager->renewSessionToken('oldId', 'newId'); - } - - public function testRenewSessionInvalid(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('renewSessionToken') - ->with('oldId', 'newId') - ->willThrowException(new InvalidTokenException()); - - $this->expectException(InvalidTokenException::class); - $this->manager->renewSessionToken('oldId', 'newId'); - } - - public function testGetTokenByIdPublicKey(): void { - $token = $this->createMock(IToken::class); - - $this->publicKeyTokenProvider->expects($this->once()) - ->method('getTokenById') - ->with(42) - ->willReturn($token); - - $this->assertSame($token, $this->manager->getTokenById(42)); - } - - public function testGetTokenByIdInvalid(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('getTokenById') - ->with(42) - ->willThrowException(new InvalidTokenException()); - - $this->expectException(InvalidTokenException::class); - $this->manager->getTokenById(42); - } - - public function testGetTokenPublicKey(): void { - $token = new PublicKeyToken(); - - $this->publicKeyTokenProvider - ->method('getToken') - ->with('tokenId') - ->willReturn($token); - - $this->assertSame($token, $this->manager->getToken('tokenId')); - } - - public function testGetTokenInvalid(): void { - $this->publicKeyTokenProvider - ->method('getToken') - ->with('tokenId') - ->willThrowException(new InvalidTokenException()); - - $this->expectException(InvalidTokenException::class); - $this->manager->getToken('tokenId'); - } - - public function testRotateInvalid(): void { - $this->expectException(InvalidTokenException::class); - $this->manager->rotate($this->createMock(IToken::class), 'oldId', 'newId'); - } - - public function testRotatePublicKey(): void { - $token = new PublicKeyToken(); - - $this->publicKeyTokenProvider - ->method('rotate') - ->with($token, 'oldId', 'newId') - ->willReturn($token); - - $this->assertSame($token, $this->manager->rotate($token, 'oldId', 'newId')); - } - - public function testMarkPasswordInvalidPublicKey(): void { - $token = $this->createMock(PublicKeyToken::class); - - $this->publicKeyTokenProvider->expects($this->once()) - ->method('markPasswordInvalid') - ->with($token, 'tokenId'); - - $this->manager->markPasswordInvalid($token, 'tokenId'); - } - - public function testMarkPasswordInvalidInvalidToken(): void { - $this->expectException(InvalidTokenException::class); - - $this->manager->markPasswordInvalid($this->createMock(IToken::class), 'tokenId'); - } - - public function testUpdatePasswords(): void { - $this->publicKeyTokenProvider->expects($this->once()) - ->method('updatePasswords') - ->with('uid', 'pass'); - - $this->manager->updatePasswords('uid', 'pass'); - } - - public function testInvalidateTokensOfUserNoClientName(): void { - $t1 = new PublicKeyToken(); - $t2 = new PublicKeyToken(); - $t1->setId(123); - $t2->setId(456); - - $this->publicKeyTokenProvider - ->expects($this->once()) - ->method('getTokenByUser') - ->with('theUser') - ->willReturn([$t1, $t2]); - - $calls = [ - ['theUser', 123], - ['theUser', 456], - ]; - $this->publicKeyTokenProvider - ->expects($this->exactly(2)) - ->method('invalidateTokenById') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - $this->manager->invalidateTokensOfUser('theUser', null); - } - - public function testInvalidateTokensOfUserClientNameGiven(): void { - $t1 = new PublicKeyToken(); - $t2 = new PublicKeyToken(); - $t3 = new PublicKeyToken(); - $t1->setId(123); - $t1->setName('Firefox session'); - $t2->setId(456); - $t2->setName('My Client Name'); - $t3->setId(789); - $t3->setName('mobile client'); - - $this->publicKeyTokenProvider - ->expects($this->once()) - ->method('getTokenByUser') - ->with('theUser') - ->willReturn([$t1, $t2, $t3]); - $this->publicKeyTokenProvider - ->expects($this->once()) - ->method('invalidateTokenById') - ->with('theUser', 456); - $this->manager->invalidateTokensOfUser('theUser', 'My Client Name'); - } -} --- a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php +++ /dev/null @@ -1,261 +0,0 @@ -dbConnection = Server::get(IDBConnection::class); - $this->time = time(); - $this->resetDatabase(); - - $this->mapper = new PublicKeyTokenMapper($this->dbConnection); - } - - private function resetDatabase() { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->delete('authtoken')->executeStatement(); - $qb->insert('authtoken')->values([ - 'uid' => $qb->createNamedParameter('user1'), - 'login_name' => $qb->createNamedParameter('User1'), - 'password' => $qb->createNamedParameter('a75c7116460c082912d8f6860a850904|3nz5qbG1nNSLLi6V|c55365a0e54cfdfac4a175bcf11a7612aea74492277bba6e5d96a24497fa9272488787cb2f3ad34d8b9b8060934fce02f008d371df3ff3848f4aa61944851ff0'), - 'name' => $qb->createNamedParameter('Firefox on Linux'), - 'token' => $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'), - 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), - 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago - 'last_check' => $this->time - 60 * 10, // 10mins ago - 'public_key' => $qb->createNamedParameter('public key'), - 'private_key' => $qb->createNamedParameter('private key'), - 'version' => $qb->createNamedParameter(2), - ])->executeStatement(); - $qb->insert('authtoken')->values([ - 'uid' => $qb->createNamedParameter('user2'), - 'login_name' => $qb->createNamedParameter('User2'), - 'password' => $qb->createNamedParameter('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'), - 'name' => $qb->createNamedParameter('Firefox on Android'), - 'token' => $qb->createNamedParameter('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'), - 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), - 'last_activity' => $qb->createNamedParameter($this->time - 60 * 60 * 24 * 3, IQueryBuilder::PARAM_INT), // Three days ago - 'last_check' => $this->time - 10, // 10secs ago - 'public_key' => $qb->createNamedParameter('public key'), - 'private_key' => $qb->createNamedParameter('private key'), - 'version' => $qb->createNamedParameter(2), - ])->executeStatement(); - $qb->insert('authtoken')->values([ - 'uid' => $qb->createNamedParameter('user1'), - 'login_name' => $qb->createNamedParameter('User1'), - 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), - 'name' => $qb->createNamedParameter('Iceweasel on Linux'), - 'token' => $qb->createNamedParameter('47af8697ba590fb82579b5f1b3b6e8066773a62100abbe0db09a289a62f5d980dc300fa3d98b01d7228468d1ab05c1aa14c8d14bd5b6eee9cdf1ac14864680c3'), - 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), - 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago - 'last_check' => $this->time - 60 * 10, // 10mins ago - 'public_key' => $qb->createNamedParameter('public key'), - 'private_key' => $qb->createNamedParameter('private key'), - 'version' => $qb->createNamedParameter(2), - ])->executeStatement(); - $qb->insert('authtoken')->values([ - 'uid' => $qb->createNamedParameter('user3'), - 'login_name' => $qb->createNamedParameter('User3'), - 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), - 'name' => $qb->createNamedParameter('Iceweasel on Linux'), - 'token' => $qb->createNamedParameter('6d9a290d239d09f2cc33a03cc54cccd46f7dc71630dcc27d39214824bd3e093f1feb4e2b55eb159d204caa15dee9556c202a5aa0b9d67806c3f4ec2cde11af67'), - 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), - 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago - 'last_check' => $this->time - 60 * 10, // 10mins ago - 'public_key' => $qb->createNamedParameter('public key'), - 'private_key' => $qb->createNamedParameter('private key'), - 'version' => $qb->createNamedParameter(2), - 'password_invalid' => $qb->createNamedParameter(1), - ])->executeStatement(); - $qb->insert('authtoken')->values([ - 'uid' => $qb->createNamedParameter('user3'), - 'login_name' => $qb->createNamedParameter('User3'), - 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), - 'name' => $qb->createNamedParameter('Iceweasel on Linux'), - 'token' => $qb->createNamedParameter('84c5808c6445b6d65b8aa5b03840f09b27de603f0fb970906fb14ea4b115b7bf5ec53fada5c093fe46afdcd7bbc9617253a4d105f7dfb32719f9973d72412f31'), - 'type' => $qb->createNamedParameter(IToken::PERMANENT_TOKEN), - 'last_activity' => $qb->createNamedParameter($this->time - 60 * 3, IQueryBuilder::PARAM_INT), // Three minutes ago - 'last_check' => $this->time - 60 * 10, // 10mins ago - 'public_key' => $qb->createNamedParameter('public key'), - 'private_key' => $qb->createNamedParameter('private key'), - 'version' => $qb->createNamedParameter(2), - 'password_invalid' => $qb->createNamedParameter(1), - ])->executeStatement(); - } - - private function getNumberOfTokens() { - $qb = $this->dbConnection->getQueryBuilder(); - $result = $qb->select($qb->func()->count('*', 'count')) - ->from('authtoken') - ->executeQuery() - ->fetchAssociative(); - return (int)$result['count']; - } - - public function testInvalidate(): void { - $token = '9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'; - - $this->mapper->invalidate($token); - - $this->assertSame(4, $this->getNumberOfTokens()); - } - - public function testInvalidateInvalid(): void { - $token = 'youwontfindthisoneinthedatabase'; - - $this->mapper->invalidate($token); - - $this->assertSame(5, $this->getNumberOfTokens()); - } - - public function testInvalidateOld(): void { - $olderThan = $this->time - 60 * 60; // One hour - - $this->mapper->invalidateOld($olderThan); - - $this->assertSame(4, $this->getNumberOfTokens()); - } - - public function testInvalidateLastUsedBefore(): void { - $before = $this->time - 60 * 2; // Two minutes - - $this->mapper->invalidateLastUsedBefore('user3', $before); - - $this->assertSame(4, $this->getNumberOfTokens()); - } - - public function testGetToken(): void { - $token = new PublicKeyToken(); - $token->setUid('user2'); - $token->setLoginName('User2'); - $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); - $token->setName('Firefox on Android'); - $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); - $token->setType(IToken::TEMPORARY_TOKEN); - $token->setRemember(IToken::DO_NOT_REMEMBER); - $token->setLastActivity($this->time - 60 * 60 * 24 * 3); - $token->setLastCheck($this->time - 10); - $token->setPublicKey('public key'); - $token->setPrivateKey('private key'); - $token->setVersion(PublicKeyToken::VERSION); - - $dbToken = $this->mapper->getToken($token->getToken()); - - $token->setId($dbToken->getId()); // We don't know the ID - $token->resetUpdatedFields(); - - $this->assertEquals($token, $dbToken); - } - - public function testGetInvalidToken(): void { - $this->expectException(DoesNotExistException::class); - - $token = 'thisisaninvalidtokenthatisnotinthedatabase'; - - $this->mapper->getToken($token); - } - - public function testGetTokenById(): void { - $token = new PublicKeyToken(); - $token->setUid('user2'); - $token->setLoginName('User2'); - $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); - $token->setName('Firefox on Android'); - $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); - $token->setType(IToken::TEMPORARY_TOKEN); - $token->setRemember(IToken::DO_NOT_REMEMBER); - $token->setLastActivity($this->time - 60 * 60 * 24 * 3); - $token->setLastCheck($this->time - 10); - $token->setPublicKey('public key'); - $token->setPrivateKey('private key'); - $token->setVersion(PublicKeyToken::VERSION); - - $dbToken = $this->mapper->getToken($token->getToken()); - $token->setId($dbToken->getId()); // We don't know the ID - $token->resetUpdatedFields(); - - $dbToken = $this->mapper->getTokenById($token->getId()); - $this->assertEquals($token, $dbToken); - } - - public function testGetTokenByIdNotFound(): void { - $this->expectException(DoesNotExistException::class); - - $this->mapper->getTokenById(-1); - } - - public function testGetInvalidTokenById(): void { - $this->expectException(DoesNotExistException::class); - - $id = '42'; - - $this->mapper->getToken($id); - } - - public function testGetTokenByUser(): void { - $this->assertCount(2, $this->mapper->getTokenByUser('user1')); - } - - public function testGetTokenByUserNotFound(): void { - $this->assertCount(0, $this->mapper->getTokenByUser('user1000')); - } - - public function testGetById(): void { - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('id') - ->from('authtoken') - ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); - $result = $qb->executeQuery(); - $id = $result->fetchAssociative()['id']; - - $token = $this->mapper->getTokenById((int)$id); - $this->assertEquals('user1', $token->getUID()); - } - - public function testDeleteByName(): void { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('name') - ->from('authtoken') - ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); - $result = $qb->executeQuery(); - $name = $result->fetchAssociative()['name']; - $this->mapper->deleteByName($name); - $this->assertEquals(4, $this->getNumberOfTokens()); - } - - public function testHasExpiredTokens(): void { - $this->assertFalse($this->mapper->hasExpiredTokens('user1')); - $this->assertTrue($this->mapper->hasExpiredTokens('user3')); - } -} --- a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php +++ /dev/null @@ -1,645 +0,0 @@ -mapper = $this->createMock(PublicKeyTokenMapper::class); - $this->config = $this->createMock(IConfig::class); - $this->config->method('getSystemValue') - ->willReturnMap([ - ['openssl', [], []], - ]); - $this->config->method('getSystemValueString') - ->willReturnMap([ - ['secret', '', '1f4h9s'], - ]); - $this->db = $this->createMock(IDBConnection::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->time = 1313131; - $this->timeFactory->method('getTime') - ->willReturn($this->time); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - - $this->hasher = Server::get(IHasher::class); - $this->crypto = Server::get(ICrypto::class); - $this->eventDispatcher = Server::get(IEventDispatcher::class); - - $this->tokenProvider = new PublicKeyTokenProvider( - $this->mapper, - $this->crypto, - $this->config, - $this->db, - $this->logger, - $this->timeFactory, - $this->hasher, - $this->cacheFactory, - $this->eventDispatcher, - ); - } - - public function testGenerateToken(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->assertInstanceOf(PublicKeyToken::class, $actual); - $this->assertSame($uid, $actual->getUID()); - $this->assertSame($user, $actual->getLoginName()); - $this->assertSame($name, $actual->getName()); - $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); - $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); - } - - public function testGenerateTokenNoPassword(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, false], - ]); - $this->expectException(PasswordlessTokenException::class); - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->assertInstanceOf(PublicKeyToken::class, $actual); - $this->assertSame($uid, $actual->getUID()); - $this->assertSame($user, $actual->getLoginName()); - $this->assertSame($name, $actual->getName()); - $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); - $this->tokenProvider->getPassword($actual, $token); - } - - public function testGenerateTokenLongPassword(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = ''; - for ($i = 0; $i < 500; $i++) { - $password .= 'e'; - } - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - $this->expectException(\RuntimeException::class); - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - } - - public function testGenerateTokenInvalidName(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' - . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' - . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' - . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->assertInstanceOf(PublicKeyToken::class, $actual); - $this->assertSame($uid, $actual->getUID()); - $this->assertSame($user, $actual->getLoginName()); - $this->assertSame('User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12User-Agent: Mozill…', $actual->getName()); - $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); - $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); - } - - public function testUpdateToken(): void { - $tk = new PublicKeyToken(); - $this->mapper->expects($this->once()) - ->method('updateActivity') - ->with($tk, $this->time); - $tk->setLastActivity($this->time - 200); - $tk->setToken('token'); - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - - $this->tokenProvider->updateTokenActivity($tk); - - $this->assertEquals($this->time, $tk->getLastActivity()); - } - - public function testUpdateTokenDebounce(): void { - $tk = new PublicKeyToken(); - $this->config->method('getSystemValueInt') - ->willReturnCallback(function ($value, $default) { - return $default; - }); - $tk->setLastActivity($this->time - 30); - - $this->mapper->expects($this->never()) - ->method('updateActivity') - ->with($tk, $this->time); - - $this->tokenProvider->updateTokenActivity($tk); - } - - public function testGetTokenByUser(): void { - $this->mapper->expects($this->once()) - ->method('getTokenByUser') - ->with('uid') - ->willReturn(['token']); - - $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid')); - } - - public function testGetPassword(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); - } - - public function testGetPasswordPasswordLessToken(): void { - $this->expectException(PasswordlessTokenException::class); - - $token = 'token1234'; - $tk = new PublicKeyToken(); - $tk->setPassword(null); - - $this->tokenProvider->getPassword($tk, $token); - } - - public function testGetPasswordInvalidToken(): void { - $this->expectException(InvalidTokenException::class); - - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->tokenProvider->getPassword($actual, 'wrongtoken'); - } - - public function testSetPassword(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->mapper->method('getTokenByUser') - ->with('user') - ->willReturn([$actual]); - - $newpass = 'newpass'; - $this->mapper->expects($this->once()) - ->method('update') - ->with($this->callback(function ($token) use ($newpass) { - return $newpass === $this->tokenProvider->getPassword($token, 'tokentokentokentokentoken'); - })); - - $this->tokenProvider->setPassword($actual, $token, $newpass); - - $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken')); - } - - public function testSetPasswordInvalidToken(): void { - $this->expectException(InvalidTokenException::class); - - $token = $this->createMock(IToken::class); - $tokenId = 'token123'; - $password = '123456'; - - $this->tokenProvider->setPassword($token, $tokenId, $password); - } - - public function testInvalidateToken(): void { - $calls = [ - [hash('sha512', 'token7' . '1f4h9s')], - [hash('sha512', 'token7')] - ]; - - $this->mapper->expects($this->exactly(2)) - ->method('invalidate') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->tokenProvider->invalidateToken('token7'); - } - - public function testInvalidateTokenById(): void { - $id = 123; - - $this->mapper->expects($this->once()) - ->method('getTokenById') - ->with($id); - - $this->tokenProvider->invalidateTokenById('uid', $id); - } - - public function testInvalidateOldTokens(): void { - $defaultSessionLifetime = 60 * 60 * 24; - $defaultRememberMeLifetime = 60 * 60 * 24 * 15; - $wipeTokenLifetime = 60 * 60 * 24 * 60; - $this->config->expects($this->exactly(4)) - ->method('getSystemValueInt') - ->willReturnMap([ - ['session_lifetime', $defaultSessionLifetime, 150], - ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300], - ['token_auth_wipe_token_retention', $wipeTokenLifetime, 500], - ['token_auth_token_retention', 60 * 60 * 24 * 365, 800], - ]); - - $calls = [ - [$this->time - 150, IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER], - [$this->time - 300, IToken::TEMPORARY_TOKEN, IToken::REMEMBER], - [$this->time - 500, IToken::WIPE_TOKEN, null], - [$this->time - 800, IToken::PERMANENT_TOKEN, null], - ]; - $this->mapper->expects($this->exactly(4)) - ->method('invalidateOld') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->tokenProvider->invalidateOldTokens(); - } - - public function testInvalidateLastUsedBefore(): void { - $this->mapper->expects($this->once()) - ->method('invalidateLastUsedBefore') - ->with('user', 946684800); - - $this->tokenProvider->invalidateLastUsedBefore('user', 946684800); - } - - public function testRenewSessionTokenWithoutPassword(): void { - $token = 'oldIdtokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = null; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->mapper - ->expects($this->once()) - ->method('getToken') - ->with(hash('sha512', 'oldIdtokentokentokentoken' . '1f4h9s')) - ->willReturn($oldToken); - $this->mapper - ->expects($this->once()) - ->method('insert') - ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) { - return $token->getUID() === $uid - && $token->getLoginName() === $user - && $token->getName() === $name - && $token->getType() === IToken::DO_NOT_REMEMBER - && $token->getLastActivity() === $this->time - && $token->getPassword() === null; - })); - $this->mapper - ->expects($this->once()) - ->method('delete') - ->with($this->callback(function ($token) use ($oldToken) { - return $token === $oldToken; - })); - - $this->tokenProvider->renewSessionToken('oldIdtokentokentokentoken', 'newIdtokentokentokentoken'); - } - - public function testRenewSessionTokenWithPassword(): void { - $token = 'oldIdtokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'password'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $this->mapper - ->expects($this->once()) - ->method('getToken') - ->with(hash('sha512', 'oldIdtokentokentokentoken' . '1f4h9s')) - ->willReturn($oldToken); - $this->mapper - ->expects($this->once()) - ->method('insert') - ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name): bool { - return $token->getUID() === $uid - && $token->getLoginName() === $user - && $token->getName() === $name - && $token->getType() === IToken::DO_NOT_REMEMBER - && $token->getLastActivity() === $this->time - && $token->getPassword() !== null - && $this->tokenProvider->getPassword($token, 'newIdtokentokentokentoken') === 'password'; - })); - $this->mapper - ->expects($this->once()) - ->method('delete') - ->with($this->callback(function ($token) use ($oldToken): bool { - return $token === $oldToken; - })); - - $this->tokenProvider->renewSessionToken('oldIdtokentokentokentoken', 'newIdtokentokentokentoken'); - } - - public function testGetToken(): void { - $token = new PublicKeyToken(); - $token->setToken('token'); - $token->setType(IToken::TEMPORARY_TOKEN); - - $this->config->method('getSystemValue') - ->with('secret') - ->willReturn('mysecret'); - - $this->mapper->method('getToken') - ->with( - $this->callback(function (string $token) { - return hash('sha512', 'unhashedTokentokentokentokentoken' . '1f4h9s') === $token; - }) - )->willReturn($token); - - $this->assertSame($token, $this->tokenProvider->getToken('unhashedTokentokentokentokentoken')); - } - - public function testGetInvalidToken(): void { - $this->expectException(InvalidTokenException::class); - - $calls = [ - 'unhashedTokentokentokentokentoken' . '1f4h9s', - 'unhashedTokentokentokentokentoken', - ]; - $this->mapper->expects($this->exactly(2)) - ->method('getToken') - ->willReturnCallback(function (string $token) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals(hash('sha512', $expected), $token); - throw new DoesNotExistException('nope'); - }); - - $this->tokenProvider->getToken('unhashedTokentokentokentokentoken'); - } - - public function testGetExpiredToken(): void { - $token = 'tokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'passme'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - $actual->setExpires(42); - - $this->mapper->method('getToken') - ->with( - $this->callback(function (string $token) { - return hash('sha512', 'tokentokentokentokentoken' . '1f4h9s') === $token; - }) - )->willReturn($actual); - - try { - $this->tokenProvider->getToken('tokentokentokentokentoken'); - $this->fail(); - } catch (ExpiredTokenException $e) { - $this->assertSame($actual, $e->getToken()); - } - } - - public function testGetTokenById(): void { - $token = $this->createMock(PublicKeyToken::class); - - $this->mapper->expects($this->once()) - ->method('getTokenById') - ->with($this->equalTo(42)) - ->willReturn($token); - - $this->assertSame($token, $this->tokenProvider->getTokenById(42)); - } - - public function testGetInvalidTokenById(): void { - $this->expectException(InvalidTokenException::class); - - $this->mapper->expects($this->once()) - ->method('getTokenById') - ->with($this->equalTo(42)) - ->willThrowException(new DoesNotExistException('nope')); - - $this->tokenProvider->getTokenById(42); - } - - public function testGetExpiredTokenById(): void { - $token = new PublicKeyToken(); - $token->setExpires(42); - - $this->mapper->expects($this->once()) - ->method('getTokenById') - ->with($this->equalTo(42)) - ->willReturn($token); - - try { - $this->tokenProvider->getTokenById(42); - $this->fail(); - } catch (ExpiredTokenException $e) { - $this->assertSame($token, $e->getToken()); - } - } - - public function testRotate(): void { - $token = 'oldtokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = 'password'; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $new = $this->tokenProvider->rotate($actual, 'oldtokentokentokentokentoken', 'newtokentokentokentokentoken'); - - $this->assertSame('password', $this->tokenProvider->getPassword($new, 'newtokentokentokentokentoken')); - } - - public function testRotateNoPassword(): void { - $token = 'oldtokentokentokentokentoken'; - $uid = 'user'; - $user = 'User'; - $password = null; - $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; - $type = IToken::PERMANENT_TOKEN; - - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); - - $oldPrivate = $actual->getPrivateKey(); - - $new = $this->tokenProvider->rotate($actual, 'oldtokentokentokentokentoken', 'newtokentokentokentokentoken'); - - $newPrivate = $new->getPrivateKey(); - - $this->assertNotSame($newPrivate, $oldPrivate); - $this->assertNull($new->getPassword()); - } - - public function testMarkPasswordInvalidInvalidToken(): void { - $token = $this->createMock(IToken::class); - - $this->expectException(InvalidTokenException::class); - - $this->tokenProvider->markPasswordInvalid($token, 'tokenId'); - } - - public function testMarkPasswordInvalid(): void { - $token = $this->createMock(PublicKeyToken::class); - $token->expects($this->once()) - ->method('getToken') - ->willReturn('token'); - - $token->expects($this->once()) - ->method('setPasswordInvalid') - ->with(true); - $this->mapper->expects($this->once()) - ->method('update') - ->with($token); - - $this->tokenProvider->markPasswordInvalid($token, 'tokenId'); - } - - public function testUpdatePasswords(): void { - $uid = 'myUID'; - $token1 = $this->tokenProvider->generateToken( - 'foobetokentokentokentoken', - $uid, - $uid, - 'bar', - 'random1', - IToken::PERMANENT_TOKEN, - IToken::REMEMBER); - $token2 = $this->tokenProvider->generateToken( - 'foobartokentokentokentoken', - $uid, - $uid, - 'bar', - 'random2', - IToken::PERMANENT_TOKEN, - IToken::REMEMBER); - $this->config->method('getSystemValueBool') - ->willReturnMap([ - ['auth.storeCryptedPassword', true, true], - ]); - - $this->mapper->method('hasExpiredTokens') - ->with($uid) - ->willReturn(true); - $this->mapper->expects($this->once()) - ->method('getTokenByUser') - ->with($uid) - ->willReturn([$token1, $token2]); - $this->mapper->expects($this->exactly(2)) - ->method('update') - ->with($this->callback(function (PublicKeyToken $t) use ($token1, $token2) { - return $t === $token1 || $t === $token2; - })); - - $this->tokenProvider->updatePasswords($uid, 'bar2'); - } -} --- a/tests/lib/Authentication/Token/PublicKeyTokenTest.php +++ /dev/null @@ -1,29 +0,0 @@ - false]; - $token = new PublicKeyToken(); - $token->setScope($scope); - $this->assertEquals(json_encode($scope), $token->getScope()); - $this->assertEquals($scope, $token->getScopeAsArray()); - } - - public function testDefaultScope(): void { - $scope = [IToken::SCOPE_FILESYSTEM => true]; - $token = new PublicKeyToken(); - $this->assertEquals($scope, $token->getScopeAsArray()); - } -} --- a/tests/lib/Authentication/Token/RemoteWipeTest.php +++ /dev/null @@ -1,173 +0,0 @@ -tokenProvider = $this->createMock(ITokenProvider::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->remoteWipe = new RemoteWipe( - $this->tokenProvider, - $this->eventDispatcher, - $this->logger - ); - } - - public function testMarkNonWipableTokenForWipe(): void { - $token = $this->createMock(IToken::class); - $result = $this->remoteWipe->markTokenForWipe($token); - $this->assertFalse($result); - } - - public function testMarkTokenForWipe(): void { - $token = $this->createMock(IWipeableToken::class); - $token->expects($this->once()) - ->method('wipe'); - - $this->tokenProvider->expects($this->once()) - ->method('updateToken') - ->with($token); - - $result = $this->remoteWipe->markTokenForWipe($token); - $this->assertTrue($result); - } - - public function testMarkAllTokensForWipeNoWipeableToken(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $token1 = $this->createMock(IToken::class); - $token2 = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getTokenByUser') - ->with('user123') - ->willReturn([$token1, $token2]); - - $result = $this->remoteWipe->markAllTokensForWipe($user); - - $this->assertFalse($result); - } - - public function testMarkAllTokensForWipe(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $token1 = $this->createMock(IToken::class); - $token2 = $this->createMock(IWipeableToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getTokenByUser') - ->with('user123') - ->willReturn([$token1, $token2]); - $token2->expects($this->once()) - ->method('wipe'); - $this->tokenProvider->expects($this->once()) - ->method('updateToken') - ->with($token2); - - $result = $this->remoteWipe->markAllTokensForWipe($user); - - $this->assertTrue($result); - } - - public function testStartWipingNotAWipeToken(): void { - $token = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('tk1') - ->willReturn($token); - $this->eventDispatcher->expects($this->never()) - ->method('dispatch'); - - $result = $this->remoteWipe->start('tk1'); - - $this->assertFalse($result); - } - - public function testStartWiping(): void { - $token = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('tk1') - ->willThrowException(new WipeTokenException($token)); - $this->eventDispatcher->expects($this->once()) - ->method('dispatch'); - $this->eventDispatcher->expects($this->once()) - ->method('dispatch') - ->with(RemoteWipeStarted::class, $this->equalTo(new RemoteWipeStarted($token))); - - $result = $this->remoteWipe->start('tk1'); - - $this->assertTrue($result); - } - - public function testFinishWipingNotAWipeToken(): void { - $token = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('tk1') - ->willReturn($token); - $this->eventDispatcher->expects($this->never()) - ->method('dispatch'); - - $result = $this->remoteWipe->finish('tk1'); - - $this->assertFalse($result); - } - - public function startFinishWiping() { - $token = $this->createMock(IToken::class); - $this->tokenProvider->expects($this->once()) - ->method('getToken') - ->with('tk1') - ->willThrowException(new WipeTokenException($token)); - $this->eventDispatcher->expects($this->once()) - ->method('dispatch'); - $this->tokenProvider->expects($this->once()) - ->method('invalidateToken') - ->with($token); - $this->eventDispatcher->expects($this->once()) - ->method('dispatch') - ->with(RemoteWipeFinished::class, $this->equalTo(new RemoteWipeFinished($token))); - - $result = $this->remoteWipe->finish('tk1'); - - $this->assertTrue($result); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php +++ /dev/null @@ -1,158 +0,0 @@ -dbConn = Server::get(IDBConnection::class); - $qb = $this->dbConn->getQueryBuilder(); - $q = $qb->delete(ProviderUserAssignmentDao::TABLE_NAME); - $q->executeStatement(); - - $this->dao = new ProviderUserAssignmentDao($this->dbConn); - } - - public function testGetState(): void { - $qb = $this->dbConn->getQueryBuilder(); - $q1 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ - 'provider_id' => $qb->createNamedParameter('twofactor_u2f'), - 'uid' => $qb->createNamedParameter('user123'), - 'enabled' => $qb->createNamedParameter(1), - ]); - $q1->executeStatement(); - $q2 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ - 'provider_id' => $qb->createNamedParameter('twofactor_totp'), - 'uid' => $qb->createNamedParameter('user123'), - 'enabled' => $qb->createNamedParameter(0), - ]); - $q2->executeStatement(); - $expected = [ - 'twofactor_u2f' => true, - 'twofactor_totp' => false, - ]; - - $state = $this->dao->getState('user123'); - - $this->assertEquals($expected, $state); - } - - public function testPersist(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $this->dao->persist('twofactor_totp', 'user123', 0); - - $q = $qb - ->select('*') - ->from(ProviderUserAssignmentDao::TABLE_NAME) - ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) - ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(0))); - $res = $q->executeQuery(); - $data = $res->fetchAllAssociative(); - $res->closeCursor(); - $this->assertCount(1, $data); - } - - public function testPersistTwice(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $this->dao->persist('twofactor_totp', 'user123', 0); - $this->dao->persist('twofactor_totp', 'user123', 1); - - $q = $qb - ->select('*') - ->from(ProviderUserAssignmentDao::TABLE_NAME) - ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) - ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(1))); - $res = $q->executeQuery(); - $data = $res->fetchAllAssociative(); - $res->closeCursor(); - - $this->assertCount(1, $data); - } - - public function testPersistSameStateTwice(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $this->dao->persist('twofactor_totp', 'user123', 1); - $this->dao->persist('twofactor_totp', 'user123', 1); - - $q = $qb - ->select('*') - ->from(ProviderUserAssignmentDao::TABLE_NAME) - ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) - ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(1))); - $res = $q->executeQuery(); - $data = $res->fetchAllAssociative(); - $res->closeCursor(); - - $this->assertCount(1, $data); - } - - public function testDeleteByUser(): void { - $this->dao->persist('twofactor_fail', 'user1', 1); - $this->dao->persist('twofactor_u2f', 'user1', 1); - $this->dao->persist('twofactor_fail', 'user2', 0); - $this->dao->persist('twofactor_u2f', 'user2', 0); - - $deleted = $this->dao->deleteByUser('user1'); - - $this->assertEquals( - [ - [ - 'uid' => 'user1', - 'provider_id' => 'twofactor_fail', - 'enabled' => true, - ], - [ - 'uid' => 'user1', - 'provider_id' => 'twofactor_u2f', - 'enabled' => true, - ], - ], - $deleted - ); - $statesUser1 = $this->dao->getState('user1'); - $statesUser2 = $this->dao->getState('user2'); - $this->assertCount(0, $statesUser1); - $this->assertCount(2, $statesUser2); - } - - public function testDeleteAll(): void { - $this->dao->persist('twofactor_fail', 'user1', 1); - $this->dao->persist('twofactor_u2f', 'user1', 1); - $this->dao->persist('twofactor_fail', 'user2', 0); - $this->dao->persist('twofactor_u2f', 'user1', 0); - - $this->dao->deleteAll('twofactor_fail'); - - $statesUser1 = $this->dao->getState('user1'); - $statesUser2 = $this->dao->getState('user2'); - $this->assertCount(1, $statesUser1); - $this->assertCount(0, $statesUser2); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/EnforcementStateTest.php +++ /dev/null @@ -1,51 +0,0 @@ -assertTrue($state->isEnforced()); - } - - public function testGetEnforcedGroups(): void { - $state = new EnforcementState(true, ['twofactorers']); - - $this->assertEquals(['twofactorers'], $state->getEnforcedGroups()); - } - - public function testGetExcludedGroups(): void { - $state = new EnforcementState(true, [], ['yoloers']); - - $this->assertEquals(['yoloers'], $state->getExcludedGroups()); - } - - public function testJsonSerialize(): void { - $state = new EnforcementState(true, ['twofactorers'], ['yoloers']); - $expected = [ - 'enforced' => true, - 'enforcedGroups' => ['twofactorers'], - 'excludedGroups' => ['yoloers'], - ]; - - $json = $state->jsonSerialize(); - - $this->assertEquals($expected, $json); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php +++ /dev/null @@ -1,760 +0,0 @@ -user = $this->createMock(IUser::class); - $this->user->expects($this->any()) - ->method('getUID') - ->willReturn('user-uid'); - $this->providerLoader = $this->createMock(ProviderLoader::class); - $this->providerRegistry = $this->createMock(IRegistry::class); - $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); - $this->session = $this->createMock(ISession::class); - $this->config = $this->createMock(IConfig::class); - $this->activityManager = $this->createMock(IManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->tokenProvider = $this->createMock(TokenProvider::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - - $this->manager = new Manager( - $this->providerLoader, - $this->providerRegistry, - $this->mandatoryTwoFactor, - $this->session, - $this->config, - $this->activityManager, - $this->logger, - $this->tokenProvider, - $this->timeFactory, - $this->dispatcher, - ); - - $this->fakeProvider = $this->createMock(IProvider::class); - $this->fakeProvider->method('getId')->willReturn('email'); - - $this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); - $this->backupProvider->method('getId')->willReturn('backup_codes'); - $this->backupProvider->method('isTwoFactorAuthEnabledForUser')->willReturn(true); - } - - private function prepareNoProviders() { - $this->providerLoader->method('getProviders') - ->with($this->user) - ->willReturn([]); - } - - private function prepareProviders() { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->with($this->user) - ->willReturn([ - $this->fakeProvider->getId() => true, - ]); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([$this->fakeProvider]); - } - - private function prepareProvidersWitBackupProvider() { - $this->providerLoader->method('getProviders') - ->with($this->user) - ->willReturn([ - $this->fakeProvider, - $this->backupProvider, - ]); - } - - public function testIsTwoFactorAuthenticatedEnforced(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(true); - - $enabled = $this->manager->isTwoFactorAuthenticated($this->user); - - $this->assertTrue($enabled); - } - - public function testIsTwoFactorAuthenticatedNoProviders(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->willReturn([]); // No providers registered - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->willReturn([]); // No providers loadable - - $this->assertFalse($this->manager->isTwoFactorAuthenticated($this->user)); - } - - public function testIsTwoFactorAuthenticatedOnlyBackupCodes(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->willReturn([ - 'backup_codes' => true, - ]); - $backupCodesProvider = $this->createMock(IProvider::class); - $backupCodesProvider - ->method('getId') - ->willReturn('backup_codes'); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->willReturn([ - $backupCodesProvider, - ]); - - $this->assertFalse($this->manager->isTwoFactorAuthenticated($this->user)); - } - - public function testIsTwoFactorAuthenticatedFailingProviders(): void { - $this->mandatoryTwoFactor->expects($this->once()) - ->method('isEnforcedFor') - ->with($this->user) - ->willReturn(false); - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->willReturn([ - 'twofactor_totp' => true, - 'twofactor_u2f' => false, - ]); // Two providers registered, but … - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->willReturn([]); // … none of them is able to load, however … - - // … 2FA is still enforced - $this->assertTrue($this->manager->isTwoFactorAuthenticated($this->user)); - } - - public static function providerStatesFixData(): array { - return [ - [false, false], - [true, true], - ]; - } - - /** - * If the 2FA registry has not been populated when a user logs in, - * the 2FA manager has to first fix the state before it checks for - * enabled providers. - * - * If any of these providers is active, 2FA is enabled - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerStatesFixData')] - public function testIsTwoFactorAuthenticatedFixesProviderStates(bool $providerEnabled, bool $expected): void { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->willReturn([]); // Nothing registered yet - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->willReturn([ - $this->fakeProvider - ]); - $this->fakeProvider->expects($this->once()) - ->method('isTwoFactorAuthEnabledForUser') - ->with($this->user) - ->willReturn($providerEnabled); - if ($providerEnabled) { - $this->providerRegistry->expects($this->once()) - ->method('enableProviderFor') - ->with( - $this->fakeProvider, - $this->user - ); - } else { - $this->providerRegistry->expects($this->once()) - ->method('disableProviderFor') - ->with( - $this->fakeProvider, - $this->user - ); - } - - $this->assertEquals($expected, $this->manager->isTwoFactorAuthenticated($this->user)); - } - - public function testGetProvider(): void { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->with($this->user) - ->willReturn([ - $this->fakeProvider->getId() => true, - ]); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([$this->fakeProvider]); - - $provider = $this->manager->getProvider($this->user, $this->fakeProvider->getId()); - - $this->assertSame($this->fakeProvider, $provider); - } - - public function testGetInvalidProvider(): void { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->with($this->user) - ->willReturn([]); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([]); - - $provider = $this->manager->getProvider($this->user, 'nonexistent'); - - $this->assertNull($provider); - } - - public function testGetLoginSetupProviders(): void { - $provider1 = $this->createMock(IProvider::class); - $provider2 = $this->createMock(IActivatableAtLogin::class); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([ - $provider1, - $provider2, - ]); - - $providers = $this->manager->getLoginSetupProviders($this->user); - - $this->assertCount(1, $providers); - $this->assertSame($provider2, reset($providers)); - } - - public function testGetProviders(): void { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->with($this->user) - ->willReturn([ - $this->fakeProvider->getId() => true, - ]); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([$this->fakeProvider]); - $expectedProviders = [ - 'email' => $this->fakeProvider, - ]; - - $providerSet = $this->manager->getProviderSet($this->user); - $providers = $providerSet->getProviders(); - - $this->assertEquals($expectedProviders, $providers); - $this->assertFalse($providerSet->isProviderMissing()); - } - - public function testGetProvidersOneMissing(): void { - $this->providerRegistry->expects($this->once()) - ->method('getProviderStates') - ->with($this->user) - ->willReturn([ - $this->fakeProvider->getId() => true, - ]); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($this->user) - ->willReturn([]); - $expectedProviders = [ - 'email' => $this->fakeProvider, - ]; - - $providerSet = $this->manager->getProviderSet($this->user); - - $this->assertTrue($providerSet->isProviderMissing()); - } - - public function testVerifyChallenge(): void { - $this->prepareProviders(); - - $challenge = 'passme'; - $event = $this->createMock(IEvent::class); - $this->fakeProvider->expects($this->once()) - ->method('verifyChallenge') - ->with($this->user, $challenge) - ->willReturn(true); - $this->session->expects($this->once()) - ->method('get') - ->with('two_factor_remember_login') - ->willReturn(false); - - $calls = [ - ['two_factor_auth_uid'], - ['two_factor_remember_login'], - ]; - $this->session->expects($this->exactly(2)) - ->method('remove') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->session->expects($this->once()) - ->method('set') - ->with(Manager::SESSION_UID_DONE, $this->user->getUID()); - $this->session->method('getId') - ->willReturn('mysessionid'); - $this->activityManager->expects($this->once()) - ->method('generateEvent') - ->willReturn($event); - $event->expects($this->once()) - ->method('setApp') - ->with($this->equalTo('core')) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setType') - ->with($this->equalTo('security')) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setAuthor') - ->with($this->equalTo($this->user->getUID())) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setAffectedUser') - ->with($this->equalTo($this->user->getUID())) - ->willReturnSelf(); - $this->fakeProvider - ->method('getDisplayName') - ->willReturn('Fake 2FA'); - $event->expects($this->once()) - ->method('setSubject') - ->with($this->equalTo('twofactor_success'), $this->equalTo([ - 'provider' => 'Fake 2FA', - ])) - ->willReturnSelf(); - $token = $this->createMock(IToken::class); - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willReturn($token); - $token->method('getId') - ->willReturn(42); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with($this->user->getUID(), 'login_token_2fa', '42'); - - $result = $this->manager->verifyChallenge('email', $this->user, $challenge); - - $this->assertTrue($result); - } - - public function testVerifyChallengeInvalidProviderId(): void { - $this->prepareProviders(); - - $challenge = 'passme'; - $this->fakeProvider->expects($this->never()) - ->method('verifyChallenge') - ->with($this->user, $challenge); - $this->session->expects($this->never()) - ->method('remove'); - - $this->assertFalse($this->manager->verifyChallenge('dontexist', $this->user, $challenge)); - } - - public function testVerifyInvalidChallenge(): void { - $this->prepareProviders(); - - $challenge = 'dontpassme'; - $event = $this->createMock(IEvent::class); - $this->fakeProvider->expects($this->once()) - ->method('verifyChallenge') - ->with($this->user, $challenge) - ->willReturn(false); - $this->session->expects($this->never()) - ->method('remove'); - $this->activityManager->expects($this->once()) - ->method('generateEvent') - ->willReturn($event); - $event->expects($this->once()) - ->method('setApp') - ->with($this->equalTo('core')) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setType') - ->with($this->equalTo('security')) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setAuthor') - ->with($this->equalTo($this->user->getUID())) - ->willReturnSelf(); - $event->expects($this->once()) - ->method('setAffectedUser') - ->with($this->equalTo($this->user->getUID())) - ->willReturnSelf(); - $this->fakeProvider - ->method('getDisplayName') - ->willReturn('Fake 2FA'); - $event->expects($this->once()) - ->method('setSubject') - ->with($this->equalTo('twofactor_failed'), $this->equalTo([ - 'provider' => 'Fake 2FA', - ])) - ->willReturnSelf(); - - $this->assertFalse($this->manager->verifyChallenge('email', $this->user, $challenge)); - } - - public function testNeedsSecondFactor(): void { - $user = $this->createMock(IUser::class); - - $calls = [ - ['app_password'], - ['two_factor_auth_uid'], - [Manager::SESSION_UID_DONE], - ]; - $this->session->expects($this->exactly(3)) - ->method('exists') - ->willReturnCallback(function () use (&$calls) { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - return false; - }); - - $this->session->method('getId') - ->willReturn('mysessionid'); - $token = $this->createMock(IToken::class); - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willReturn($token); - $token->method('getId') - ->willReturn(42); - - $user->method('getUID') - ->willReturn('user'); - $this->config->method('getUserKeys') - ->with('user', 'login_token_2fa') - ->willReturn([ - '42' - ]); - - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->providerLoader, - $this->providerRegistry, - $this->mandatoryTwoFactor, - $this->session, - $this->config, - $this->activityManager, - $this->logger, - $this->tokenProvider, - $this->timeFactory, - $this->dispatcher, - ]) - ->onlyMethods(['isTwoFactorAuthenticated'])// Do not actually load the apps - ->getMock(); - - $manager->method('isTwoFactorAuthenticated') - ->with($user) - ->willReturn(true); - - $this->assertTrue($manager->needsSecondFactor($user)); - } - - public function testNeedsSecondFactorUserIsNull(): void { - $user = null; - $this->session->expects($this->never()) - ->method('exists'); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testNeedsSecondFactorWithNoProviderAvailableAnymore(): void { - $this->prepareNoProviders(); - - $user = null; - $this->session->expects($this->never()) - ->method('exists') - ->with('two_factor_auth_uid') - ->willReturn(true); - $this->session->expects($this->never()) - ->method('remove') - ->with('two_factor_auth_uid'); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testPrepareTwoFactorLogin(): void { - $calls = [ - ['two_factor_auth_uid', $this->user->getUID()], - ['two_factor_remember_login', true], - ]; - $this->session->expects($this->exactly(2)) - ->method('set') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->session->method('getId') - ->willReturn('mysessionid'); - $token = $this->createMock(IToken::class); - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willReturn($token); - $token->method('getId') - ->willReturn(42); - - $this->timeFactory->method('getTime') - ->willReturn(1337); - - $this->config->method('setUserValue') - ->with($this->user->getUID(), 'login_token_2fa', '42', '1337'); - - $this->manager->prepareTwoFactorLogin($this->user, true); - } - - public function testPrepareTwoFactorLoginDontRemember(): void { - $calls = [ - ['two_factor_auth_uid', $this->user->getUID()], - ['two_factor_remember_login', false], - ]; - $this->session->expects($this->exactly(2)) - ->method('set') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->session->method('getId') - ->willReturn('mysessionid'); - $token = $this->createMock(IToken::class); - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willReturn($token); - $token->method('getId') - ->willReturn(42); - - $this->timeFactory->method('getTime') - ->willReturn(1337); - - $this->config->method('setUserValue') - ->with($this->user->getUID(), 'login_token_2fa', '42', '1337'); - - $this->manager->prepareTwoFactorLogin($this->user, false); - } - - public function testNeedsSecondFactorSessionAuth(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('user'); - - $this->session->method('exists') - ->willReturnCallback(function ($var) { - if ($var === Manager::SESSION_UID_KEY) { - return false; - } elseif ($var === 'app_password') { - return false; - } elseif ($var === 'app_api') { - return false; - } - return true; - }); - $this->session->method('get') - ->willReturnCallback(function ($var) { - if ($var === Manager::SESSION_UID_KEY) { - return 'user'; - } elseif ($var === 'app_api') { - return true; - } - return null; - }); - $this->session->expects($this->once()) - ->method('get') - ->willReturnMap([ - [Manager::SESSION_UID_DONE, 'user'], - ['app_api', true] - ]); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testNeedsSecondFactorSessionAuthFailDBPass(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('user'); - - $this->session->method('exists') - ->willReturn(false); - $this->session->method('getId') - ->willReturn('mysessionid'); - - $token = $this->createMock(IToken::class); - $token->method('getId') - ->willReturn(40); - - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willReturn($token); - - $this->config->method('getUserKeys') - ->with('user', 'login_token_2fa') - ->willReturn([ - '42', '43', '44' - ]); - - $this->session->expects($this->once()) - ->method('set') - ->with(Manager::SESSION_UID_DONE, 'user'); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testNeedsSecondFactorInvalidToken(): void { - $this->prepareNoProviders(); - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('user'); - - $this->session->method('exists') - ->willReturn(false); - $this->session->method('getId') - ->willReturn('mysessionid'); - - $this->tokenProvider->method('getToken') - ->with('mysessionid') - ->willThrowException(new InvalidTokenException()); - - $this->config->method('getUserKeys')->willReturn([]); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testNeedsSecondFactorAppPassword(): void { - $user = $this->createMock(IUser::class); - $this->session->method('exists') - ->willReturnMap([ - ['app_password', true], - ['app_api', true] - ]); - - $this->assertFalse($this->manager->needsSecondFactor($user)); - } - - public function testClearTwoFactorPending() { - $this->config->method('getUserKeys') - ->with('theUserId', 'login_token_2fa') - ->willReturn([ - '42', '43', '44' - ]); - - $deleteUserValueCalls = [ - ['theUserId', 'login_token_2fa', '42'], - ['theUserId', 'login_token_2fa', '43'], - ['theUserId', 'login_token_2fa', '44'], - ]; - $this->config->expects($this->exactly(3)) - ->method('deleteUserValue') - ->willReturnCallback(function () use (&$deleteUserValueCalls): void { - $expected = array_shift($deleteUserValueCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $invalidateCalls = [ - ['theUserId', 42], - ['theUserId', 43], - ['theUserId', 44], - ]; - $this->tokenProvider->expects($this->exactly(3)) - ->method('invalidateTokenById') - ->willReturnCallback(function () use (&$invalidateCalls): void { - $expected = array_shift($invalidateCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->manager->clearTwoFactorPending('theUserId'); - } - - public function testClearTwoFactorPendingTokenDoesNotExist() { - $this->config->method('getUserKeys') - ->with('theUserId', 'login_token_2fa') - ->willReturn([ - '42', '43', '44' - ]); - - $deleteUserValueCalls = [ - ['theUserId', 'login_token_2fa', '42'], - ['theUserId', 'login_token_2fa', '43'], - ['theUserId', 'login_token_2fa', '44'], - ]; - $this->config->expects($this->exactly(3)) - ->method('deleteUserValue') - ->willReturnCallback(function () use (&$deleteUserValueCalls): void { - $expected = array_shift($deleteUserValueCalls); - $this->assertEquals($expected, func_get_args()); - }); - - $invalidateCalls = [ - ['theUserId', 42], - ['theUserId', 43], - ['theUserId', 44], - ]; - $this->tokenProvider->expects($this->exactly(3)) - ->method('invalidateTokenById') - ->willReturnCallback(function ($user, $tokenId) use (&$invalidateCalls): void { - $expected = array_shift($invalidateCalls); - $this->assertEquals($expected, func_get_args()); - if ($tokenId === 43) { - throw new DoesNotExistException('token does not exist'); - } - }); - - $this->manager->clearTwoFactorPending('theUserId'); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/MandatoryTwoFactorTest.php +++ /dev/null @@ -1,180 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->groupManager = $this->createMock(IGroupManager::class); - - $this->mandatoryTwoFactor = new MandatoryTwoFactor($this->config, $this->groupManager); - } - - public function testIsNotEnforced(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'false'], - ['twofactor_enforced_groups', [], []], - ['twofactor_enforced_excluded_groups', [], []], - ]); - - $state = $this->mandatoryTwoFactor->getState(); - - $this->assertFalse($state->isEnforced()); - } - - public function testIsEnforced(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'true'], - ['twofactor_enforced_groups', [], []], - ['twofactor_enforced_excluded_groups', [], []], - ]); - - $state = $this->mandatoryTwoFactor->getState(); - - $this->assertTrue($state->isEnforced()); - } - - public function testIsNotEnforcedForAnybody(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'false'], - ['twofactor_enforced_groups', [], []], - ['twofactor_enforced_excluded_groups', [], []], - ]); - - $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); - - $this->assertFalse($isEnforced); - } - - public function testIsEnforcedForAGroupMember(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'true'], - ['twofactor_enforced_groups', [], ['twofactorers']], - ['twofactor_enforced_excluded_groups', [], []], - ]); - $this->groupManager->method('isInGroup') - ->willReturnCallback(function ($user, $group) { - return $user === 'user123' && $group === 'twofactorers'; - }); - - $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); - - $this->assertTrue($isEnforced); - } - - public function testIsEnforcedForOtherGroups(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'true'], - ['twofactor_enforced_groups', [], ['twofactorers']], - ['twofactor_enforced_excluded_groups', [], []], - ]); - $this->groupManager->method('isInGroup') - ->willReturn(false); - - $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); - - $this->assertFalse($isEnforced); - } - - public function testIsEnforcedButMemberOfExcludedGroup(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user123'); - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false', 'true'], - ['twofactor_enforced_groups', [], []], - ['twofactor_enforced_excluded_groups', [], ['yoloers']], - ]); - $this->groupManager->method('isInGroup') - ->willReturnCallback(function ($user, $group) { - return $user === 'user123' && $group === 'yoloers'; - }); - - $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); - - $this->assertFalse($isEnforced); - } - - public function testSetEnforced(): void { - $this->config - ->expects($this->exactly(3)) - ->method('setSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'true'], - ['twofactor_enforced_groups', []], - ['twofactor_enforced_excluded_groups', []], - ]); - - $this->mandatoryTwoFactor->setState(new EnforcementState(true)); - } - - public function testSetEnforcedForGroups(): void { - $this->config - ->expects($this->exactly(3)) - ->method('setSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'true'], - ['twofactor_enforced_groups', ['twofactorers']], - ['twofactor_enforced_excluded_groups', ['yoloers']], - ]); - - $this->mandatoryTwoFactor->setState(new EnforcementState(true, ['twofactorers'], ['yoloers'])); - } - - public function testSetNotEnforced(): void { - $this->config - ->expects($this->exactly(3)) - ->method('setSystemValue') - ->willReturnMap([ - ['twofactor_enforced', 'false'], - ['twofactor_enforced_groups', []], - ['twofactor_enforced_excluded_groups', []], - ]); - - $this->mandatoryTwoFactor->setState(new EnforcementState(false)); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php +++ /dev/null @@ -1,120 +0,0 @@ -appManager = $this->createMock(IAppManager::class); - $this->user = $this->createMock(IUser::class); - - $this->registrationContext = $this->createMock(RegistrationContext::class); - $coordinator = $this->createMock(Coordinator::class); - $coordinator->method('getRegistrationContext') - ->willReturn($this->registrationContext); - - $this->loader = new ProviderLoader($this->appManager, $coordinator); - } - - public function testFailHardIfProviderCanNotBeLoaded(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Could not load two-factor auth provider \\OCA\\MyFaulty2faApp\\DoesNotExist'); - - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($this->user) - ->willReturn(['mail', 'twofactor_totp']); - $this->appManager - ->method('getAppInfo') - ->willReturnMap([ - ['mail', false, null, []], - ['twofactor_totp', false, null, [ - 'two-factor-providers' => [ - '\\OCA\\MyFaulty2faApp\\DoesNotExist', - ], - ]], - ]); - - $this->loader->getProviders($this->user); - } - - public function testGetProviders(): void { - $provider = $this->createMock(IProvider::class); - $provider->method('getId')->willReturn('test'); - \OC::$server->registerService('\\OCA\\TwoFactorTest\\Provider', function () use ($provider) { - return $provider; - }); - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($this->user) - ->willReturn(['twofactor_test']); - $this->appManager - ->method('getAppInfo') - ->with('twofactor_test') - ->willReturn(['two-factor-providers' => [ - '\\OCA\\TwoFactorTest\\Provider', - ]]); - - $providers = $this->loader->getProviders($this->user); - - $this->assertCount(1, $providers); - $this->assertArrayHasKey('test', $providers); - $this->assertSame($provider, $providers['test']); - } - - public function testGetProvidersBootstrap(): void { - $provider = $this->createMock(IProvider::class); - $provider->method('getId')->willReturn('test'); - - \OC::$server->registerService('\\OCA\\TwoFactorTest\\Provider', function () use ($provider) { - return $provider; - }); - - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($this->user) - ->willReturn([]); - - $this->registrationContext->method('getTwoFactorProviders') - ->willReturn([ - new ServiceRegistration('twofactor_test', '\\OCA\\TwoFactorTest\\Provider') - ]); - - $providers = $this->loader->getProviders($this->user); - - $this->assertCount(1, $providers); - $this->assertArrayHasKey('test', $providers); - $this->assertSame($provider, $providers['test']); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/ProviderManagerTest.php +++ /dev/null @@ -1,135 +0,0 @@ -providerLoader = $this->createMock(ProviderLoader::class); - $this->registry = $this->createMock(IRegistry::class); - - $this->providerManager = new ProviderManager( - $this->providerLoader, - $this->registry - ); - } - - public function testTryEnableInvalidProvider(): void { - $this->expectException(InvalidProviderException::class); - - $user = $this->createMock(IUser::class); - $this->providerManager->tryEnableProviderFor('none', $user); - } - - public function testTryEnableUnsupportedProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([ - 'u2f' => $provider, - ]); - $this->registry->expects($this->never()) - ->method('enableProviderFor'); - - $res = $this->providerManager->tryEnableProviderFor('u2f', $user); - - $this->assertFalse($res); - } - - public function testTryEnableProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IActivatableByAdmin::class); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([ - 'u2f' => $provider, - ]); - $provider->expects($this->once()) - ->method('enableFor') - ->with($user); - $this->registry->expects($this->once()) - ->method('enableProviderFor') - ->with($provider, $user); - - $res = $this->providerManager->tryEnableProviderFor('u2f', $user); - - $this->assertTrue($res); - } - - public function testTryDisableInvalidProvider(): void { - $this->expectException(InvalidProviderException::class); - - $user = $this->createMock(IUser::class); - $this->providerManager->tryDisableProviderFor('none', $user); - } - - public function testTryDisableUnsupportedProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([ - 'u2f' => $provider, - ]); - $this->registry->expects($this->never()) - ->method('disableProviderFor'); - - $res = $this->providerManager->tryDisableProviderFor('u2f', $user); - - $this->assertFalse($res); - } - - public function testTryDisableProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IDeactivatableByAdmin::class); - $this->providerLoader->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([ - 'u2f' => $provider, - ]); - $provider->expects($this->once()) - ->method('disableFor') - ->with($user); - $this->registry->expects($this->once()) - ->method('disableProviderFor') - ->with($provider, $user); - - $res = $this->providerManager->tryDisableProviderFor('u2f', $user); - - $this->assertTrue($res); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php +++ /dev/null @@ -1,75 +0,0 @@ -createMock(IProvider::class); - $p1->method('getId')->willReturn('p1'); - $p2 = $this->createMock(IProvider::class); - $p2->method('getId')->willReturn('p2'); - $expected = [ - 'p1' => $p1, - 'p2' => $p2, - ]; - - $set = new ProviderSet([$p2, $p1], false); - - $this->assertEquals($expected, $set->getProviders()); - } - - public function testGet3rdPartyProviders(): void { - $p1 = $this->createMock(IProvider::class); - $p1->method('getId')->willReturn('p1'); - $p2 = $this->createMock(IProvider::class); - $p2->method('getId')->willReturn('p2'); - $p3 = $this->createMock(BackupCodesProvider::class); - $p3->method('getId')->willReturn('p3'); - $expected = [ - 'p1' => $p1, - 'p2' => $p2, - ]; - - $set = new ProviderSet([$p2, $p1], false); - - $this->assertEquals($expected, $set->getPrimaryProviders()); - } - - public function testGetProvider(): void { - $p1 = $this->createMock(IProvider::class); - $p1->method('getId')->willReturn('p1'); - - $set = new ProviderSet([$p1], false); - $provider = $set->getProvider('p1'); - - $this->assertEquals($p1, $provider); - } - - public function testGetProviderNotFound(): void { - $set = new ProviderSet([], false); - $provider = $set->getProvider('p1'); - - $this->assertNull($provider); - } - - public function testIsProviderMissing(): void { - $set = new ProviderSet([], true); - - $this->assertTrue($set->isProviderMissing()); - } -} --- a/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php +++ /dev/null @@ -1,164 +0,0 @@ -dao = $this->createMock(ProviderUserAssignmentDao::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - - $this->registry = new Registry($this->dao, $this->dispatcher); - } - - public function testGetProviderStates(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->once())->method('getUID')->willReturn('user123'); - $state = [ - 'twofactor_totp' => true, - ]; - $this->dao->expects($this->once())->method('getState')->willReturn($state); - - $actual = $this->registry->getProviderStates($user); - - $this->assertEquals($state, $actual); - } - - public function testEnableProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $user->expects($this->once())->method('getUID')->willReturn('user123'); - $provider->expects($this->once())->method('getId')->willReturn('p1'); - $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', - true); - - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with( - $this->equalTo(IRegistry::EVENT_PROVIDER_ENABLED), - $this->callback(function (RegistryEvent $e) use ($user, $provider) { - return $e->getUser() === $user && $e->getProvider() === $provider; - }) - ); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new TwoFactorProviderForUserRegistered( - $user, - $provider, - )); - - $this->registry->enableProviderFor($provider, $user); - } - - public function testEnableStatelessProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IStatelessProvider::class); - - $this->dao->expects($this->never())->method('persist'); - - $this->dispatcher->expects($this->never())->method('dispatch'); - $this->dispatcher->expects($this->never())->method('dispatchTyped'); - - $this->registry->enableProviderFor($provider, $user); - } - - public function testDisableProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $user->expects($this->once())->method('getUID')->willReturn('user123'); - $provider->expects($this->once())->method('getId')->willReturn('p1'); - $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', - false); - - $this->dispatcher->expects($this->once()) - ->method('dispatch') - ->with( - $this->equalTo(IRegistry::EVENT_PROVIDER_DISABLED), - $this->callback(function (RegistryEvent $e) use ($user, $provider) { - return $e->getUser() === $user && $e->getProvider() === $provider; - }) - ); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new TwoFactorProviderForUserUnregistered( - $user, - $provider, - )); - - $this->registry->disableProviderFor($provider, $user); - } - - public function testDisableStatelessProvider(): void { - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IStatelessProvider::class); - - $this->dao->expects($this->never())->method('persist'); - - $this->dispatcher->expects($this->never())->method('dispatch'); - $this->dispatcher->expects($this->never())->method('dispatchTyped'); - - $this->registry->disableProviderFor($provider, $user); - } - - public function testDeleteUserData(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->once())->method('getUID')->willReturn('user123'); - $this->dao->expects($this->once()) - ->method('deleteByUser') - ->with('user123') - ->willReturn([ - [ - 'provider_id' => 'twofactor_u2f', - ] - ]); - - $calls = [ - [new TwoFactorProviderDisabled('twofactor_u2f')], - [new TwoFactorProviderUserDeleted($user, 'twofactor_u2f')], - ]; - $this->dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function () use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - }); - - $this->registry->deleteUserData($user); - } - - public function testCleanUp(): void { - $this->dao->expects($this->once()) - ->method('deleteAll') - ->with('twofactor_u2f'); - - $this->registry->cleanUp('twofactor_u2f'); - } -} --- a/tests/lib/Avatar/AvatarManagerTest.php +++ /dev/null @@ -1,336 +0,0 @@ -userSession = $this->createMock(IUserSession::class); - $this->userManager = $this->createMock(Manager::class); - $this->appData = $this->createMock(IAppData::class); - $this->l10n = $this->createMock(IL10N::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->config = $this->createMock(IConfig::class); - $this->accountManager = $this->createMock(IAccountManager::class); - $this->knownUserService = $this->createMock(KnownUserService::class); - $this->cloudIdManager = $this->createMock(ICloudIdManager::class); - - $this->avatarManager = new AvatarManager( - $this->userSession, - $this->userManager, - $this->appData, - $this->l10n, - $this->logger, - $this->config, - $this->accountManager, - $this->knownUserService, - $this->cloudIdManager - ); - } - - public function testGetAvatarForSelf(): void { - $user = $this->createMock(User::class); - $user - ->expects($this->any()) - ->method('getUID') - ->willReturn('valid-user'); - - $user - ->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - // requesting user - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('valid-user') - ->willReturn($user); - - $account = $this->createMock(IAccount::class); - $this->accountManager->expects($this->once()) - ->method('getAccount') - ->with($user) - ->willReturn($account); - - $property = $this->createMock(IAccountProperty::class); - $account->expects($this->once()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_AVATAR) - ->willReturn($property); - - $property->expects($this->once()) - ->method('getScope') - ->willReturn(IAccountManager::SCOPE_PRIVATE); - - $this->knownUserService->expects($this->any()) - ->method('isKnownToUser') - ->with('valid-user', 'valid-user') - ->willReturn(true); - - $folder = $this->createMock(ISimpleFolder::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('valid-user') - ->willReturn($folder); - - $expected = new UserAvatar($folder, $this->l10n, $user, $this->logger, $this->config); - $this->assertEquals($expected, $this->avatarManager->getAvatar('valid-user')); - } - - public function testGetAvatarValidUserDifferentCasing(): void { - $user = $this->createMock(User::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('vaLid-USER') - ->willReturn($user); - - $user->expects($this->once()) - ->method('getUID') - ->willReturn('valid-user'); - - $user - ->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($user); - - $folder = $this->createMock(ISimpleFolder::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('valid-user') - ->willReturn($folder); - - $account = $this->createMock(IAccount::class); - $this->accountManager->expects($this->once()) - ->method('getAccount') - ->with($user) - ->willReturn($account); - - $property = $this->createMock(IAccountProperty::class); - $account->expects($this->once()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_AVATAR) - ->willReturn($property); - - $property->expects($this->once()) - ->method('getScope') - ->willReturn(IAccountManager::SCOPE_FEDERATED); - - $expected = new UserAvatar($folder, $this->l10n, $user, $this->logger, $this->config); - $this->assertEquals($expected, $this->avatarManager->getAvatar('vaLid-USER')); - } - - public static function dataGetAvatarScopes(): array { - return [ - // public access cannot see real avatar - [IAccountManager::SCOPE_PRIVATE, true, false, true], - // unknown users cannot see real avatar - [IAccountManager::SCOPE_PRIVATE, false, false, true], - // known users can see real avatar - [IAccountManager::SCOPE_PRIVATE, false, true, false], - [IAccountManager::SCOPE_LOCAL, false, false, false], - [IAccountManager::SCOPE_LOCAL, true, false, false], - [IAccountManager::SCOPE_FEDERATED, false, false, false], - [IAccountManager::SCOPE_FEDERATED, true, false, false], - [IAccountManager::SCOPE_PUBLISHED, false, false, false], - [IAccountManager::SCOPE_PUBLISHED, true, false, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetAvatarScopes')] - public function testGetAvatarScopes($avatarScope, $isPublicCall, $isKnownUser, $expectedPlaceholder): void { - if ($isPublicCall) { - $requestingUser = null; - } else { - $requestingUser = $this->createMock(IUser::class); - $requestingUser->method('getUID')->willReturn('requesting-user'); - } - - // requesting user - $this->userSession->expects($this->once()) - ->method('getUser') - ->willReturn($requestingUser); - - $user = $this->createMock(User::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('valid-user'); - - $user - ->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('valid-user') - ->willReturn($user); - - $account = $this->createMock(IAccount::class); - $this->accountManager->expects($this->once()) - ->method('getAccount') - ->with($user) - ->willReturn($account); - - $property = $this->createMock(IAccountProperty::class); - $account->expects($this->once()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_AVATAR) - ->willReturn($property); - - $property->expects($this->once()) - ->method('getScope') - ->willReturn($avatarScope); - - $folder = $this->createMock(ISimpleFolder::class); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('valid-user') - ->willReturn($folder); - - if (!$isPublicCall) { - $this->knownUserService->expects($this->any()) - ->method('isKnownToUser') - ->with('requesting-user', 'valid-user') - ->willReturn($isKnownUser); - } else { - $this->knownUserService->expects($this->never()) - ->method('isKnownToUser'); - } - - if ($expectedPlaceholder) { - $expected = new PlaceholderAvatar($folder, $user, $this->config, $this->logger); - } else { - $expected = new UserAvatar($folder, $this->l10n, $user, $this->logger, $this->config); - } - $this->assertEquals($expected, $this->avatarManager->getAvatar('valid-user')); - } - - public function testGetAvatarInvalidUser(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('user does not exist'); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('invalidUser') - ->willReturn(null); - - $this->avatarManager->getAvatar('invalidUser'); - } - - public function testGetAvatarForRemoteUser(): void { - $cloudId = 'user@https://remote.example.com'; - - $this->userManager - ->expects($this->once()) - ->method('get') - ->willReturn(null); - - $resolvedCloudId = $this->createMock(ICloudId::class); - $resolvedCloudId->method('getUser')->willReturn('user'); - $resolvedCloudId->method('getRemote')->willReturn('https://remote.example.com'); - $resolvedCloudId->method('getDisplayId')->willReturn('user@remote.example.com'); - - $this->cloudIdManager->expects($this->once()) - ->method('isValidCloudId') - ->with($cloudId) - ->willReturn(true); - $this->cloudIdManager->method('resolveCloudId') - ->with($cloudId) - ->willReturn($resolvedCloudId); - $this->overwriteService(ICloudIdManager::class, $this->cloudIdManager); - - $this->appData->expects($this->once())->method('getFolder'); - $this->accountManager->expects($this->never())->method('getAccount'); - - $avatar = $this->avatarManager->getAvatar($cloudId); - - $this->assertInstanceOf(RemoteAvatar::class, $avatar); - $this->assertTrue($avatar->exists()); - $this->assertTrue($avatar->isCustomAvatar()); - $this->assertSame('user@remote.example.com', $avatar->getDisplayName()); - } - - public function testGetAvatarThrowsForUnknownUserThatIsNotACloudId(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('user does not exist'); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('invalidUser') - ->willReturn(null); - - $this->cloudIdManager->expects($this->once()) - ->method('isValidCloudId') - ->with('invalidUser') - ->willReturn(false); - - $this->avatarManager->getAvatar('invalidUser'); - } -} --- a/tests/lib/Avatar/GuestAvatarTest.php +++ /dev/null @@ -1,63 +0,0 @@ -createMock(LoggerInterface::class); - $config = $this->createMock(IConfig::class); - $this->guestAvatar = new GuestAvatar('einstein', $config, $logger); - } - - /** - * Asserts that testGet() returns the expected avatar. - * - * For the test a static name "einstein" is used and - * the generated image is compared with an expected one. - */ - public function testGet(): void { - $avatar = $this->guestAvatar->getFile(32); - self::assertInstanceOf(InMemoryFile::class, $avatar); - $expectedFile = file_get_contents( - __DIR__ . '/../../data/guest_avatar_einstein_32.png' - ); - self::assertEquals(trim($expectedFile), trim($avatar->getContent())); - } - - /** - * Asserts that "testIsCustomAvatar" returns false for guests. - * - * @return void - */ - public function testIsCustomAvatar(): void { - self::assertFalse($this->guestAvatar->isCustomAvatar()); - } -} --- a/tests/lib/Avatar/RemoteAvatarTest.php +++ /dev/null @@ -1,145 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $cloudId = $this->createMock(ICloudId::class); - $cloudId->method('getUser')->willReturn('user'); - $cloudId->method('getRemote')->willReturn('https://remote.example.com'); - $cloudId->method('getDisplayId')->willReturn('user@remote.example.com'); - - $this->cloudIdManager = $this->createMock(ICloudIdManager::class); - $this->cloudIdManager->method('resolveCloudId') - ->with(self::CLOUD_ID) - ->willReturn($cloudId); - $this->overwriteService(ICloudIdManager::class, $this->cloudIdManager); - - $this->clientService = $this->createMock(IClientService::class); - $this->overwriteService(IClientService::class, $this->clientService); - - $this->folder = $this->createMock(ISimpleFolder::class); - $this->avatar = new RemoteAvatar($this->folder, self::CLOUD_ID, $this->config, $this->logger); - } - - /** - * Stubs the client returned by IClientService::newClient() to respond to - * a single GET request, optionally asserting the requested URL/options. - * - * @param string|resource|false $body - */ - private function mockRemoteClient(string $contentType, string $body, ?string $expectedUrl = null, ?array $expectedOptions = null): void { - $response = $this->createMock(IResponse::class); - $response->method('getHeader')->with('Content-Type')->willReturn($contentType); - $response->method('getBody')->willReturn($body); - - $client = $this->createMock(IClient::class); - $matcher = $client->expects($this->once())->method('get'); - if ($expectedUrl !== null) { - $matcher->with($expectedUrl, $expectedOptions ?? $this->anything()); - } - $matcher->willReturn($response); - - $this->clientService->method('newClient')->willReturn($client); - } - - public function testExists(): void { - $this->assertTrue($this->avatar->exists()); - } - - public function testGetDisplayName(): void { - $this->assertSame('user@remote.example.com', $this->avatar->getDisplayName()); - } - - public function testGetFileFetchesAvatarFromRemoteInstance(): void { - $this->config->method('getSystemValueBool') - ->with('sharing.federation.allowSelfSignedCertificates', false) - ->willReturn(false); - - $fileContents = 'png-bytes'; - - $this->mockRemoteClient( - 'image/png', - $fileContents, - 'https://remote.example.com/index.php/avatar/user/64', - ['verify' => true], - ); - - $expectedFile = $this->createMock(ISimpleFile::class); - $expectedFile->expects($this->once())->method('getContent')->willReturn($fileContents); - - $this->folder->expects($this->once())->method('newFile')->willReturn($expectedFile); - - $file = $this->avatar->getFile(64); - $this->assertInstanceOf(ISimpleFile::class, $file); - $this->assertSame($fileContents, $file->getContent()); - } - - public function testGetFileFetchesAvatarFromCache(): void { - $this->config->method('getSystemValueBool') - ->with('sharing.federation.allowSelfSignedCertificates', false) - ->willReturn(false); - - $fileContents = 'png-bytes'; - - $expectedFile = $this->createMock(ISimpleFile::class); - $expectedFile->expects($this->once())->method('getContent')->willReturn($fileContents); - $expectedFile->expects($this->once())->method('getMTime')->willReturn(time() + (60 * 60 * 24)); - $expectedFile->expects($this->once())->method('getName')->willReturn('avatar.64.png'); - $this->folder->expects($this->once())->method('getDirectoryListing')->willReturn([$expectedFile]); - - $this->clientService->expects($this->never())->method('newClient'); - - $file = $this->avatar->getFile(64); - $this->assertInstanceOf(ISimpleFile::class, $file); - $this->assertSame($fileContents, $file->getContent()); - } - - public function testGetFileThrowsOnUnexpectedContentType(): void { - $this->mockRemoteClient('text/html', ''); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Unknown filetype'); - - $this->avatar->getFile(64); - } - - public function testIsCustomAvatar(): void { - $this->assertTrue($this->avatar->isCustomAvatar()); - } -} --- a/tests/lib/Avatar/UserAvatarTest.php +++ /dev/null @@ -1,295 +0,0 @@ -folder = $this->createMock(SimpleFolder::class); - // abcdefghi is a convenient name that our algorithm convert to our nextcloud blue 0082c9 - $this->user = $this->getUserWithDisplayName('abcdefghi'); - $this->config = $this->createMock(IConfig::class); - - $this->avatar = $this->getUserAvatar($this->user); - } - - public static function avatarTextData(): array { - return [ - ['', '?'], - ['matchish', 'M'], - ['Firstname Lastname', 'FL'], - ['Firstname Lastname Rest', 'FL'], - ]; - } - - public function testGetNoAvatar(): void { - $file = $this->createMock(ISimpleFile::class); - $this->folder->method('newFile') - ->willReturn($file); - - $this->folder->method('getFile') - ->willReturnCallback(function (string $path): void { - if ($path === 'avatar.64.png') { - throw new NotFoundException(); - } - }); - $this->folder->method('fileExists') - ->willReturnCallback(function ($path) { - if ($path === 'generated') { - return true; - } - return false; - }); - - $data = null; - $file->method('putContent') - ->with($this->callback(function ($d) use (&$data) { - $data = $d; - return true; - })); - - $file->method('getContent') - ->willReturnCallback(function () use (&$data) { - return $data; - }); - - $result = $this->avatar->get(); - $this->assertTrue($result->valid()); - } - - public function testGetAvatarSizeMatch(): void { - $this->folder->method('fileExists') - ->willReturnMap([ - ['avatar.jpg', true], - ['avatar.128.jpg', true], - ['generated', false], - ]); - - $expected = new Image(); - $expected->loadFromFile(\OC::$SERVERROOT . '/tests/data/testavatar.png'); - - $file = $this->createMock(ISimpleFile::class); - $file->method('getContent')->willReturn($expected->data()); - $this->folder->method('getFile')->with('avatar.128.jpg')->willReturn($file); - - $this->assertEquals($expected->data(), $this->avatar->get(128)->data()); - } - - public function testGetAvatarSizeMinusOne(): void { - $this->folder->method('fileExists') - ->willReturnMap([ - ['avatar.jpg', true], - ['generated', false], - ]); - - $expected = new Image(); - $expected->loadFromFile(\OC::$SERVERROOT . '/tests/data/testavatar.png'); - - $file = $this->createMock(ISimpleFile::class); - $file->method('getContent')->willReturn($expected->data()); - $this->folder->method('getFile')->with('avatar.jpg')->willReturn($file); - - $this->assertEquals($expected->data(), $this->avatar->get(-1)->data()); - } - - public function testGetAvatarNoSizeMatch(): void { - $this->folder->method('fileExists') - ->willReturnMap([ - ['avatar.jpg', false], - ['avatar.png', true], - ['avatar.32.png', false], - ['generated', false], - ]); - - $expected = new Image(); - $expected->loadFromFile(\OC::$SERVERROOT . '/tests/data/testavatar.png'); - $expected2 = new Image(); - $expected2->loadFromFile(\OC::$SERVERROOT . '/tests/data/testavatar.png'); - $expected2->resize(32); - - $file = $this->createMock(ISimpleFile::class); - $file->method('getContent')->willReturn($expected->data()); - - $this->folder->method('getFile') - ->willReturnCallback( - function ($path) use ($file) { - if ($path === 'avatar.png') { - return $file; - } else { - throw new NotFoundException; - } - } - ); - - $newFile = $this->createMock(ISimpleFile::class); - $newFile->expects($this->once()) - ->method('putContent') - ->with($expected2->data()); - $newFile->expects($this->once()) - ->method('getContent') - ->willReturn($expected2->data()); - $this->folder->expects($this->once()) - ->method('newFile') - ->with('avatar.32.png') - ->willReturn($newFile); - - $this->assertEquals($expected2->data(), $this->avatar->get(32)->data()); - } - - public function testExistsNo(): void { - $this->assertFalse($this->avatar->exists()); - } - - public function testExiststJPG(): void { - $this->folder->method('fileExists') - ->willReturnMap([ - ['avatar.jpg', true], - ['avatar.png', false], - ]); - $this->assertTrue($this->avatar->exists()); - } - - public function testExistsPNG(): void { - $this->folder->method('fileExists') - ->willReturnMap([ - ['avatar.jpg', false], - ['avatar.png', true], - ]); - $this->assertTrue($this->avatar->exists()); - } - - public function testSetAvatar(): void { - $avatarFileJPG = $this->createMock(File::class); - $avatarFileJPG->method('getName') - ->willReturn('avatar.jpg'); - $avatarFileJPG->expects($this->once())->method('delete'); - - $avatarFilePNG = $this->createMock(File::class); - $avatarFilePNG->method('getName') - ->willReturn('avatar.png'); - $avatarFilePNG->expects($this->once())->method('delete'); - - $resizedAvatarFile = $this->createMock(File::class); - $resizedAvatarFile->method('getName') - ->willReturn('avatar.32.jpg'); - $resizedAvatarFile->expects($this->once())->method('delete'); - - $this->folder->method('getDirectoryListing') - ->willReturn([$avatarFileJPG, $avatarFilePNG, $resizedAvatarFile]); - - $generated = $this->createMock(ISimpleFile::class); - $this->folder->method('getFile') - ->with('generated') - ->willReturn($generated); - - $newFile = $this->createMock(ISimpleFile::class); - $this->folder->expects($this->once()) - ->method('newFile') - ->with('avatar.png') - ->willReturn($newFile); - - $image = new Image(); - $image->loadFromFile(\OC::$SERVERROOT . '/tests/data/testavatar.png'); - $newFile->expects($this->once()) - ->method('putContent') - ->with($image->data()); - - $this->config->expects($this->exactly(3)) - ->method('setUserValue'); - $this->config->expects($this->once()) - ->method('getUserValue'); - - $this->user->expects($this->exactly(1))->method('triggerChange'); - - $this->avatar->set($image->data()); - } - - public function testGenerateSvgAvatar(): void { - $avatar = $this->invokePrivate($this->avatar, 'getAvatarVector', [$this->user->getDisplayName(), 64, false]); - - $svg = ' - - - A - '; - $this->assertEquals($avatar, $svg); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('avatarTextData')] - public function testGetAvatarText($displayName, $expectedAvatarText): void { - $user = $this->getUserWithDisplayName($displayName); - $avatar = $this->getUserAvatar($user); - - $avatarText = $this->invokePrivate($avatar, 'getAvatarText'); - $this->assertEquals($expectedAvatarText, $avatarText); - } - - public function testHashToInt(): void { - $hashToInt = $this->invokePrivate($this->avatar, 'hashToInt', ['abcdef', 18]); - $this->assertTrue(gettype($hashToInt) === 'integer'); - } - - public function testMixPalette(): void { - $colorFrom = new Color(0, 0, 0); - $colorTo = new Color(6, 12, 18); - $steps = 6; - $palette = Color::mixPalette($steps, $colorFrom, $colorTo); - foreach ($palette as $j => $color) { - // calc increment - $incR = (int)($colorTo->red() / $steps * $j); - $incG = (int)($colorTo->green() / $steps * $j); - $incB = (int)($colorTo->blue() / $steps * $j); - // ensure everything is equal - $this->assertEquals($color, new Color($incR, $incG, $incB)); - } - $hashToInt = $this->invokePrivate($this->avatar, 'hashToInt', ['abcdef', 18]); - $this->assertTrue(gettype($hashToInt) === 'integer'); - } - - private function getUserWithDisplayName($name) { - $user = $this->createMock(User::class); - $user->method('getDisplayName')->willReturn($name); - return $user; - } - - private function getUserAvatar($user) { - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l */ - $l = $this->createMock(IL10N::class); - $l->method('t')->willReturnArgument(0); - - return new UserAvatar( - $this->folder, - $l, - $user, - $this->createMock(LoggerInterface::class), - $this->config - ); - } -} --- a/tests/lib/BackgroundJob/DummyJob.php +++ /dev/null @@ -1,39 +0,0 @@ - $job - * @param mixed $argument - */ - #[\Override] - public function add($job, $argument = null, ?int $firstCheck = null): void { - if (is_string($job)) { - /** @var IJob $job */ - $job = Server::get($job); - } - $job->setArgument($argument); - $job->setId(Server::get(ISnowflakeGenerator::class)->nextId()); - if (!$this->has($job, null)) { - $this->jobs[] = $job; - } - } - - #[\Override] - public function scheduleAfter(string $job, int $runAfter, $argument = null): void { - $this->add($job, $argument, $runAfter); - } - - /** - * @param IJob|string $job - * @param mixed $argument - */ - #[\Override] - public function remove($job, $argument = null): void { - foreach ($this->jobs as $index => $listJob) { - if (get_class($job) === get_class($listJob) && $job->getArgument() == $listJob->getArgument()) { - unset($this->jobs[$index]); - return; - } - } - } - - #[\Override] - public function removeById(string $id): void { - foreach ($this->jobs as $index => $listJob) { - if ($listJob->getId() === $id) { - unset($this->jobs[$index]); - return; - } - } - } - - /** - * check if a job is in the list - * - * @param $job - * @param mixed $argument - * @return bool - */ - #[\Override] - public function has($job, $argument): bool { - return array_search($job, $this->jobs) !== false; - } - - /** - * get all jobs in the list - * - * @return IJob[] - */ - public function getAll(): array { - return $this->jobs; - } - - #[\Override] - public function getJobsIterator($job, ?int $limit, int $offset): iterable { - if ($job instanceof IJob) { - $jobClass = get_class($job); - } else { - $jobClass = $job; - } - - $jobs = array_slice( - array_filter( - $this->jobs, - fn ($job) => ($jobClass === null) || (get_class($job) === $jobClass) - ), - $offset, - $limit - ); - - return new ArrayIterator($jobs); - } - - /** - * get the next job in the list - */ - #[\Override] - public function getNext(bool $onlyTimeSensitive = false, ?array $jobClasses = null): ?IJob { - if (count($this->jobs) > 0) { - if ($this->last < (count($this->jobs) - 1)) { - $i = $this->last + 1; - } else { - $i = 0; - } - return $this->jobs[$i]; - } else { - return null; - } - } - - /** - * set the job that was last ran - * - * @param Job $job - */ - #[\Override] - public function setLastJob(IJob $job): void { - $i = array_search($job, $this->jobs); - if ($i !== false) { - $this->last = $i; - } else { - $this->last = 0; - } - } - - #[\Override] - public function getById(string $id): ?IJob { - foreach ($this->jobs as $job) { - if ($job->getId() === $id) { - return $job; - } - } - return null; - } - - #[\Override] - public function getDetailsById(string $id): ?array { - return null; - } - - #[\Override] - public function setLastRun(IJob $job): void { - $job->setLastRun(time()); - } - - #[\Override] - public function hasReservedJob(?string $className = null): bool { - return isset($this->reserved[$className ?? '']) && $this->reserved[$className ?? '']; - } - - public function setHasReservedJob(?string $className, bool $hasReserved): void { - $this->reserved[$className ?? ''] = $hasReserved; - } - - #[\Override] - public function setExecutionTime(IJob $job, $timeTaken): void { - } - - #[\Override] - public function resetBackgroundJob(IJob $job): void { - } -} --- a/tests/lib/BackgroundJob/JobClassesRegistryTest.php +++ /dev/null @@ -1,82 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->snowflakeGenerator = Server::get(ISnowflakeGenerator::class); - $this->registry = new JobClassesRegistry($this->connection, $this->snowflakeGenerator); - } - - public function testResolveNonExistingClass() { - $className = 'invalid_class_name_122278'; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class ' . $className . ' doesn’t exists'); - $this->registry->getId($className); - } - - public function testResolveInvalidClass() { - $className = self::class; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class ' . $className . ' isn’t an instance of OCP\BackgroundJob\IJob'); - $this->registry->getId($className); - } - - public function testResolveValidClass() { - $className = DummyJob::class; - - $classId = $this->registry->getId($className); - $this->assertIsString($classId); - $this->assertGreaterThan(0, $classId); - - // Renew register. ID should stay the same - $this->registry = new JobClassesRegistry($this->connection, $this->snowflakeGenerator); - $newId = $this->registry->getId($className); - $this->assertEquals($classId, $newId); - } - - public function testResolveValidId() { - $className = DummyJob::class; - - $classId = $this->registry->getId($className); - $resolvedClass = $this->registry->getName($classId); - - $this->assertEquals($className, $resolvedClass); - } - - public function testResolveInvalidId() { - $classId = PHP_INT_MAX; - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class ID ' . $classId . ' doesn’t match any class name'); - $this->registry->getName($classId); - } -} --- a/tests/lib/BackgroundJob/JobListTest.php +++ /dev/null @@ -1,357 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->clearJobsList(); - $this->config = $this->createMock(IConfig::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->instance = new JobList( - $this->connection, - $this->config, - $this->timeFactory, - Server::get(LoggerInterface::class), - Server::get(ISnowflakeGenerator::class), - ); - } - - protected function clearJobsList(): void { - $query = $this->connection->getQueryBuilder(); - $query->delete('jobs'); - $query->executeStatement(); - } - - protected function getAllSorted(): array { - $iterator = $this->instance->getJobsIterator(null, null, 0); - $jobs = []; - - foreach ($iterator as $job) { - $jobs[] = clone $job; - } - - return $jobs; - } - - public static function argumentProvider(): array { - return [ - [null], - [false], - ['foobar'], - [12], - [[ - 'asd' => 5, - 'foo' => 'bar' - ]] - ]; - } - - #[DataProvider('argumentProvider')] - public function testAddRemove(mixed $argument): void { - $existingJobs = $this->getAllSorted(); - $job = new TestJob(); - $this->instance->add($job, $argument); - - $jobs = $this->getAllSorted(); - - $this->assertCount(count($existingJobs) + 1, $jobs); - $addedJob = $jobs[count($jobs) - 1]; - $this->assertInstanceOf('\Test\BackgroundJob\TestJob', $addedJob); - $this->assertEquals($argument, $addedJob->getArgument()); - - $this->instance->remove($job, $argument); - - $jobs = $this->getAllSorted(); - $this->assertEquals($existingJobs, $jobs); - } - - public function testAddAcceptsArgumentUnderMaxLength(): void { - $argument = str_repeat('a', $this->instance::MAX_ARGUMENT_JSON_LENGTH - 100); - $job = new TestJob(); - $this->assertFalse($this->instance->has($job, $argument)); - $this->instance->add($job, $argument); - - $this->assertTrue($this->instance->has($job, $argument)); - } - - public function testAddRejectsArgumentAboveMaxLength(): void { - $argument = str_repeat('a', $this->instance::MAX_ARGUMENT_JSON_LENGTH + 100); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Background job arguments can\'t exceed ' . $this->instance::MAX_ARGUMENT_JSON_LENGTH . ' characters (json encoded)'); - - $this->instance->add(new TestJob(), $argument); - } - - #[DataProvider('argumentProvider')] - public function testRemoveDifferentArgument(mixed $argument): void { - $existingJobs = $this->getAllSorted(); - $job = new TestJob(); - $this->instance->add($job, $argument); - - $jobs = $this->getAllSorted(); - $this->instance->remove($job, 10); - $jobs2 = $this->getAllSorted(); - - $this->assertEquals($jobs, $jobs2); - - $this->instance->remove($job, $argument); - - $jobs = $this->getAllSorted(); - $this->assertEquals($existingJobs, $jobs); - } - - #[DataProvider('argumentProvider')] - public function testHas(mixed $argument): void { - $job = new TestJob(); - $this->assertFalse($this->instance->has($job, $argument)); - $this->instance->add($job, $argument); - - $this->assertTrue($this->instance->has($job, $argument)); - - $this->instance->remove($job, $argument); - - $this->assertFalse($this->instance->has($job, $argument)); - } - - #[DataProvider('argumentProvider')] - public function testHasDifferentArgument(mixed $argument): void { - $job = new TestJob(); - $this->instance->add($job, $argument); - - $this->assertFalse($this->instance->has($job, 10)); - } - - protected function createTempJob($class, - $argument, - int $reservedTime = 0, - int $lastChecked = 0, - int $lastRun = 0): string { - if ($lastChecked === 0) { - $lastChecked = time(); - } - $id = Server::get(ISnowflakeGenerator::class)->nextId(); - - $query = $this->connection->getQueryBuilder(); - $query->insert('jobs') - ->values([ - 'id' => $query->createNamedParameter($id), - 'class' => $query->createNamedParameter($class), - 'argument' => $query->createNamedParameter($argument), - 'last_run' => $query->createNamedParameter($lastRun, IQueryBuilder::PARAM_INT), - 'last_checked' => $query->createNamedParameter($lastChecked, IQueryBuilder::PARAM_INT), - 'reserved_at' => $query->createNamedParameter($reservedTime, IQueryBuilder::PARAM_INT), - ]); - $query->executeStatement(); - return $id; - } - - public function testGetNext(): void { - $job = new TestJob(); - $this->createTempJob(get_class($job), 1, 0, 12345); - $this->createTempJob(get_class($job), 2, 0, 12346); - - $jobs = $this->getAllSorted(); - $savedJob1 = $jobs[0]; - - $this->timeFactory->expects($this->atLeastOnce()) - ->method('getTime') - ->willReturn(123456789); - $nextJob = $this->instance->getNext(); - - $this->assertEquals($savedJob1, $nextJob); - } - - public function testGetNextSkipReserved(): void { - $job = new TestJob(); - $this->createTempJob(get_class($job), 1, 123456789, 12345); - $this->createTempJob(get_class($job), 2, 0, 12346); - - $this->timeFactory->expects($this->atLeastOnce()) - ->method('getTime') - ->willReturn(123456789); - $nextJob = $this->instance->getNext(); - - $this->assertEquals(get_class($job), get_class($nextJob)); - $this->assertEquals(2, $nextJob->getArgument()); - } - - public function testGetNextSkipTimed(): void { - $job = new TestTimedJobNew($this->timeFactory); - $jobId = $this->createTempJob(get_class($job), 1, 123456789, 12345, 123456789 - 5); - $this->timeFactory->expects(self::atLeastOnce()) - ->method('getTime') - ->willReturn(123456789); - - $nextJob = $this->instance->getNext(); - - self::assertNull($nextJob); - $job = $this->instance->getById($jobId); - self::assertInstanceOf(TestTimedJobNew::class, $job); - self::assertEquals(123456789 - 5, $job->getLastRun()); - } - - public function testGetNextSkipNonExisting(): void { - $job = new TestJob(); - $this->createTempJob('\OC\Non\Existing\Class', 1, 0, 12345); - $this->createTempJob(get_class($job), 2, 0, 12346); - - $this->timeFactory->expects($this->atLeastOnce()) - ->method('getTime') - ->willReturn(123456789); - $nextJob = $this->instance->getNext(); - - $this->assertEquals(get_class($job), get_class($nextJob)); - $this->assertEquals(2, $nextJob->getArgument()); - } - - /** - * @param $argument - */ - #[DataProvider('argumentProvider')] - public function testGetById($argument): void { - $job = new TestJob(); - $this->instance->add($job, $argument); - - $jobs = $this->getAllSorted(); - - $addedJob = $jobs[count($jobs) - 1]; - - $this->assertEquals($addedJob, $this->instance->getById($addedJob->getId())); - } - - public function testSetLastRun(): void { - $job = new TestJob(); - $this->instance->add($job); - - $jobs = $this->getAllSorted(); - - $addedJob = $jobs[count($jobs) - 1]; - - $timeStart = time(); - $this->instance->setLastRun($addedJob); - $timeEnd = time(); - - $addedJob = $this->instance->getById($addedJob->getId()); - - $this->assertGreaterThanOrEqual($timeStart, $addedJob->getLastRun()); - $this->assertLessThanOrEqual($timeEnd, $addedJob->getLastRun()); - } - - public function testRemoveByIdWithSnowflakeId(): void { - $this->instance->add(new TestJob(), 'remove-by-id'); - $job = $this->instance->getJobs(null, 1, 0)[0]; - - $this->instance->removeById($job->getId()); - - $this->assertNull($this->instance->getById($job->getId())); - } - - public function testResetBackgroundJobWithSnowflakeId(): void { - $this->instance->add(new TestJob(), 'reset'); - $job = $this->instance->getJobs(null, 1, 0)[0]; - - $this->instance->resetBackgroundJob($job); - - $row = $this->instance->getDetailsById($job->getId()); - $this->assertSame('0', (string)$row['last_run']); - $this->assertSame('0', (string)$row['reserved_at']); - } - - public function testHasReservedJobs(): void { - $this->clearJobsList(); - - $this->timeFactory->expects($this->atLeastOnce()) - ->method('getTime') - ->willReturn(123456789); - - $job = new TestJob($this->timeFactory, $this, function (): void { - }); - - $job2 = new TestJob($this->timeFactory, $this, function (): void { - }); - - $this->instance->add($job, 1); - $this->instance->add($job2, 2); - - $this->assertCount(2, iterator_to_array($this->instance->getJobsIterator(null, 10, 0))); - - $this->assertFalse($this->instance->hasReservedJob()); - $this->assertFalse($this->instance->hasReservedJob(TestJob::class)); - - $job = $this->instance->getNext(); - $this->assertNotNull($job); - $this->assertTrue($this->instance->hasReservedJob()); - $this->assertTrue($this->instance->hasReservedJob(TestJob::class)); - $job = $this->instance->getNext(); - $this->assertNotNull($job); - $this->assertTrue($this->instance->hasReservedJob()); - $this->assertTrue($this->instance->hasReservedJob(TestJob::class)); - } - - public function testHasReservedJobsAndParallelAwareJob(): void { - $this->clearJobsList(); - - $this->timeFactory->expects($this->atLeastOnce()) - ->method('getTime') - ->willReturnCallback(function () use (&$time) { - return time(); - }); - - $job = new TestParallelAwareJob($this->timeFactory, $this, function (): void { - }); - - $job2 = new TestParallelAwareJob($this->timeFactory, $this, function (): void { - }); - - $this->instance->add($job, 1); - $this->instance->add($job2, 2); - - $this->assertCount(2, iterator_to_array($this->instance->getJobsIterator(null, 10, 0))); - - $this->assertFalse($this->instance->hasReservedJob()); - $this->assertFalse($this->instance->hasReservedJob(TestParallelAwareJob::class)); - - $job = $this->instance->getNext(); - $this->assertNotNull($job); - $this->assertTrue($this->instance->hasReservedJob()); - $this->assertTrue($this->instance->hasReservedJob(TestParallelAwareJob::class)); - $job = $this->instance->getNext(); - $this->assertNull($job); // Job doesn't allow parallel runs - } -} --- a/tests/lib/BackgroundJob/JobRunsTest.php +++ /dev/null @@ -1,111 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->registry = Server::get(JobClassesRegistry::class); - $this->runs = new JobRuns( - $this->connection, - Server::get(ISnowflakeGenerator::class), - Server::get(ISnowflakeDecoder::class), - $this->registry, - $this->createMock(LoggerInterface::class), - ); - } - - public function testJobStarted(): void { - $myPid = 1337; - $myClass = DummyJob::class; - - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - - $this->assertGreaterThan(0, $runId); - } - - public function testJobSucceeded(): void { - $myPid = 1337; - $myClass = DummyJob::class; - - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - - $result = $this->runs->finished($runId, 12, 9876543); - - $this->assertTrue($result); - } - - public function testJobFailed(): void { - $myPid = 1337; - $myClass = DummyJob::class; - - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - - $result = $this->runs->finished($runId, 13, 87654321, JobStatus::FAILED); - - $this->assertTrue($result); - } - - public function testRunningJobs(): void { - $myPid = 1337; - $myClass = DummyJob::class; - - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - - $runningJobs = 0; - foreach ($this->runs->runningJobs() as $job) { - $this->assertInstanceOf(JobRun::class, $job); - ++$runningJobs; - } - $this->assertGreaterThan(0, $runningJobs); - } - - public function testCompletedJobs(): void { - $myPid = 1337; - $myClass = DummyJob::class; - - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - $this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::FAILED); - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - $this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::SUCCEEDED); - $runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid); - $this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::CRASHED); - - $completedJobs = 0; - foreach ($this->runs->runningJobs() as $job) { - $this->assertInstanceOf(JobRun::class, $job); - ++$completedJobs; - } - $this->assertGreaterThan(0, $completedJobs); - } -} --- a/tests/lib/BackgroundJob/JobTest.php +++ /dev/null @@ -1,67 +0,0 @@ -run = false; - $this->timeFactory = Server::get(ITimeFactory::class); - $this->logger = $this->createMock(LoggerInterface::class); - - \OC::$server->registerService(LoggerInterface::class, fn ($c) => $this->logger); - } - - public function testRemoveAfterException(): void { - $jobList = new DummyJobList(); - $e = new \Exception(); - $job = new TestJob($this->timeFactory, $this, function () use ($e): void { - throw $e; - }); - $jobList->add($job); - - $this->logger->expects($this->once()) - ->method('error'); - - $this->assertCount(1, $jobList->getAll()); - $job->start($jobList); - $this->assertTrue($this->run); - $this->assertCount(1, $jobList->getAll()); - } - - public function testRemoveAfterError(): void { - $jobList = new DummyJobList(); - $job = new TestJob($this->timeFactory, $this, function (): void { - $test = null; - $test->someMethod(); - }); - $jobList->add($job); - - $this->logger->expects($this->once()) - ->method('error'); - - $this->assertCount(1, $jobList->getAll()); - $job->start($jobList); - $this->assertTrue($this->run); - $this->assertCount(1, $jobList->getAll()); - } - - public function markRun() { - $this->run = true; - } -} --- a/tests/lib/BackgroundJob/QueuedJobTest.php +++ /dev/null @@ -1,43 +0,0 @@ -ran = true; - } -} - -class QueuedJobTest extends \Test\TestCase { - private DummyJobList $jobList; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->jobList = new DummyJobList(); - } - - public function testJobShouldBeRemovedNew(): void { - $job = new TestQueuedJobNew(Server::get(ITimeFactory::class)); - $job->setId(42); - $this->jobList->add($job); - - $this->assertTrue($this->jobList->has($job, null)); - $job->start($this->jobList); - $this->assertTrue($job->ran); - } -} --- a/tests/lib/BackgroundJob/TestJob.php +++ /dev/null @@ -1,40 +0,0 @@ -callback = $callback; - } - - #[\Override] - public function run($argument) { - $this->testCase->markRun(); - $callback = $this->callback; - $callback($argument); - } -} --- a/tests/lib/BackgroundJob/TestParallelAwareJob.php +++ /dev/null @@ -1,40 +0,0 @@ -setAllowParallelRuns(false); - $this->callback = $callback; - } - - #[\Override] - public function run($argument) { - $this->testCase->markRun(); - $callback = $this->callback; - $callback($argument); - } -} --- a/tests/lib/BackgroundJob/TestTimedJobNew.php +++ /dev/null @@ -1,27 +0,0 @@ -setInterval(10); - } - - #[\Override] - public function run($argument) { - $this->ran = true; - } -} --- a/tests/lib/BackgroundJob/TimedJobTest.php +++ /dev/null @@ -1,58 +0,0 @@ -jobList = new DummyJobList(); - $this->time = Server::get(ITimeFactory::class); - } - - public function testShouldRunAfterIntervalNew(): void { - $job = new TestTimedJobNew($this->time); - $job->setId(42); - $this->jobList->add($job); - - $job->setLastRun(time() - 12); - $job->start($this->jobList); - $this->assertTrue($job->ran); - } - - public function testShouldNotRunWithinIntervalNew(): void { - $job = new TestTimedJobNew($this->time); - $job->setId(42); - $this->jobList->add($job); - - $job->setLastRun(time() - 5); - $job->start($this->jobList); - $this->assertFalse($job->ran); - } - - public function testShouldNotTwiceNew(): void { - $job = new TestTimedJobNew($this->time); - $job->setId(42); - $this->jobList->add($job); - - $job->setLastRun(time() - 15); - $job->start($this->jobList); - $this->assertTrue($job->ran); - $job->ran = false; - $job->start($this->jobList); - $this->assertFalse($job->ran); - } -} --- a/tests/lib/BinaryFinderTest.php +++ /dev/null @@ -1,94 +0,0 @@ -oldEnv = getenv('PATH'); - // BinaryFinder always includes the "PATH" environment variable into the search path, - // which we want to avoid in this test because they are not usually found in webserver - // deployments. - putenv('PATH=""'); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cache = new ArrayCache(); - $this->cacheFactory->method('createLocal')->with('findBinaryPath')->willReturn($this->cache); - } - - #[\Override] - protected function tearDown(): void { - putenv('PATH=' . $this->oldEnv); - } - - public function testDefaultFindsCat() { - $config = $this->createMock(IConfig::class); - $config - ->method('getSystemValue') - ->with('binary_search_paths', $this->anything()) - ->willReturnCallback(function ($key, $default) { - return $default; - }); - $finder = new BinaryFinder($this->cacheFactory, $config); - $this->assertStringEndsWith('/cat', $finder->findBinaryPath('cat')); - $this->assertStringEndsWith('/cat', $this->cache->get('cat')); - } - - public function testDefaultDoesNotFindCata() { - $config = $this->createMock(IConfig::class); - $config - ->method('getSystemValue') - ->with('binary_search_paths', $this->anything()) - ->willReturnCallback(function ($key, $default) { - return $default; - }); - $finder = new BinaryFinder($this->cacheFactory, $config); - $this->assertFalse($finder->findBinaryPath('cata')); - $this->assertFalse($this->cache->get('cata')); - } - - public function testCustomPathFindsCat() { - $tmpdir = Server::get(ITempManager::class)->getTemporaryFolder(); - touch($tmpdir . '/cat'); - chmod($tmpdir . '/cat', 100); - - $config = $this->createMock(IConfig::class); - $config - ->method('getSystemValue') - ->with('binary_search_paths', $this->anything()) - ->willReturn([$tmpdir]); - $finder = new BinaryFinder($this->cacheFactory, $config); - $this->assertEquals($tmpdir . '/cat', $finder->findBinaryPath('cat')); - $this->assertEquals($tmpdir . '/cat', $this->cache->get('cat')); - } - - public function testWrongCustomPathDoesNotFindCat() { - $tmpdir = Server::get(ITempManager::class)->getTemporaryFolder(); - - $config = $this->createMock(IConfig::class); - $config - ->method('getSystemValue') - ->with('binary_search_paths') - ->willReturn([$tmpdir]); - $finder = new BinaryFinder($this->cacheFactory, $config); - $this->assertFalse($finder->findBinaryPath('cat')); - $this->assertFalse($this->cache->get('cat')); - } -} --- a/tests/lib/Cache/CappedMemoryCacheTest.php +++ /dev/null @@ -1,67 +0,0 @@ -instance = new CappedMemoryCache(); - } - - public function testSetOverCap(): void { - $instance = new CappedMemoryCache(3); - - $instance->set('1', 'a'); - $instance->set('2', 'b'); - $instance->set('3', 'c'); - $instance->set('4', 'd'); - $instance->set('5', 'e'); - - $this->assertFalse($instance->hasKey('1')); - $this->assertFalse($instance->hasKey('2')); - $this->assertTrue($instance->hasKey('3')); - $this->assertTrue($instance->hasKey('4')); - $this->assertTrue($instance->hasKey('5')); - } - - #[\Override] - public function testClear(): void { - $value = 'ipsum lorum'; - $this->instance->set('1_value1', $value); - $this->instance->set('1_value2', $value); - $this->instance->set('2_value1', $value); - $this->instance->set('3_value1', $value); - - $this->assertTrue($this->instance->clear()); - $this->assertFalse($this->instance->hasKey('1_value1')); - $this->assertFalse($this->instance->hasKey('1_value2')); - $this->assertFalse($this->instance->hasKey('2_value1')); - $this->assertFalse($this->instance->hasKey('3_value1')); - } - - public function testIndirectSet(): void { - $this->instance->set('array', []); - - $this->instance['array'][] = 'foo'; - - $this->assertEquals(['foo'], $this->instance->get('array')); - - $this->instance['array']['bar'] = 'qwerty'; - - $this->assertEquals(['foo', 'bar' => 'qwerty'], $this->instance->get('array')); - } -} --- a/tests/lib/Cache/FileCacheTest.php +++ /dev/null @@ -1,163 +0,0 @@ -skipUnless(OC_User::isLoggedIn()); - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - //login - $this->createUser('test', 'test'); - - $this->user = \OC_User::getUser(); - \OC_User::setUserId('test'); - - //clear all proxies and hooks so we can do clean testing - \OC_Hook::clear('OC_Filesystem'); - - /** @var IMountManager $manager */ - $manager = Server::get(IMountManager::class); - $manager->removeMount('/test'); - - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/test/cache'); - - //set up the users dir - $this->rootView = new View(''); - $this->rootView->mkdir('/test'); - - $this->instance = new File(); - - // forces creation of cache folder for subsequent tests - $this->instance->set('hack', 'hack'); - } - - #[\Override] - protected function tearDown(): void { - if ($this->instance) { - $this->instance->remove('hack', 'hack'); - } - - \OC_User::setUserId($this->user); - - if ($this->instance) { - $this->instance->clear(); - $this->instance = null; - } - - parent::tearDown(); - } - - private function setupMockStorage() { - $mockStorage = $this->getMockBuilder(Local::class) - ->onlyMethods(['filemtime', 'unlink']) - ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]]) - ->getMock(); - - Filesystem::mount($mockStorage, [], '/test/cache'); - - return $mockStorage; - } - - public function testGarbageCollectOldKeys(): void { - $mockStorage = $this->setupMockStorage(); - - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(100); - $mockStorage->expects($this->once()) - ->method('unlink') - ->with('key1') - ->willReturn(true); - - $this->instance->set('key1', 'value1'); - $this->instance->gc(); - } - - public function testGarbageCollectLeaveRecentKeys(): void { - $mockStorage = $this->setupMockStorage(); - - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(time() + 3600); - $mockStorage->expects($this->never()) - ->method('unlink') - ->with('key1'); - $this->instance->set('key1', 'value1'); - $this->instance->gc(); - } - - public static function lockExceptionProvider(): array { - return [ - [new LockedException('key1')], - [new LockNotAcquiredException('key1', 1)], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('lockExceptionProvider')] - public function testGarbageCollectIgnoreLockedKeys($testException): void { - $mockStorage = $this->setupMockStorage(); - - $mockStorage->expects($this->atLeastOnce()) - ->method('filemtime') - ->willReturn(100); - $mockStorage->expects($this->atLeastOnce()) - ->method('unlink') - ->willReturnOnConsecutiveCalls($this->throwException($testException), true); - - $this->instance->set('key1', 'value1'); - $this->instance->set('key2', 'value2'); - - $this->instance->gc(); - } -} --- a/tests/lib/Cache/TestCache.php +++ /dev/null @@ -1,103 +0,0 @@ -instance) { - $this->instance->clear(); - } - - parent::tearDown(); - } - - public function testSimple(): void { - $this->assertNull($this->instance->get('value1')); - $this->assertFalse($this->instance->hasKey('value1')); - - $value = 'foobar'; - $this->instance->set('value1', $value); - $this->assertTrue($this->instance->hasKey('value1')); - $received = $this->instance->get('value1'); - $this->assertEquals($value, $received, 'Value received from cache not equal to the original'); - $value = 'ipsum lorum'; - $this->instance->set('value1', $value); - $received = $this->instance->get('value1'); - $this->assertEquals($value, $received, 'Value not overwritten by second set'); - - $value2 = 'foobar'; - $this->instance->set('value2', $value2); - $received2 = $this->instance->get('value2'); - $this->assertTrue($this->instance->hasKey('value1')); - $this->assertTrue($this->instance->hasKey('value2')); - $this->assertEquals($value, $received, 'Value changed while setting other variable'); - $this->assertEquals($value2, $received2, 'Second value not equal to original'); - - $this->assertFalse($this->instance->hasKey('not_set')); - $this->assertNull($this->instance->get('not_set'), 'Unset value not equal to null'); - - $this->assertTrue($this->instance->remove('value1')); - $this->assertFalse($this->instance->hasKey('value1')); - } - - public function testClear(): void { - $value = 'ipsum lorum'; - $this->instance->set('1_value1', $value . '1'); - $this->instance->set('1_value2', $value . '2'); - $this->instance->set('2_value1', $value . '3'); - $this->instance->set('3_value1', $value . '4'); - - $this->assertEquals([ - '1_value1' => 'ipsum lorum1', - '1_value2' => 'ipsum lorum2', - '2_value1' => 'ipsum lorum3', - '3_value1' => 'ipsum lorum4', - ], [ - '1_value1' => $this->instance->get('1_value1'), - '1_value2' => $this->instance->get('1_value2'), - '2_value1' => $this->instance->get('2_value1'), - '3_value1' => $this->instance->get('3_value1'), - ]); - $this->assertTrue($this->instance->clear('1_')); - - $this->assertEquals([ - '1_value1' => null, - '1_value2' => null, - '2_value1' => 'ipsum lorum3', - '3_value1' => 'ipsum lorum4', - ], [ - '1_value1' => $this->instance->get('1_value1'), - '1_value2' => $this->instance->get('1_value2'), - '2_value1' => $this->instance->get('2_value1'), - '3_value1' => $this->instance->get('3_value1'), - ]); - - $this->assertTrue($this->instance->clear()); - - $this->assertEquals([ - '1_value1' => null, - '1_value2' => null, - '2_value1' => null, - '3_value1' => null, - ], [ - '1_value1' => $this->instance->get('1_value1'), - '1_value2' => $this->instance->get('1_value2'), - '2_value1' => $this->instance->get('2_value1'), - '3_value1' => $this->instance->get('3_value1'), - ]); - } -} --- a/tests/lib/Calendar/CalendarEventBuilderTest.php +++ /dev/null @@ -1,152 +0,0 @@ -timeFactory = $this->createMock(ITimeFactory::class); - $this->timeFactory->method('now') - ->willReturn(new DateTimeImmutable('20250105T000000Z')); - - $this->calendarEventBuilder = new CalendarEventBuilder( - 'event-uid-123', - $this->timeFactory, - ); - } - - public function testToIcs(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setStatus(CalendarEventStatus::CONFIRMED); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('mailto:organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $expected = file_get_contents(\OC::$SERVERROOT . '/tests/data/ics/event-builder-complete.ics'); - $actual = $this->calendarEventBuilder->toIcs(); - $this->assertEquals($expected, $actual); - } - - public function testToIcsWithoutOrganizerAndAttendees(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setStatus(CalendarEventStatus::CONFIRMED); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - - $expected = file_get_contents(\OC::$SERVERROOT . '/tests/data/ics/event-builder-without-attendees.ics'); - $actual = $this->calendarEventBuilder->toIcs(); - $this->assertEquals($expected, $actual); - } - - public function testToIcsWithoutMailtoPrefix(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setStatus(CalendarEventStatus::CONFIRMED); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee2@domain.tld'); - - $expected = file_get_contents(\OC::$SERVERROOT . '/tests/data/ics/event-builder-complete.ics'); - $actual = $this->calendarEventBuilder->toIcs(); - $this->assertEquals($expected, $actual); - } - - public function testCreateInCalendar(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setStatus(CalendarEventStatus::CONFIRMED); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $expectedIcs = file_get_contents(\OC::$SERVERROOT . '/tests/data/ics/event-builder-complete.ics'); - $calendar = $this->createMock(ICreateFromString::class); - $calendar->expects(self::once()) - ->method('createFromString') - ->with('event-uid-123.ics', $expectedIcs); - - $actual = $this->calendarEventBuilder->createInCalendar($calendar); - $this->assertEquals('event-uid-123.ics', $actual); - } - - public function testToIcsWithoutStartDate(): void { - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/start date/i'); - $this->calendarEventBuilder->toIcs(); - } - - public function testToIcsWithoutEndDate(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/end date/i'); - $this->calendarEventBuilder->toIcs(); - } - - public function testToIcsWithoutSummary(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->setOrganizer('organizer@domain.tld'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/summary/i'); - $this->calendarEventBuilder->toIcs(); - } - - public function testToIcsWithoutOrganizerWithAttendees(): void { - $this->calendarEventBuilder->setStartDate(new DateTimeImmutable('2025-01-05T17:09:58Z')); - $this->calendarEventBuilder->setEndDate(new DateTimeImmutable('2025-01-05T17:19:58Z')); - $this->calendarEventBuilder->setSummary('My event'); - $this->calendarEventBuilder->setDescription('Foo bar baz'); - $this->calendarEventBuilder->addAttendee('attendee1@domain.tld'); - $this->calendarEventBuilder->addAttendee('mailto:attendee2@domain.tld'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/organizer/i'); - $this->calendarEventBuilder->toIcs(); - } -} --- a/tests/lib/Calendar/ManagerTest.php +++ /dev/null @@ -1,1247 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->time = $this->createMock(ITimeFactory::class); - $this->secureRandom = $this->createMock(ISecureRandom::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->serverFactory = $this->createMock(ServerFactory::class); - $this->propertyMapper = $this->createMock(PropertyMapper::class); - - $this->manager = new Manager( - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ); - - // construct calendar with a 1 hour event and same start/end time zones - $this->vCalendar1a = new VCalendar(); - /** @var VEvent $vEvent */ - $vEvent = $this->vCalendar1a->add('VEVENT', []); - $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); - $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); - $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); - $vEvent->add('SUMMARY', 'Test Event'); - $vEvent->add('SEQUENCE', 3); - $vEvent->add('STATUS', 'CONFIRMED'); - $vEvent->add('TRANSP', 'OPAQUE'); - $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); - $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ - 'CN' => 'Attendee One', - 'CUTYPE' => 'INDIVIDUAL', - 'PARTSTAT' => 'NEEDS-ACTION', - 'ROLE' => 'REQ-PARTICIPANT', - 'RSVP' => 'TRUE' - ]); - - // construct calendar with a event for reply - $this->vCalendar2a = new VCalendar(); - /** @var VEvent $vEvent */ - $vEvent = $this->vCalendar2a->add('VEVENT', []); - $vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff'); - $vEvent->add('DTSTART', '20210820'); - $vEvent->add('DTEND', '20220821'); - $vEvent->add('SUMMARY', 'berry basket'); - $vEvent->add('SEQUENCE', 3); - $vEvent->add('STATUS', 'CONFIRMED'); - $vEvent->add('TRANSP', 'OPAQUE'); - $vEvent->add('ORGANIZER', 'mailto:linus@stardew-tent-living.com', ['CN' => 'admin']); - $vEvent->add('ATTENDEE', 'mailto:pierre@general-store.com', [ - 'CN' => 'pierre@general-store.com', - 'CUTYPE' => 'INDIVIDUAL', - 'ROLE' => 'REQ-PARTICIPANT', - 'PARTSTAT' => 'ACCEPTED', - ]); - - // construct calendar with a event for reply - $this->vCalendar3a = new VCalendar(); - /** @var VEvent $vEvent */ - $vEvent = $this->vCalendar3a->add('VEVENT', []); - $vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff'); - $vEvent->add('DTSTART', '20210820'); - $vEvent->add('DTEND', '20220821'); - $vEvent->add('SUMMARY', 'berry basket'); - $vEvent->add('SEQUENCE', 3); - $vEvent->add('STATUS', 'CANCELLED'); - $vEvent->add('TRANSP', 'OPAQUE'); - $vEvent->add('ORGANIZER', 'mailto:linus@stardew-tent-living.com', ['CN' => 'admin']); - $vEvent->add('ATTENDEE', 'mailto:pierre@general-store.com', [ - 'CN' => 'pierre@general-store.com', - 'CUTYPE' => 'INDIVIDUAL', - 'ROLE' => 'REQ-PARTICIPANT', - 'PARTSTAT' => 'ACCEPTED', - ]); - - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')] - public function testSearch($search1, $search2, $expected): void { - /** @var ICalendar | MockObject $calendar1 */ - $calendar1 = $this->createMock(ICalendar::class); - $calendar1->method('getKey')->willReturn('simple:1'); - $calendar1->expects($this->once()) - ->method('search') - ->with('', [], [], null, null) - ->willReturn($search1); - - /** @var ICalendar | MockObject $calendar2 */ - $calendar2 = $this->createMock(ICalendar::class); - $calendar2->method('getKey')->willReturn('simple:2'); - $calendar2->expects($this->once()) - ->method('search') - ->with('', [], [], null, null) - ->willReturn($search2); - - $this->manager->registerCalendar($calendar1); - $this->manager->registerCalendar($calendar2); - - $result = $this->manager->search(''); - $this->assertEquals($expected, $result); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')] - public function testSearchOptions($search1, $search2, $expected): void { - /** @var ICalendar | MockObject $calendar1 */ - $calendar1 = $this->createMock(ICalendar::class); - $calendar1->method('getKey')->willReturn('simple:1'); - $calendar1->expects($this->once()) - ->method('search') - ->with('searchTerm', ['SUMMARY', 'DESCRIPTION'], - ['timerange' => ['start' => null, 'end' => null]], 5, 20) - ->willReturn($search1); - - /** @var ICalendar | MockObject $calendar2 */ - $calendar2 = $this->createMock(ICalendar::class); - $calendar2->method('getKey')->willReturn('simple:2'); - $calendar2->expects($this->once()) - ->method('search') - ->with('searchTerm', ['SUMMARY', 'DESCRIPTION'], - ['timerange' => ['start' => null, 'end' => null]], 5, 20) - ->willReturn($search2); - - $this->manager->registerCalendar($calendar1); - $this->manager->registerCalendar($calendar2); - - $result = $this->manager->search('searchTerm', ['SUMMARY', 'DESCRIPTION'], - ['timerange' => ['start' => null, 'end' => null]], 5, 20); - $this->assertEquals($expected, $result); - } - - public static function searchProvider(): array { - $search1 = [ - [ - 'id' => 1, - 'data' => 'foobar', - ], - [ - 'id' => 2, - 'data' => 'barfoo', - ] - ]; - $search2 = [ - [ - 'id' => 3, - 'data' => 'blablub', - ], - [ - 'id' => 4, - 'data' => 'blubbla', - ] - ]; - - $expected = [ - [ - 'id' => 1, - 'data' => 'foobar', - 'calendar-key' => 'simple:1', - ], - [ - 'id' => 2, - 'data' => 'barfoo', - 'calendar-key' => 'simple:1', - ], - [ - 'id' => 3, - 'data' => 'blablub', - 'calendar-key' => 'simple:2', - ], - [ - 'id' => 4, - 'data' => 'blubbla', - 'calendar-key' => 'simple:2', - ] - ]; - - return [ - [ - $search1, - $search2, - $expected - ] - ]; - } - - public function testRegisterUnregister(): void { - /** @var ICalendar | MockObject $calendar1 */ - $calendar1 = $this->createMock(ICalendar::class); - $calendar1->method('getKey')->willReturn('key1'); - - /** @var ICalendar | MockObject $calendar2 */ - $calendar2 = $this->createMock(ICalendar::class); - $calendar2->method('getKey')->willReturn('key2'); - - $this->manager->registerCalendar($calendar1); - $this->manager->registerCalendar($calendar2); - - $result = $this->manager->getCalendars(); - $this->assertCount(2, $result); - $this->assertContains($calendar1, $result); - $this->assertContains($calendar2, $result); - - $this->manager->unregisterCalendar($calendar1); - - $result = $this->manager->getCalendars(); - $this->assertCount(1, $result); - $this->assertContains($calendar2, $result); - } - - public function testGetCalendars(): void { - /** @var ICalendar | MockObject $calendar1 */ - $calendar1 = $this->createMock(ICalendar::class); - $calendar1->method('getKey')->willReturn('key1'); - - /** @var ICalendar | MockObject $calendar2 */ - $calendar2 = $this->createMock(ICalendar::class); - $calendar2->method('getKey')->willReturn('key2'); - - $this->manager->registerCalendar($calendar1); - $this->manager->registerCalendar($calendar2); - - $result = $this->manager->getCalendars(); - $this->assertCount(2, $result); - $this->assertContains($calendar1, $result); - $this->assertContains($calendar2, $result); - - $this->manager->clear(); - - $result = $this->manager->getCalendars(); - - $this->assertCount(0, $result); - } - - public function testEnabledIfNot(): void { - $isEnabled = $this->manager->isEnabled(); - $this->assertFalse($isEnabled); - } - - public function testIfEnabledIfSo(): void { - /** @var ICalendar | MockObject $calendar */ - $calendar = $this->createMock(ICalendar::class); - $this->manager->registerCalendar($calendar); - - $isEnabled = $this->manager->isEnabled(); - $this->assertTrue($isEnabled); - } - - public function testHandleImipWithNoCalendars(): void { - // construct calendar manager returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([]); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message could not be processed because user has no calendar that can process iMip messages'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize()); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipWithNoEvent(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message does not contain any event(s)'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - $calendar->remove('VEVENT'); - // Act - $result = $manager->handleIMip($userId, $calendar->serialize()); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipMissingOrganizerWithRecipient(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([['uri' => 'principals/user/attendee1/personal']]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - $calendar->VEVENT->remove('ORGANIZER'); - // construct user calendar returns - $userCalendar->expects(self::once()) - ->method('handleIMipMessage'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize(), ['recipient' => 'organizer@testing.com']); - } - - public function testHandleImipMissingOrganizerNoRecipient(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - $calendar->VEVENT->remove('ORGANIZER'); - // Logger expects warning - $this->logger->expects($this->once()) - ->method('warning') - ->with('iMip message event does not contain an organizer and no recipient was provided'); - - $result = $manager->handleIMip($userId, $calendar->serialize(), []); - } - - public function testHandleImipWithNoUid(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message event does not contains a UID'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - $calendar->VEVENT->remove('UID'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize()); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipWithNoMatch(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message could not be processed because no corresponding event was found in any calendar'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize()); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipWithCalendarUnableToHandleImip(): void { - // construct mock user calendar which is writable but can not process iMip messages - $userCalendar = $this->createMock(ITestCalendarWithoutImip::class); - $userCalendar->expects(self::never()) - ->method('search'); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message could not be processed because user has no calendar that can process iMip messages'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize()); - // Assert - $this->assertFalse($result); - } - - public function testHandleImip(): void { - // construct mock user calendar - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([['uri' => 'principals/user/attendee1/personal']]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // construct user calendar returns - $userCalendar->expects(self::once()) - ->method('handleIMipMessage'); - // test method - $result = $manager->handleIMip($userId, $calendar->serialize()); - } - - public function testHandleImipWithAbsentCreateOption(): void { - // construct mock user calendar (no matching event found) - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - $manager->expects(self::once()) - ->method('getPrimaryCalendar') - ->willReturn(null); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // construct user calendar returns - should create new event - $userCalendar->expects(self::once()) - ->method('handleIMipMessage') - ->with($userId, self::callback(function ($data) { - return str_contains($data, 'STATUS:TENTATIVE'); - })); - // test method with absent=create option - $result = $manager->handleIMip($userId, $calendar->serialize(), [ - 'absent' => 'create', - 'absentCreateStatus' => 'tentative', - ]); - // Assert - $this->assertTrue($result); - } - - public function testHandleImipWithAbsentIgnoreOption(): void { - // construct mock user calendar (no matching event found) - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - // construct logger returns - should log warning since event not found and absent=ignore - $this->logger->expects(self::once())->method('warning') - ->with('iMip message could not be processed because no corresponding event was found in any calendar'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // test method with absent=ignore option - $result = $manager->handleIMip($userId, $calendar->serialize(), [ - 'absent' => 'ignore', - ]); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipWithAbsentCreateNoWritableCalendar(): void { - // construct mock user calendar (not writable) - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(false); - $userCalendar->expects(self::never()) - ->method('search'); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - $manager->expects(self::never()) - ->method('getPrimaryCalendar'); - // construct logger returns - $this->logger->expects(self::once())->method('warning') - ->with('iMip message could not be processed because user has no calendar that can process iMip messages'); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // test method with absent=create option but no writable calendar - $result = $manager->handleIMip($userId, $calendar->serialize(), [ - 'absent' => 'create', - 'absentCreateStatus' => 'tentative', - ]); - // Assert - $this->assertFalse($result); - } - - public function testHandleImipWithAbsentCreateUsesPrimaryCalendar(): void { - // construct mock user calendar (no matching event found) - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([]); - // construct mock primary calendar - $primaryCalendar = $this->createMock(ITestCalendar::class); - $primaryCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $primaryCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - $manager->expects(self::once()) - ->method('getPrimaryCalendar') - ->willReturn($primaryCalendar); - // construct parameters - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // primary calendar should receive the event - $primaryCalendar->expects(self::once()) - ->method('handleIMipMessage') - ->with($userId, self::callback(function ($data) { - return str_contains($data, 'STATUS:TENTATIVE'); - })); - // test method with absent=create option - $result = $manager->handleIMip($userId, $calendar->serialize(), [ - 'absent' => 'create', - 'absentCreateStatus' => 'tentative', - ]); - // Assert - $this->assertTrue($result); - } - - public function testHandleImipWithAbsentCreateOverwritesExistingStatus(): void { - // construct mock user calendar (no matching event found) - $userCalendar = $this->createMock(ITestCalendar::class); - $userCalendar->expects(self::once()) - ->method('isDeleted') - ->willReturn(false); - $userCalendar->expects(self::once()) - ->method('isWritable') - ->willReturn(true); - $userCalendar->expects(self::once()) - ->method('search') - ->willReturn([]); - // construct mock calendar manager and returns - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['getCalendarsForPrincipal', 'getPrimaryCalendar']) - ->getMock(); - $manager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->willReturn([$userCalendar]); - $manager->expects(self::once()) - ->method('getPrimaryCalendar') - ->willReturn(null); - // construct parameters - calendar already has CONFIRMED status - $userId = 'attendee1'; - $calendar = $this->vCalendar1a; - $calendar->add('METHOD', 'REQUEST'); - // The original event has STATUS:CONFIRMED, but it should be overwritten to TENTATIVE - $userCalendar->expects(self::once()) - ->method('handleIMipMessage') - ->with($userId, self::callback(function ($data) { - // Should contain TENTATIVE and not CONFIRMED - return str_contains($data, 'STATUS:TENTATIVE') && !str_contains($data, 'STATUS:CONFIRMED'); - })); - // test method with absent=create option - $result = $manager->handleIMip($userId, $calendar->serialize(), [ - 'absent' => 'create', - 'absentCreateStatus' => 'tentative', - ]); - // Assert - $this->assertTrue($result); - } - - public function testhandleIMipRequestWithInvalidPrincipal() { - $invalidPrincipal = 'invalid-principal-uri'; - $sender = 'sender@example.com'; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar1a->serialize(); - - $this->logger->expects(self::once()) - ->method('error') - ->with('Invalid principal URI provided for iMip request'); - - $result = $this->manager->handleIMipRequest($invalidPrincipal, $sender, $recipient, $calendarData); - $this->assertFalse($result); - } - - public function testhandleIMipRequest() { - $principalUri = 'principals/users/attendee1'; - $sender = 'sender@example.com'; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar1a->serialize(); - - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['handleIMip']) - ->getMock(); - $manager->expects(self::once()) - ->method('handleIMip') - ->with('attendee1', $calendarData) - ->willReturn(true); - - $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendarData); - $this->assertTrue($result); - } - - public function testhandleIMipReplyWithInvalidPrincipal() { - $invalidPrincipal = 'invalid-principal-uri'; - $sender = 'sender@example.com'; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar2a->serialize(); - - $this->logger->expects(self::once()) - ->method('error') - ->with('Invalid principal URI provided for iMip reply'); - - $result = $this->manager->handleIMipReply($invalidPrincipal, $sender, $recipient, $calendarData); - $this->assertFalse($result); - } - - public function testhandleIMipReply() { - $principalUri = 'principals/users/attendee2'; - $sender = 'sender@example.com'; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar2a->serialize(); - - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['handleIMip']) - ->getMock(); - $manager->expects(self::once()) - ->method('handleIMip') - ->with('attendee2', $calendarData) - ->willReturn(true); - - $result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData); - $this->assertTrue($result); - } - - public function testhandleIMipCancelWithInvalidPrincipal() { - $invalidPrincipal = 'invalid-principal-uri'; - $sender = 'sender@example.com'; - $replyTo = null; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar3a->serialize(); - - $this->logger->expects(self::once()) - ->method('error') - ->with('Invalid principal URI provided for iMip cancel'); - - $result = $this->manager->handleIMipCancel($invalidPrincipal, $sender, $replyTo, $recipient, $calendarData); - $this->assertFalse($result); - } - - public function testhandleIMipCancel() { - $principalUri = 'principals/users/attendee3'; - $sender = 'sender@example.com'; - $replyTo = null; - $recipient = 'recipient@example.com'; - $calendarData = $this->vCalendar3a->serialize(); - - /** @var Manager&MockObject $manager */ - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->coordinator, - $this->container, - $this->logger, - $this->time, - $this->secureRandom, - $this->userManager, - $this->serverFactory, - $this->propertyMapper, - ]) - ->onlyMethods(['handleIMip']) - ->getMock(); - $manager->expects(self::once()) - ->method('handleIMip') - ->with('attendee3', $calendarData) - ->willReturn(true); - - $result = $manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData); - $this->assertTrue($result); - } - - private function getFreeBusyResponse(): string { - return << - - - - mailto:admin@imap.localhost - - 2.0;Success - BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -METHOD:REPLY -BEGIN:VFREEBUSY -DTSTART:20250116T060000Z -DTEND:20250117T060000Z -DTSTAMP:20250111T125634Z -FREEBUSY:20250116T060000Z/20250116T230000Z -FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z -ATTENDEE:mailto:admin@imap.localhost -UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 -ORGANIZER;CN=admin:mailto:admin@imap.localhost -END:VFREEBUSY -END:VCALENDAR - - - - - mailto:empty@imap.localhost - - 2.0;Success - BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -METHOD:REPLY -BEGIN:VFREEBUSY -DTSTART:20250116T060000Z -DTEND:20250117T060000Z -DTSTAMP:20250111T125634Z -ATTENDEE:mailto:empty@imap.localhost -UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 -ORGANIZER;CN=admin:mailto:admin@imap.localhost -END:VFREEBUSY -END:VCALENDAR - - - - - mailto:user@imap.localhost - - 2.0;Success - BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.6//EN -CALSCALE:GREGORIAN -METHOD:REPLY -BEGIN:VFREEBUSY -DTSTART:20250116T060000Z -DTEND:20250117T060000Z -DTSTAMP:20250111T125634Z -FREEBUSY:20250116T060000Z/20250116T230000Z -FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z -ATTENDEE:mailto:user@imap.localhost -UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 -ORGANIZER;CN=admin:mailto:admin@imap.localhost -END:VFREEBUSY -END:VCALENDAR - - - - - mailto:nouser@domain.tld - - 3.7;Could not find principal - - -EOF; - } - - public function testCheckAvailability(): void { - $organizer = $this->createMock(IUser::class); - $organizer->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $organizer->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('admin@imap.localhost'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - - $this->userManager->expects(self::exactly(3)) - ->method('getByEmail') - ->willReturnMap([ - ['user@imap.localhost', [$user1]], - ['empty@imap.localhost', [$user2]], - ['nouser@domain.tld', []], - ]); - - $authPlugin = $this->createMock(CustomPrincipalPlugin::class); - $authPlugin->expects(self::once()) - ->method('setCurrentPrincipal') - ->with('principals/users/admin'); - - $server = $this->createMock(Server::class); - $server->expects(self::once()) - ->method('getPlugin') - ->with('auth') - ->willReturn($authPlugin); - $server->expects(self::once()) - ->method('invokeMethod') - ->willReturnCallback(function ( - RequestInterface $request, - ResponseInterface $response, - bool $sendResponse, - ): void { - $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); - $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals('calendars/admin/outbox', $request->getPath()); - $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); - $this->assertEquals('0', $request->getHeader('Depth')); - $this->assertEquals($requestBody, $request->getBodyAsString()); - $this->assertFalse($sendResponse); - $response->setStatus(200); - $response->setBody($this->getFreeBusyResponse()); - }); - - $this->serverFactory->expects(self::once()) - ->method('createAttendeeAvailabilityServer') - ->willReturn($server); - - $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); - $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); - $actual = $this->manager->checkAvailability($start, $end, $organizer, [ - 'user@imap.localhost', - 'empty@imap.localhost', - 'nouser@domain.tld', - ]); - $expected = [ - new AvailabilityResult('admin@imap.localhost', false), - new AvailabilityResult('empty@imap.localhost', true), - new AvailabilityResult('user@imap.localhost', false), - ]; - $this->assertEquals($expected, $actual); - } - - public function testCheckAvailabilityWithMailtoPrefix(): void { - $organizer = $this->createMock(IUser::class); - $organizer->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $organizer->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('admin@imap.localhost'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - - $this->userManager->expects(self::exactly(3)) - ->method('getByEmail') - ->willReturnMap([ - ['user@imap.localhost', [$user1]], - ['empty@imap.localhost', [$user2]], - ['nouser@domain.tld', []], - ]); - - $authPlugin = $this->createMock(CustomPrincipalPlugin::class); - $authPlugin->expects(self::once()) - ->method('setCurrentPrincipal') - ->with('principals/users/admin'); - - $server = $this->createMock(Server::class); - $server->expects(self::once()) - ->method('getPlugin') - ->with('auth') - ->willReturn($authPlugin); - $server->expects(self::once()) - ->method('invokeMethod') - ->willReturnCallback(function ( - RequestInterface $request, - ResponseInterface $response, - bool $sendResponse, - ): void { - $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); - $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals('calendars/admin/outbox', $request->getPath()); - $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); - $this->assertEquals('0', $request->getHeader('Depth')); - $this->assertEquals($requestBody, $request->getBodyAsString()); - $this->assertFalse($sendResponse); - $response->setStatus(200); - $response->setBody($this->getFreeBusyResponse()); - }); - - $this->serverFactory->expects(self::once()) - ->method('createAttendeeAvailabilityServer') - ->willReturn($server); - - $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); - $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); - $actual = $this->manager->checkAvailability($start, $end, $organizer, [ - 'mailto:user@imap.localhost', - 'mailto:empty@imap.localhost', - 'mailto:nouser@domain.tld', - ]); - $expected = [ - new AvailabilityResult('admin@imap.localhost', false), - new AvailabilityResult('empty@imap.localhost', true), - new AvailabilityResult('user@imap.localhost', false), - ]; - $this->assertEquals($expected, $actual); - } - -} --- a/tests/lib/Calendar/Resource/ManagerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->server = $this->createMock(ContainerInterface::class); - $this->resourcesRoomsUpdater = $this->createMock(ResourcesRoomsUpdater::class); - - $this->manager = new Manager( - $this->coordinator, - $this->server, - $this->resourcesRoomsUpdater, - ); - } - - public function testGetBackendFromBootstrapRegistration(): void { - $backendClass = '\\OCA\\CalendarResourceFoo\\Backend'; - $backend = $this->createMock(IBackend::class); - $backend->method('getBackendIdentifier')->willReturn('from_bootstrap'); - $context = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($context); - $context->expects(self::once()) - ->method('getCalendarResourceBackendRegistrations') - ->willReturn([ - new ServiceRegistration('calendar_resource_foo', $backendClass) - ]); - $this->server->expects(self::once()) - ->method('get') - ->with($backendClass) - ->willReturn($backend); - - self::assertEquals($backend, $this->manager->getBackend('from_bootstrap')); - } - - public function testUpdate(): void { - $this->resourcesRoomsUpdater->expects(self::once()) - ->method('updateResources'); - - $this->manager->update(); - } -} --- a/tests/lib/Calendar/ResourcesRoomsUpdaterTest.php +++ /dev/null @@ -1,430 +0,0 @@ -resourceManager = $this->createMock(Resource\Manager::class); - $this->roomManager = $this->createMock(Room\Manager::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->calDavBackend = $this->createMock(CalDavBackend::class); - - $this->container->method('get') - ->willReturnMap([ - [Resource\Manager::class, $this->resourceManager], - [Room\Manager::class, $this->roomManager], - ]); - - $this->updater = new ResourcesRoomsUpdater( - $this->container, - self::$realDatabase, - $this->calDavBackend - ); - } - - #[\Override] - protected function tearDown(): void { - $query = self::$realDatabase->getQueryBuilder(); - $query->delete('calendar_resources')->executeStatement(); - $query->delete('calendar_resources_md')->executeStatement(); - $query->delete('calendar_rooms')->executeStatement(); - $query->delete('calendar_rooms_md')->executeStatement(); - parent::tearDown(); - } - - /** - * Data in Cache: - * resources: - * [backend1, res1, Beamer1, {}] - [] - * [backend1, res2, TV1, {}] - [] - * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] - * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] - * [backend3, res5, Beamer3, {}] - [] - * [backend3, res6, Pointer, {foo, bar}] - ['meta99' => 'value99'] - * - * Data in Backend: - * backend1 gone - * backend2 throws BackendTemporarilyUnavailableException - * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] - * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] - * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] - * [backend4, res9, Beamer2, {}] - [] - * - * Expected after run: - * [backend1, res1, Beamer1, {}] - [] - * [backend1, res2, TV1, {}] - [] - * [backend2, res3, Beamer2, {}] - ['meta1' => 'value1', 'meta2' => 'value2'] - * [backend2, res4, TV2, {}] - ['meta1' => 'value1', 'meta3' => 'value3-old'] - * [backend3, res6, Pointer123, {foo, biz}] - ['meta99' => 'value99-new', 'meta123' => 'meta456'] - * [backend3, res7, Resource4, {biz}] - ['meta1' => 'value1'] - * [backend4, res8, Beamer, {}] - ['meta2' => 'value2'] - * [backend4, res9, Beamer2, {}] - [] - */ - - public function testUpdateBoth(): void { - $this->createTestResourcesInCache(); - - $backend2 = $this->createMock(IBackend::class); - $backend3 = $this->createMock(IBackend::class); - $backend4 = $this->createMock(IBackend::class); - - $res6 = $this->createMock(tmpI::class); - $res7 = $this->createMock(tmpI::class); - $res8 = $this->createMock(tmpI::class); - $res9 = $this->createMock(IResource::class); - - $backend2->method('getBackendIdentifier') - ->willReturn('backend2'); - $backend2->method('listAllResources') - ->willThrowException(new BackendTemporarilyUnavailableException()); - $backend2->method('getResource') - ->willThrowException(new BackendTemporarilyUnavailableException()); - $backend2->method('getAllResources') - ->willThrowException(new BackendTemporarilyUnavailableException()); - $backend3->method('getBackendIdentifier') - ->willReturn('backend3'); - $backend3->method('listAllResources') - ->willReturn(['res6', 'res7']); - $backend3->method('getResource') - ->willReturnMap([ - ['res6', $res6], - ['res7', $res7], - ]); - $backend4->method('getBackendIdentifier') - ->willReturn('backend4'); - $backend4->method('listAllResources') - ->willReturn(['res8', 'res9']); - $backend4->method('getResource') - ->willReturnMap([ - ['res8', $res8], - ['res9', $res9], - ]); - - $res6->method('getId')->willReturn('res6'); - $res6->method('getDisplayName')->willReturn('Pointer123'); - $res6->method('getGroupRestrictions')->willReturn(['foo', 'biz']); - $res6->method('getEMail')->willReturn('res6@foo.bar'); - $res6->method('getBackend')->willReturn($backend3); - - $res6->method('getAllAvailableMetadataKeys')->willReturn(['meta99', 'meta123']); - $res6->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta99': - return 'value99-new'; - case 'meta123': - return 'meta456'; - default: - return null; - } - }); - - $res7->method('getId')->willReturn('res7'); - $res7->method('getDisplayName')->willReturn('Resource4'); - $res7->method('getGroupRestrictions')->willReturn(['biz']); - $res7->method('getEMail')->willReturn('res7@foo.bar'); - $res7->method('getBackend')->willReturn($backend3); - $res7->method('getAllAvailableMetadataKeys')->willReturn(['meta1']); - $res7->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta1': - return 'value1'; - default: - return null; - } - }); - - $res8->method('getId')->willReturn('res8'); - $res8->method('getDisplayName')->willReturn('Beamer'); - $res8->method('getGroupRestrictions')->willReturn([]); - $res8->method('getEMail')->willReturn('res8@foo.bar'); - $res8->method('getBackend')->willReturn($backend4); - $res8->method('getAllAvailableMetadataKeys')->willReturn(['meta2']); - $res8->method('getMetadataForKey')->willReturnCallback(function ($key) { - switch ($key) { - case 'meta2': - return 'value2'; - default: - return null; - } - }); - - $res9->method('getId')->willReturn('res9'); - $res9->method('getDisplayName')->willReturn('Beamer2'); - $res9->method('getGroupRestrictions')->willReturn([]); - $res9->method('getEMail')->willReturn('res9@foo.bar'); - $res9->method('getBackend')->willReturn($backend4); - - $this->resourceManager - ->method('getBackends') - ->willReturn([ - $backend2, $backend3, $backend4 - ]); - $this->resourceManager - ->method('getBackend') - ->willReturnMap([ - ['backend2', $backend2], - ['backend3', $backend3], - ['backend4', $backend4], - ]); - - $this->updater->updateResources(); - $this->updater->updateRooms(); - - $query = self::$realDatabase->getQueryBuilder(); - $query->select('*')->from('calendar_resources'); - - $rows = []; - $ids = []; - $stmt = $query->executeQuery(); - while ($row = $stmt->fetchAssociative()) { - $ids[$row['backend_id'] . '::' . $row['resource_id']] = $row['id']; - unset($row['id']); - $rows[] = $row; - } - - $this->assertEquals([ - [ - 'backend_id' => 'backend1', - 'resource_id' => 'res1', - 'displayname' => 'Beamer1', - 'email' => 'res1@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend1', - 'resource_id' => 'res2', - 'displayname' => 'TV1', - 'email' => 'res2@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend2', - 'resource_id' => 'res3', - 'displayname' => 'Beamer2', - 'email' => 'res3@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend2', - 'resource_id' => 'res4', - 'displayname' => 'TV2', - 'email' => 'res4@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend3', - 'resource_id' => 'res6', - 'displayname' => 'Pointer123', - 'email' => 'res6@foo.bar', - 'group_restrictions' => '["foo","biz"]', - ], - [ - 'backend_id' => 'backend3', - 'resource_id' => 'res7', - 'displayname' => 'Resource4', - 'email' => 'res7@foo.bar', - 'group_restrictions' => '["biz"]', - ], - [ - 'backend_id' => 'backend4', - 'resource_id' => 'res8', - 'displayname' => 'Beamer', - 'email' => 'res8@foo.bar', - 'group_restrictions' => '[]', - ], - [ - 'backend_id' => 'backend4', - 'resource_id' => 'res9', - 'displayname' => 'Beamer2', - 'email' => 'res9@foo.bar', - 'group_restrictions' => '[]', - ], - ], $rows); - - $query2 = self::$realDatabase->getQueryBuilder(); - $query2->select('*')->from('calendar_resources_md'); - - $rows2 = []; - $stmt = $query2->executeQuery(); - while ($row = $stmt->fetchAssociative()) { - unset($row['id']); - $rows2[] = $row; - } - - $this->assertEquals([ - [ - 'resource_id' => $ids['backend2::res3'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend2::res3'], - 'key' => 'meta2', - 'value' => 'value2', - ], - [ - 'resource_id' => $ids['backend2::res4'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend2::res4'], - 'key' => 'meta3', - 'value' => 'value3-old', - ], - [ - 'resource_id' => $ids['backend3::res6'], - 'key' => 'meta99', - 'value' => 'value99-new', - ], - [ - 'resource_id' => $ids['backend3::res7'], - 'key' => 'meta1', - 'value' => 'value1', - ], - [ - 'resource_id' => $ids['backend3::res6'], - 'key' => 'meta123', - 'value' => 'meta456', - ], - [ - 'resource_id' => $ids['backend4::res8'], - 'key' => 'meta2', - 'value' => 'value2', - ] - ], $rows2); - } - - protected function createTestResourcesInCache() { - $query = self::$realDatabase->getQueryBuilder(); - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend1'), - 'resource_id' => $query->createNamedParameter('res1'), - 'email' => $query->createNamedParameter('res1@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer1'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->executeStatement(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend1'), - 'resource_id' => $query->createNamedParameter('res2'), - 'email' => $query->createNamedParameter('res2@foo.bar'), - 'displayname' => $query->createNamedParameter('TV1'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->executeStatement(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend2'), - 'resource_id' => $query->createNamedParameter('res3'), - 'email' => $query->createNamedParameter('res3@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer2'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->executeStatement(); - $id3 = $query->getLastInsertId(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend2'), - 'resource_id' => $query->createNamedParameter('res4'), - 'email' => $query->createNamedParameter('res4@foo.bar'), - 'displayname' => $query->createNamedParameter('TV2'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->executeStatement(); - $id4 = $query->getLastInsertId(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend3'), - 'resource_id' => $query->createNamedParameter('res5'), - 'email' => $query->createNamedParameter('res5@foo.bar'), - 'displayname' => $query->createNamedParameter('Beamer3'), - 'group_restrictions' => $query->createNamedParameter('[]'), - ]) - ->executeStatement(); - - $query->insert('calendar_resources') - ->values([ - 'backend_id' => $query->createNamedParameter('backend3'), - 'resource_id' => $query->createNamedParameter('res6'), - 'email' => $query->createNamedParameter('res6@foo.bar'), - 'displayname' => $query->createNamedParameter('Pointer'), - 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'), - ]) - ->executeStatement(); - $id6 = $query->getLastInsertId(); - - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id3), - 'key' => $query->createNamedParameter('meta1'), - 'value' => $query->createNamedParameter('value1') - ]) - ->executeStatement(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id3), - 'key' => $query->createNamedParameter('meta2'), - 'value' => $query->createNamedParameter('value2') - ]) - ->executeStatement(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id4), - 'key' => $query->createNamedParameter('meta1'), - 'value' => $query->createNamedParameter('value1') - ]) - ->executeStatement(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id4), - 'key' => $query->createNamedParameter('meta3'), - 'value' => $query->createNamedParameter('value3-old') - ]) - ->executeStatement(); - $query->insert('calendar_resources_md') - ->values([ - 'resource_id' => $query->createNamedParameter($id6), - 'key' => $query->createNamedParameter('meta99'), - 'value' => $query->createNamedParameter('value99') - ]) - ->executeStatement(); - } -} --- a/tests/lib/Calendar/Room/ManagerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->server = $this->createMock(ContainerInterface::class); - $this->resourcesRoomsUpdater = $this->createMock(ResourcesRoomsUpdater::class); - - $this->manager = new Manager( - $this->coordinator, - $this->server, - $this->resourcesRoomsUpdater, - ); - } - - public function testGetBackendFromBootstrapRegistration(): void { - $backendClass = '\\OCA\\CalendarRoomFoo\\Backend'; - $backend = $this->createMock(IBackend::class); - $backend->method('getBackendIdentifier')->willReturn('from_bootstrap'); - $context = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($context); - $context->expects(self::once()) - ->method('getCalendarRoomBackendRegistrations') - ->willReturn([ - new ServiceRegistration('calendar_room_foo', $backendClass) - ]); - $this->server->expects(self::once()) - ->method('get') - ->with($backendClass) - ->willReturn($backend); - - self::assertEquals($backend, $this->manager->getBackend('from_bootstrap')); - } - - public function testUpdate(): void { - $this->resourcesRoomsUpdater->expects(self::once()) - ->method('updateRooms'); - - $this->manager->update(); - } -} --- a/tests/lib/CapabilitiesManagerTest.php +++ /dev/null @@ -1,202 +0,0 @@ -logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $this->manager = new CapabilitiesManager($this->logger); - } - - /** - * Test no capabilities - */ - public function testNoCapabilities(): void { - $res = $this->manager->getCapabilities(); - $this->assertEmpty($res); - } - - /** - * Test a valid capabilitie - */ - public function testValidCapability(): void { - $this->manager->registerCapability(function () { - return new SimpleCapability(); - }); - - $res = $this->manager->getCapabilities(); - $this->assertEquals(['foo' => 1], $res); - } - - /** - * Test a public capabilitie - */ - public function testPublicCapability(): void { - $this->manager->registerCapability(function () { - return new PublicSimpleCapability1(); - }); - $this->manager->registerCapability(function () { - return new SimpleCapability2(); - }); - $this->manager->registerCapability(function () { - return new SimpleCapability3(); - }); - - $res = $this->manager->getCapabilities(true); - $this->assertEquals(['foo' => 1], $res); - } - - /** - * Test that we need something that implents ICapability - */ - public function testNoICapability(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The given Capability (Test\\NoCapability) does not implement the ICapability interface'); - - $this->manager->registerCapability(function () { - return new NoCapability(); - }); - - $res = $this->manager->getCapabilities(); - $this->assertEquals([], $res); - } - - /** - * Test a bunch of merged Capabilities - */ - public function testMergedCapabilities(): void { - $this->manager->registerCapability(function () { - return new SimpleCapability(); - }); - $this->manager->registerCapability(function () { - return new SimpleCapability2(); - }); - $this->manager->registerCapability(function () { - return new SimpleCapability3(); - }); - - $res = $this->manager->getCapabilities(); - $expected = [ - 'foo' => 1, - 'bar' => [ - 'x' => 1, - 'y' => 2 - ] - ]; - - $this->assertEquals($expected, $res); - } - - /** - * Test deep identical capabilities - */ - public function testDeepIdenticalCapabilities(): void { - $this->manager->registerCapability(function () { - return new DeepCapability(); - }); - $this->manager->registerCapability(function () { - return new DeepCapability(); - }); - - $res = $this->manager->getCapabilities(); - $expected = [ - 'foo' => [ - 'bar' => [ - 'baz' => true - ] - ] - ]; - - $this->assertEquals($expected, $res); - } - - public function testInvalidCapability(): void { - $this->manager->registerCapability(function (): void { - throw new QueryException(); - }); - - $this->logger->expects($this->once()) - ->method('error'); - - $res = $this->manager->getCapabilities(); - - $this->assertEquals([], $res); - } -} - -class SimpleCapability implements ICapability { - #[\Override] - public function getCapabilities() { - return [ - 'foo' => 1 - ]; - } -} - -class SimpleCapability2 implements ICapability { - #[\Override] - public function getCapabilities() { - return [ - 'bar' => ['x' => 1] - ]; - } -} - -class SimpleCapability3 implements ICapability { - #[\Override] - public function getCapabilities() { - return [ - 'bar' => ['y' => 2] - ]; - } -} - -class PublicSimpleCapability1 implements IPublicCapability { - #[\Override] - public function getCapabilities() { - return [ - 'foo' => 1 - ]; - } -} - -class NoCapability { - public function getCapabilities() { - return [ - 'baz' => 'z' - ]; - } -} - -class DeepCapability implements ICapability { - #[\Override] - public function getCapabilities() { - return [ - 'foo' => [ - 'bar' => [ - 'baz' => true - ] - ] - ]; - } -} --- a/tests/lib/Collaboration/Collaborators/GroupPluginTest.php +++ /dev/null @@ -1,489 +0,0 @@ -config = $this->createMock(IConfig::class); - - $this->groupManager = $this->createMock(IGroupManager::class); - - $this->session = $this->createMock(IUserSession::class); - - $this->searchResult = new SearchResult(); - - $this->user = $this->getUserMock('admin', 'Administrator'); - } - - public function instantiatePlugin(): void { - // cannot be done within setUp, because dependent mocks needs to be set - // up with configuration etc. first - $this->plugin = new GroupPlugin( - $this->config, - $this->groupManager, - $this->session - ); - } - - public function getUserMock(string $uid, string $displayName): IUser&MockObject { - $user = $this->createMock(IUser::class); - - $user->expects($this->any()) - ->method('getUID') - ->willReturn($uid); - - $user->expects($this->any()) - ->method('getDisplayName') - ->willReturn($displayName); - - return $user; - } - - protected function getGroupMock(string $gid, ?string $displayName = null, bool $hide = false): IGroup&MockObject { - $group = $this->createMock(IGroup::class); - - $group->expects($this->any()) - ->method('getGID') - ->willReturn($gid); - - if (is_null($displayName)) { - // note: this is how the Group class behaves - $displayName = $gid; - } - - $group->expects($this->any()) - ->method('getDisplayName') - ->willReturn($displayName); - - $group->method('hideFromCollaboration') - ->willReturn($hide); - - return $group; - } - - public static function dataGetGroups(): array { - return [ - ['test', false, true, false, [], [], [], [], true, false], - ['test', false, false, false, [], [], [], [], true, false], - // group sharing disabled - ['test', false, true, true, [], [], [], [], false, false], - // group without display name - [ - 'test', false, true, false, - [['test1']], - [], - [], - [['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - true, - false, - ], - // group with display name, search by id - [ - 'test', false, true, false, - [['test1', 'Test One']], - [], - [], - [['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - true, - false, - ], - // group with display name, search by display name - [ - 'one', false, true, false, - [['test1', 'Test One']], - [], - [], - [['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - true, - false, - ], - // group with display name, search by display name, exact expected - [ - 'Test One', false, true, false, - [['test1', 'Test One']], - [], - [['label' => 'Test One', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - [], - true, - false, - ], - [ - 'test', false, false, false, - [['test1']], - [], - [], - [], - true, - false, - ], - [ - 'test', false, true, false, - [ - ['test'], - ['test1'], - ], - [], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - false, - false, - ], - [ - 'test', false, false, false, - [ - ['test'], - ['test1'], - ], - [], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [], - true, - false, - ], - [ - 'test', false, true, false, - [ - ['test0'], - ['test1'], - ], - [], - [], - [ - ['label' => 'test0', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test0']], - ['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']], - ], - false, - false, - ], - [ - 'test', false, false, false, - [ - ['test0'], - ['test1'], - ], - [], - [], - [], - true, - false, - ], - [ - 'test', false, true, false, - [ - ['test0'], - ['test1'], - ], - [], - [ - ['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']], - ], - [ - ['label' => 'test0', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test0']], - ['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']], - ], - false, - ['test'], - ], - [ - 'test', false, false, false, - [ - ['test0'], - ['test1'], - ], - [], - [ - ['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']], - ], - [], - true, - ['test'], - ], - ['test', true, true, false, [], [], [], [], true, false], - ['test', true, false, false, [], [], [], [], true, false], - [ - 'test', true, true, false, - [ - ['test1'], - ['test2'], - ], - [['test1']], - [], - [['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - false, - false, - ], - [ - 'test', true, false, false, - [ - ['test1'], - ['test2'], - ], - [['test1']], - [], - [], - true, - false, - ], - [ - 'test', true, true, false, - [ - ['test'], - ['test1'], - ], - [['test']], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [], - false, - false, - ], - [ - 'test', true, false, false, - [ - ['test'], - ['test1'], - ], - [['test']], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [], - true, - false, - ], - [ - 'test', true, true, false, - [ - ['test'], - ['test1'], - ], - [['test1']], - [], - [['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - false, - false, - ], - [ - 'test', true, false, false, - [ - ['test'], - ['test1'], - ], - [['test1']], - [], - [], - true, - false, - ], - [ - 'test', true, true, false, - [ - ['test'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']]], - false, - false, - ], - [ - 'test', true, false, false, - [ - ['test'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']]], - [], - true, - false, - ], - [ - 'test', true, true, false, - [ - ['test0'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [], - [ - ['label' => 'test0', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test0']], - ['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']], - ], - false, - false, - ], - [ - 'test', true, false, false, - [ - ['test0'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [], - [], - true, - false, - ], - [ - 'test', true, true, false, - [ - ['test0'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [ - ['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']], - ], - [ - ['label' => 'test0', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test0']], - ['label' => 'test1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test1']], - ], - false, - ['test'], - ], - [ - 'test', true, false, false, - [ - ['test0'], - ['test1'], - ], - [['test'], ['test0'], ['test1']], - [ - ['label' => 'test', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'test']], - ], - [], - true, - ['test'], - ], - [ - 'test', false, false, false, - [ - ['test', null, true], - ['test1'], - ], - [], - [], - [], - true, - false, - ], - ]; - } - - #[DataProvider('dataGetGroups')] - public function testSearch( - string $searchTerm, - bool $shareWithGroupOnly, - bool $shareeEnumeration, - bool $groupSharingDisabled, - array $groupResponse, - array $userGroupsResponse, - array $exactExpected, - array $expected, - bool $reachedEnd, - array|false $singleGroup, - ): void { - $groupResponse = array_map( - fn ($args) => $this->getGroupMock(...$args), - $groupResponse - ); - if (is_array($singleGroup)) { - $singleGroup = $this->getGroupMock(...$singleGroup); - } - $userGroupsResponse = array_map( - fn ($args) => $this->getGroupMock(...$args), - $userGroupsResponse - ); - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) use ($shareWithGroupOnly, $shareeEnumeration, $groupSharingDisabled) { - if ($appName !== 'core') { - return $default; - } - switch ($key) { - case 'shareapi_only_share_with_group_members': - return $shareWithGroupOnly ? 'yes' : 'no'; - case 'shareapi_allow_share_dialog_user_enumeration': - return $shareeEnumeration ? 'yes' : 'no'; - case 'shareapi_allow_group_sharing': - return $groupSharingDisabled ? 'no' : 'yes'; - default: - return $default; - } - } - ); - - $this->instantiatePlugin(); - - if (!$groupSharingDisabled) { - $this->groupManager->expects($this->once()) - ->method('search') - ->with($searchTerm, $this->limit, $this->offset) - ->willReturn($groupResponse); - } - - if ($singleGroup !== false) { - $this->groupManager->expects($this->once()) - ->method('get') - ->with($searchTerm) - ->willReturn($singleGroup); - } - - if ($shareWithGroupOnly) { - $this->session->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $numGetUserGroupsCalls = empty($groupResponse) ? 0 : 1; - $this->groupManager->expects($this->exactly($numGetUserGroupsCalls)) - ->method('getUserGroups') - ->with($this->user) - ->willReturn($userGroupsResponse); - } - - $moreResults = $this->plugin->search($searchTerm, $this->limit, $this->offset, $this->searchResult); - $result = $this->searchResult->asArray(); - - if (!$groupSharingDisabled) { - $this->assertEquals($exactExpected, $result['exact']['groups']); - $this->assertEquals($expected, $result['groups']); - } - $this->assertSame($reachedEnd, $moreResults); - } -} --- a/tests/lib/Collaboration/Collaborators/LookupPluginTest.php +++ /dev/null @@ -1,539 +0,0 @@ -userSession = $this->createMock(IUserSession::class); - $this->cloudIdManager = $this->createMock(ICloudIdManager::class); - $this->config = $this->createMock(IConfig::class); - $this->globalScaleConfig = $this->createMock(GlobalScaleConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->clientService = $this->createMock(IClientService::class); - $cloudId = $this->createMock(ICloudId::class); - $cloudId->expects($this->any())->method('getRemote')->willReturn('myNextcloud.net'); - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getCloudId')->willReturn('user@myNextcloud.net'); - $this->userSession->expects($this->any())->method('getUser') - ->willReturn($user); - $this->cloudIdManager->expects($this->any())->method('resolveCloudId') - ->willReturnCallback(function ($cloudId) { - if ($cloudId === 'user@myNextcloud.net') { - return new CloudId('user@myNextcloud.net', 'user', 'myNextcloud.net'); - } - if (str_contains($cloudId, '@enceladus.moon')) { - [$user, $remote] = explode('@', $cloudId, 2); - return new CloudId($cloudId, $user, $remote); - } - return new CloudId('user@someNextcloud.net', 'user', 'someNextcloud.net'); - }); - - $this->plugin = new LookupPlugin( - $this->config, - $this->clientService, - $this->userSession, - $this->cloudIdManager, - $this->logger, - null, - $this->globalScaleConfig, - ); - } - - public function testSearchNoLookupServerURI(): void { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('files_sharing', 'lookupServerEnabled', 'no') - ->willReturn('yes'); - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, true], - ]); - - $this->globalScaleConfig->expects($this->once()) - ->method('isGlobalScaleEnabled') - ->willReturn(true); - - $this->config->expects($this->once()) - ->method('getSystemValueString') - ->with('lookup_server', 'https://lookup.nextcloud.com') - ->willReturn(''); - - $this->clientService->expects($this->never()) - ->method('newClient'); - - /** @var ISearchResult|MockObject $searchResult */ - $searchResult = $this->createMock(ISearchResult::class); - - $this->plugin->search('foobar', 10, 0, $searchResult); - } - - public function testSearchNoInternet(): void { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('files_sharing', 'lookupServerEnabled', 'no') - ->willReturn('yes'); - $this->config->expects($this->exactly(1)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, false], - ]); - - $this->globalScaleConfig->expects($this->exactly(1)) - ->method('isGlobalScaleEnabled') - ->willReturn(false); - - $this->clientService->expects($this->never()) - ->method('newClient'); - - /** @var ISearchResult|MockObject $searchResult */ - $searchResult = $this->createMock(ISearchResult::class); - - $this->plugin->search('foobar', 10, 0, $searchResult); - } - - /** - * @param array $searchParams - */ - #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')] - public function testSearch(array $searchParams): void { - $type = new SearchResultType('lookup'); - - /** @var ISearchResult|MockObject $searchResult */ - $searchResult = $this->createMock(ISearchResult::class); - $searchResult->expects($this->once()) - ->method('addResultSet') - ->with($type, $searchParams['expectedResult'], []); - - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('files_sharing', 'lookupServerEnabled', 'no') - ->willReturn('yes'); - $this->config->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, true], - ['gss.selfsigned.allow', false, false], - ]); - - $this->globalScaleConfig->expects($this->once()) - ->method('isGlobalScaleEnabled') - ->willReturn(true); - - $this->config->expects($this->once()) - ->method('getSystemValueString') - ->with('lookup_server', 'https://lookup.nextcloud.com') - ->willReturn($searchParams['server']); - - $response = $this->createMock(IResponse::class); - $response->expects($this->once()) - ->method('getBody') - ->willReturn(json_encode($searchParams['resultBody'])); - - $client = $this->createMock(IClient::class); - $client->expects($this->once()) - ->method('get') - ->willReturnCallback(function ($url) use ($searchParams, $response) { - $this->assertSame(strpos($url, $searchParams['server'] . '/users?search='), 0); - $this->assertNotFalse(strpos($url, urlencode($searchParams['search']))); - return $response; - }); - - $this->clientService->expects($this->once()) - ->method('newClient') - ->willReturn($client); - - $moreResults = $this->plugin->search( - $searchParams['search'], - $searchParams['limit'], - $searchParams['offset'], - $searchResult - ); - - $this->assertFalse($moreResults); - } - - /** - * @param array $searchParams - * @param bool $GSEnabled - * @param bool $LookupEnabled - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchEnableDisableLookupServer')] - public function testSearchEnableDisableLookupServer(array $searchParams, $GSEnabled, $LookupEnabled): void { - $type = new SearchResultType('lookup'); - - /** @var ISearchResult|MockObject $searchResult */ - $searchResult = $this->createMock(ISearchResult::class); - - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('files_sharing', 'lookupServerEnabled', 'no') - ->willReturn($LookupEnabled ? 'yes' : 'no'); - if ($GSEnabled) { - $searchResult->expects($this->once()) - ->method('addResultSet') - ->with($type, $searchParams['expectedResult'], []); - - $this->config->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, true], - ['gss.selfsigned.allow', false, false], - ]); - - $this->globalScaleConfig->expects($this->once()) - ->method('isGlobalScaleEnabled') - ->willReturn($GSEnabled); - $this->config->expects($this->once()) - ->method('getSystemValueString') - ->with('lookup_server', 'https://lookup.nextcloud.com') - ->willReturn($searchParams['server']); - - $response = $this->createMock(IResponse::class); - $response->expects($this->once()) - ->method('getBody') - ->willReturn(json_encode($searchParams['resultBody'])); - - $client = $this->createMock(IClient::class); - $client->expects($this->once()) - ->method('get') - ->willReturnCallback(function ($url) use ($searchParams, $response) { - $this->assertSame(strpos($url, $searchParams['server'] . '/users?search='), 0); - $this->assertNotFalse(strpos($url, urlencode($searchParams['search']))); - return $response; - }); - - $this->clientService->expects($this->once()) - ->method('newClient') - ->willReturn($client); - } else { - $searchResult->expects($this->never())->method('addResultSet'); - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, true], - ]); - - $this->globalScaleConfig->expects($this->once()) - ->method('isGlobalScaleEnabled') - ->willReturn($GSEnabled); - } - $moreResults = $this->plugin->search( - $searchParams['search'], - $searchParams['limit'], - $searchParams['offset'], - $searchResult - ); - - $this->assertFalse($moreResults); - } - - public function testSearchGSDisabled(): void { - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->willReturnMap([ - ['has_internet_connection', true, true], - ]); - - $this->globalScaleConfig->expects($this->once()) - ->method('isGlobalScaleEnabled') - ->willReturn(false); - - /** @var ISearchResult|MockObject $searchResult */ - $searchResult = $this->createMock(ISearchResult::class); - $searchResult->expects($this->never()) - ->method('addResultSet'); - $searchResult->expects($this->never()) - ->method('markExactIdMatch'); - - $this->assertFalse($this->plugin->search('irr', 10, 0, $searchResult)); - } - - public static function dataSearchEnableDisableLookupServer(): array { - $fedIDs = [ - 'foo@enceladus.moon', - 'foobar@enceladus.moon', - 'foongus@enceladus.moon', - ]; - - return [ - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [ - ['federationId' => $fedIDs[0]], - ['federationId' => $fedIDs[1]], - ['federationId' => $fedIDs[2]], - ], - 'expectedResult' => [ - [ - 'label' => $fedIDs[0], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[0], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[0]], - ], - [ - 'label' => $fedIDs[1], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[1], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[1]], - ], - [ - 'label' => $fedIDs[2], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[2], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[2]], - ], - ] - ],// GS , Lookup - true, true - ], - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [ - ['federationId' => $fedIDs[0]], - ['federationId' => $fedIDs[1]], - ['federationId' => $fedIDs[2]], - ], - 'expectedResult' => [ - [ - 'label' => $fedIDs[0], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[0], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[0]], - ], - [ - 'label' => $fedIDs[1], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[1], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[1]], - ], - [ - 'label' => $fedIDs[2], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[2], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[2]], - ], - ] - ],// GS , Lookup - true, false - ], - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [ - ['federationId' => $fedIDs[0]], - ['federationId' => $fedIDs[1]], - ['federationId' => $fedIDs[2]], - ], - 'expectedResult' => [ - [ - 'label' => $fedIDs[0], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[0] - ], - 'extra' => ['federationId' => $fedIDs[0]], - ], - [ - 'label' => $fedIDs[1], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[1] - ], - 'extra' => ['federationId' => $fedIDs[1]], - ], - [ - 'label' => $fedIDs[2], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[2] - ], - 'extra' => ['federationId' => $fedIDs[2]], - ], - ] - ],// GS , Lookup - false, true - ], - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [ - ['federationId' => $fedIDs[0]], - ['federationId' => $fedIDs[1]], - ['federationId' => $fedIDs[2]], - ], - 'expectedResult' => [ - [ - 'label' => $fedIDs[0], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[0] - ], - 'extra' => ['federationId' => $fedIDs[0]], - ], - [ - 'label' => $fedIDs[1], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[1] - ], - 'extra' => ['federationId' => $fedIDs[1]], - ], - [ - 'label' => $fedIDs[2], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'shareWith' => $fedIDs[2] - ], - 'extra' => ['federationId' => $fedIDs[2]], - ], - ] - ],// GS , Lookup - false, false - ], - ]; - } - - public static function searchDataProvider(): array { - $fedIDs = [ - 'foo@enceladus.moon', - 'foobar@enceladus.moon', - 'foongus@enceladus.moon', - ]; - - return [ - // #0, standard search with results - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [ - ['federationId' => $fedIDs[0]], - ['federationId' => $fedIDs[1]], - ['federationId' => $fedIDs[2]], - ], - 'expectedResult' => [ - [ - 'label' => $fedIDs[0], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[0], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[0]], - ], - [ - 'label' => $fedIDs[1], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[1], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[1]], - ], - [ - 'label' => $fedIDs[2], - 'value' => [ - 'shareType' => IShare::TYPE_REMOTE, - 'globalScale' => true, - 'shareWith' => $fedIDs[2], - 'server' => 'enceladus.moon', - 'isTrustedServer' => false, - ], - 'extra' => ['federationId' => $fedIDs[2]], - ], - ] - ]], - // #1, search without results - [[ - 'search' => 'foo', - 'limit' => 10, - 'offset' => 0, - 'server' => 'https://lookup.example.io', - 'resultBody' => [], - 'expectedResult' => [], - ]], - ]; - } -} --- a/tests/lib/Collaboration/Collaborators/MailPluginTest.php +++ /dev/null @@ -1,1078 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->contactsManager = $this->createMock(IManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->knownUserService = $this->createMock(KnownUserService::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->userManager - ->method('get') - ->willReturnCallback(function (string $uid): IUser { - $user = $this->createMock(IUser::class); - $user - ->method('getUID') - ->willReturn($uid); - - return $user; - }); - $this->cloudIdManager = new CloudIdManager( - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->contactsManager, - $this->createMock(IURLGenerator::class), - $this->createMock(IUserManager::class), - ); - - $this->searchResult = new SearchResult(); - } - - public function instantiatePlugin(int $shareType) { - $this->plugin = new MailPlugin( - $this->contactsManager, - $this->cloudIdManager, - $this->config, - $this->groupManager, - $this->knownUserService, - $this->userSession, - $this->getEmailValidatorWithStrictEmailCheck(), - $this->userManager, - [], - $shareType, - ); - } - - #[DataProvider('dataSearchEmail')] - public function testSearchEmail(string $searchTerm, array $contacts, bool $shareeEnumeration, array $expectedResult, bool $expectedExactIdMatch, bool $expectedMoreResults, bool $validEmail): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) use ($shareeEnumeration) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return $shareeEnumeration ? 'yes' : 'no'; - } - return $default; - } - ); - - $this->instantiatePlugin(IShare::TYPE_EMAIL); - - $currentUser = $this->createMock(IUser::class); - $currentUser->method('getUID') - ->willReturn('current'); - $this->userSession->method('getUser') - ->willReturn($currentUser); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($search, $searchAttributes) use ($searchTerm, $contacts) { - if ($search === $searchTerm) { - return $contacts; - } - return []; - }); - - $moreResults = $this->plugin->search($searchTerm, 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertSame($expectedExactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('emails'))); - $this->assertEquals($expectedResult, $result); - $this->assertSame($expectedMoreResults, $moreResults); - } - - public static function dataSearchEmail(): array { - return [ - // data set 0 - ['test', [], true, ['emails' => [], 'exact' => ['emails' => []]], false, false, false], - // data set 1 - ['test', [], false, ['emails' => [], 'exact' => ['emails' => []]], false, false, false], - // data set 2 - [ - 'test@remote.com', - [], - true, - ['emails' => [], 'exact' => ['emails' => [['uuid' => 'test@remote.com', 'label' => 'test@remote.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@remote.com']]]]], - false, - false, - true, - ], - // data set 3 - [ // no valid email address - 'test@remote', - [], - true, - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 4 - [ - 'test@remote.com', - [], - false, - ['emails' => [], 'exact' => ['emails' => [['uuid' => 'test@remote.com', 'label' => 'test@remote.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@remote.com']]]]], - false, - false, - true, - ], - // data set 5 - [ - 'test', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@example.com', - ], - ], - ], - true, - ['emails' => [['uuid' => 'uid1', 'name' => 'User @ Localhost', 'type' => '', 'label' => 'User @ Localhost (username@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'username@example.com']]], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 6 - [ - 'test', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'isLocalSystemBook' => true, - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - false, - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 7 - [ - 'test@remote.com', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ example.com', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ example.com', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ example.com', - 'EMAIL' => [ - 'username@example.com', - ], - ], - ], - true, - ['emails' => [['uuid' => 'uid1', 'name' => 'User @ example.com', 'type' => '', 'label' => 'User @ example.com (username@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'username@example.com']]], 'exact' => ['emails' => [['label' => 'test@remote.com', 'uuid' => 'test@remote.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@remote.com']]]]], - false, - false, - true, - ], - // data set 8 - [ - 'test@remote.com', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'isLocalSystemBook' => true, - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - false, - ['emails' => [], 'exact' => ['emails' => [['label' => 'test@remote.com', 'uuid' => 'test@remote.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@remote.com']]]]], - false, - false, - true, - ], - // data set 9 - [ - 'username@example.com', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ example.com', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ example.com', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ example.com', - 'EMAIL' => [ - 'username@example.com', - ], - ], - ], - true, - ['emails' => [], 'exact' => ['emails' => [['name' => 'User @ example.com', 'uuid' => 'uid1', 'type' => '', 'label' => 'User @ example.com (username@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'username@example.com']]]]], - true, - false, - false, - ], - // data set 10 - [ - 'username@example.com', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User3 @ example.com', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ example.com', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ example.com', - 'EMAIL' => [ - 'username@example.com', - ], - ], - ], - false, - ['emails' => [], 'exact' => ['emails' => [['name' => 'User @ example.com', 'uuid' => 'uid1', 'type' => '', 'label' => 'User @ example.com (username@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'username@example.com']]]]], - true, - false, - false, - ], - // data set 11 - // contact with space - [ - 'user name@localhost', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User Name @ Localhost', - 'EMAIL' => [ - 'user name@localhost', - ], - ], - ], - false, - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 12 - // remote with space, no contact - [ - 'user space@remote.com', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'isLocalSystemBook' => true, - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - false, - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 13 - // Local user found by email => no result - [ - 'test@example.com', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - ] - ], - false, - ['exact' => []], - false, - false, - true, - ], - // data set 14 - // Current local user found by email => no result - [ - 'test@example.com', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['current@localhost'], - 'isLocalSystemBook' => true, - ] - ], - true, - ['exact' => []], - false, - false, - true, - ], - // data set 15 - // Several local users found by email => no result nor pagination - [ - 'test@example', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User1', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test1@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2', - 'EMAIL' => ['test@example.de'], - 'CLOUD' => ['test2@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid3', - 'FN' => 'User3', - 'EMAIL' => ['test@example.org'], - 'CLOUD' => ['test3@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid4', - 'FN' => 'User4', - 'EMAIL' => ['test@example.net'], - 'CLOUD' => ['test4@localhost'], - 'isLocalSystemBook' => true, - ], - ], - true, - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - false, - ], - // data set 16 - // Pagination and "more results" for normal emails - [ - 'test@example', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User1', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test1@localhost'], - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2', - 'EMAIL' => ['test@example.de'], - 'CLOUD' => ['test2@localhost'], - ], - [ - 'UID' => 'uid3', - 'FN' => 'User3', - 'EMAIL' => ['test@example.org'], - 'CLOUD' => ['test3@localhost'], - ], - [ - 'UID' => 'uid4', - 'FN' => 'User4', - 'EMAIL' => ['test@example.net'], - 'CLOUD' => ['test4@localhost'], - ], - ], - true, - ['emails' => [ - ['uuid' => 'uid1', 'name' => 'User1', 'type' => '', 'label' => 'User1 (test@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@example.com']], - ['uuid' => 'uid2', 'name' => 'User2', 'type' => '', 'label' => 'User2 (test@example.de)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@example.de']], - ], 'exact' => ['emails' => []]], - false, - true, - false, - ], - // data set 17 - // multiple email addresses with type - [ - 'User Name', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User Name', - 'EMAIL' => [ - ['type' => 'HOME', 'value' => 'username@example.com'], - ['type' => 'WORK', 'value' => 'other@example.com'], - ], - ], - ], - false, - ['emails' => [ - ], 'exact' => ['emails' => [ - ['name' => 'User Name', 'uuid' => 'uid1', 'type' => 'HOME', 'label' => 'User Name (username@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'username@example.com']], - ['name' => 'User Name', 'uuid' => 'uid1', 'type' => 'WORK', 'label' => 'User Name (other@example.com)', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'other@example.com']] - ]]], - false, - false, - false, - ], - // data set 18 - // idn email - [ - 'test@lölölölölölölöl.com', - [], - true, - ['emails' => [], 'exact' => ['emails' => [['uuid' => 'test@lölölölölölölöl.com', 'label' => 'test@lölölölölölölöl.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@lölölölölölölöl.com']]]]], - false, - false, - true, - ], - ]; - } - - #[DataProvider('dataSearchUser')] - public function testSearchUser(string $searchTerm, array $contacts, bool $shareeEnumeration, array $expectedResult, bool $expectedExactIdMatch, bool $expectedMoreResults): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) use ($shareeEnumeration) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return $shareeEnumeration ? 'yes' : 'no'; - } - return $default; - } - ); - - $this->instantiatePlugin(IShare::TYPE_USER); - - $currentUser = $this->createMock(IUser::class); - $currentUser->method('getUID') - ->willReturn('current'); - $this->userSession->method('getUser') - ->willReturn($currentUser); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($search, $searchAttributes) use ($searchTerm, $contacts) { - if ($search === $searchTerm) { - return $contacts; - } - return []; - }); - - $moreResults = $this->plugin->search($searchTerm, 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertSame($expectedExactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('users'))); - $this->assertEquals($expectedResult, $result); - $this->assertSame($expectedMoreResults, $moreResults); - } - - public static function dataSearchUser(): array { - return [ - // data set 0 - ['test', [], true, ['users' => [], 'exact' => ['users' => [],]], false, false], - // data set 1 - ['test', [], false, ['users' => [], 'exact' => ['users' => [],]], false, false], - // data set 2 - [ - 'test@remote.com', - [], - true, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 3 - [ - 'test@remote.com', - [], - false, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 4 - [ - 'test', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - true, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 5 - [ - 'test', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'isLocalSystemBook' => true, - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - false, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 6 - [ - 'test@remote.com', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - true, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 7 - [ - 'username@localhost', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'EMAIL' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'EMAIL' => [ - 'username@localhost', - ], - ], - ], - true, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - // data set 8 - // Local user found by email - [ - 'test@example.com', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - ] - ], - false, - ['users' => [], 'exact' => ['users' => [['uuid' => 'uid1', 'name' => 'User', 'label' => 'User (test@example.com)', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'shareWithDisplayNameUnique' => 'test@example.com']]]], - true, - false, - ], - // data set 9 - // Current local user found by email => no result - [ - 'test@example.com', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['current@localhost'], - 'isLocalSystemBook' => true, - ] - ], - true, - ['exact' => []], - false, - false, - ], - // data set 10 - // Pagination and "more results" for user matches by emails - [ - 'test@example', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User1', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test1@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2', - 'EMAIL' => ['test@example.de'], - 'CLOUD' => ['test2@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid3', - 'FN' => 'User3', - 'EMAIL' => ['test@example.org'], - 'CLOUD' => ['test3@localhost'], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'uid4', - 'FN' => 'User4', - 'EMAIL' => ['test@example.net'], - 'CLOUD' => ['test4@localhost'], - 'isLocalSystemBook' => true, - ], - ], - true, - ['users' => [ - ['uuid' => 'uid1', 'name' => 'User1', 'label' => 'User1 (test@example.com)', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1'], 'shareWithDisplayNameUnique' => 'test@example.com'], - ['uuid' => 'uid2', 'name' => 'User2', 'label' => 'User2 (test@example.de)', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2'], 'shareWithDisplayNameUnique' => 'test@example.de'], - ], 'exact' => ['users' => []]], - false, - true, - ], - // data set 11 - // Pagination and "more results" for normal emails - [ - 'test@example', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User1', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test1@localhost'], - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2', - 'EMAIL' => ['test@example.de'], - 'CLOUD' => ['test2@localhost'], - ], - [ - 'UID' => 'uid3', - 'FN' => 'User3', - 'EMAIL' => ['test@example.org'], - 'CLOUD' => ['test3@localhost'], - ], - [ - 'UID' => 'uid4', - 'FN' => 'User4', - 'EMAIL' => ['test@example.net'], - 'CLOUD' => ['test4@localhost'], - ], - ], - true, - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - ], - ]; - } - - #[DataProvider('dataSearchEmailGroupsOnly')] - public function testSearchEmailGroupsOnly(string $searchTerm, array $contacts, array $expectedResult, bool $expectedExactIdMatch, bool $expectedMoreResults, array $userToGroupMapping, bool $validEmail): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return 'yes'; - } elseif ($appName === 'core' && $key === 'shareapi_only_share_with_group_members') { - return 'yes'; - } - return $default; - } - ); - - $this->instantiatePlugin(IShare::TYPE_EMAIL); - - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ - $currentUser = $this->createMock('\OCP\IUser'); - - $currentUser->expects($this->any()) - ->method('getUID') - ->willReturn('currentUser'); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($search, $searchAttributes) use ($searchTerm, $contacts) { - if ($search === $searchTerm) { - return $contacts; - } - return []; - }); - - $this->userSession->expects($this->any()) - ->method('getUser') - ->willReturn($currentUser); - - $this->groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->willReturnCallback(function (IUser $user) use ($userToGroupMapping) { - return $userToGroupMapping[$user->getUID()]; - }); - - $moreResults = $this->plugin->search($searchTerm, 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertSame($expectedExactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('emails'))); - $this->assertEquals($expectedResult, $result); - $this->assertSame($expectedMoreResults, $moreResults); - } - - public static function dataSearchEmailGroupsOnly(): array { - return [ - // The user `User` can share with the current user - [ - 'test', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group1'], - ], - false, - ], - // The user `User` cannot share with the current user - [ - 'test', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['emails' => [], 'exact' => ['emails' => []]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group2'], - ], - false, - ], - // The user `User` cannot share with the current user, but there is an exact match on the e-mail address -> share by e-mail - [ - 'test@example.com', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['emails' => [], 'exact' => ['emails' => [['label' => 'test@example.com', 'uuid' => 'test@example.com', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test@example.com']]]]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group2'], - ], - true, - ] - ]; - } - - #[DataProvider('dataSearchUserGroupsOnly')] - public function testSearchUserGroupsOnly(string $searchTerm, array $contacts, array $expectedResult, bool $expectedExactIdMatch, bool $expectedMoreResults, array $userToGroupMapping): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return 'yes'; - } elseif ($appName === 'core' && $key === 'shareapi_only_share_with_group_members') { - return 'yes'; - } - return $default; - } - ); - - $this->instantiatePlugin(IShare::TYPE_USER); - - $currentUser = $this->createMock(IUser::class); - - $currentUser->expects($this->any()) - ->method('getUID') - ->willReturn('currentUser'); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($search, $searchAttributes) use ($searchTerm, $contacts) { - if ($search === $searchTerm) { - return $contacts; - } - return []; - }); - - $this->userSession->expects($this->any()) - ->method('getUser') - ->willReturn($currentUser); - - $this->groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->willReturnCallback(function (IUser $user) use ($userToGroupMapping) { - return $userToGroupMapping[$user->getUID()]; - }); - - $moreResults = $this->plugin->search($searchTerm, 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertSame($expectedExactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('emails'))); - $this->assertEquals($expectedResult, $result); - $this->assertSame($expectedMoreResults, $moreResults); - } - - public static function dataSearchUserGroupsOnly(): array { - return [ - // The user `User` can share with the current user - [ - 'test', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['users' => [['label' => 'User (test@example.com)', 'uuid' => 'User', 'name' => 'User', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test'], 'shareWithDisplayNameUnique' => 'test@example.com',]], 'exact' => ['users' => []]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group1'], - ], - ], - // The user `User` cannot share with the current user - [ - 'test', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group2'], - ], - ], - // The user `User` cannot share with the current user, but there is an exact match on the e-mail address -> share by e-mail - [ - 'test@example.com', - [ - [ - 'FN' => 'User', - 'EMAIL' => ['test@example.com'], - 'CLOUD' => ['test@localhost'], - 'isLocalSystemBook' => true, - 'UID' => 'User', - ] - ], - ['users' => [], 'exact' => ['users' => [],]], - false, - false, - [ - 'currentUser' => ['group1'], - 'User' => ['group2'], - ], - ] - ]; - } -} --- a/tests/lib/Collaboration/Collaborators/RemotePluginTest.php +++ /dev/null @@ -1,510 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->config = $this->createMock(IConfig::class); - $this->contactsManager = $this->createMock(IManager::class); - $this->cloudIdManager = new CloudIdManager( - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->contactsManager, - $this->createMock(IURLGenerator::class), - $this->createMock(IUserManager::class), - ); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->trustedServers = $this->createMock(TrustedServers::class); - $this->searchResult = new SearchResult(); - } - - public function instantiatePlugin() { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('admin'); - $userSession = $this->createMock(IUserSession::class); - $userSession->expects($this->any()) - ->method('getUser') - ->willReturn($user); - $this->plugin = new RemotePlugin($this->contactsManager, $this->cloudIdManager, $this->config, $this->userManager, $userSession, $this->appConfig, $this->trustedServers); - } - - /** - * - * @param string $searchTerm - * @param array $contacts - * @param bool $shareeEnumeration - * @param array $expected - * @param bool $exactIdMatch - * @param bool $reachedEnd - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetRemote')] - public function testSearch($searchTerm, array $contacts, $shareeEnumeration, array $expected, $exactIdMatch, $reachedEnd): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) use ($shareeEnumeration) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return $shareeEnumeration ? 'yes' : 'no'; - } - return $default; - } - ); - - $this->instantiatePlugin(); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturnCallback(function ($search, $searchAttributes) use ($searchTerm, $contacts) { - if ($search === $searchTerm) { - return $contacts; - } - return []; - }); - - $moreResults = $this->plugin->search($searchTerm, 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertSame($exactIdMatch, $this->searchResult->hasExactIdMatch(new SearchResultType('remotes'))); - $this->assertEquals($expected, $result); - $this->assertSame($reachedEnd, $moreResults); - } - - /** - * - * @param string $remote - * @param string $expectedUser - * @param string $expectedUrl - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSplitUserRemote')] - public function testSplitUserRemote($remote, $expectedUser, $expectedUrl): void { - $this->instantiatePlugin(); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturn([]); - - [$remoteUser, $remoteUrl] = $this->plugin->splitUserRemote($remote); - $this->assertSame($expectedUser, $remoteUser); - $this->assertSame($expectedUrl, $remoteUrl); - } - - /** - * @param string $id - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSplitUserRemoteError')] - public function testSplitUserRemoteError($id): void { - $this->expectException(\Exception::class); - - $this->instantiatePlugin(); - $this->plugin->splitUserRemote($id); - } - - public function testTrustedServerMetadata(): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return 'yes'; - } - return $default; - } - ); - - $this->trustedServers->expects($this->any()) - ->method('isTrustedServer') - ->willReturnCallback(function ($serverUrl) { - return $serverUrl === 'trustedserver.com'; - }); - - $this->instantiatePlugin(); - - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturn([]); - - $this->plugin->search('test@trustedserver.com', 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertNotEmpty($result['exact']['remotes']); - $this->assertTrue($result['exact']['remotes'][0]['value']['isTrustedServer']); - } - - public function testEmailSearchInContacts(): void { - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback( - function ($appName, $key, $default) { - if ($appName === 'core' && $key === 'shareapi_allow_share_dialog_user_enumeration') { - return 'yes'; - } - return $default; - } - ); - - $this->trustedServers->expects($this->any()) - ->method('isTrustedServer') - ->willReturnCallback(function ($serverUrl) { - return $serverUrl === 'trustedserver.com'; - }); - - $this->instantiatePlugin(); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with('john@gmail.com', ['CLOUD', 'FN', 'EMAIL']) - ->willReturn([ - [ - 'FN' => 'John Doe', - 'EMAIL' => 'john@gmail.com', - 'CLOUD' => 'john@trustedserver.com', - 'UID' => 'john-contact-id' - ] - ]); - - $this->plugin->search('john@gmail.com', 2, 0, $this->searchResult); - $result = $this->searchResult->asArray(); - - $this->assertNotEmpty($result['exact']['remotes']); - $this->assertEquals('john@trustedserver.com', $result['exact']['remotes'][0]['value']['shareWith']); - $this->assertTrue($result['exact']['remotes'][0]['value']['isTrustedServer']); - } - - public static function dataGetRemote() { - return [ - ['test', [], true, ['remotes' => [], 'exact' => ['remotes' => []]], false, true], - ['test', [], false, ['remotes' => [], 'exact' => ['remotes' => []]], false, true], - [ - 'test@remote', - [], - true, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote', 'isTrustedServer' => false], 'uuid' => 'test', 'name' => 'test']]]], - false, - true, - ], - [ - 'test@remote', - [], - false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote', 'isTrustedServer' => false], 'uuid' => 'test', 'name' => 'test']]]], - false, - true, - ], - [ - 'test', - [ - [ - 'UID' => 'uid', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - true, - ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid1', 'type' => '', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost', 'isTrustedServer' => false]]], 'exact' => ['remotes' => []]], - false, - true, - ], - [ - 'test', - [ - [ - 'UID' => 'uid', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - false, - ['remotes' => [], 'exact' => ['remotes' => []]], - false, - true, - ], - [ - 'test@remote', - [ - [ - 'UID' => 'uid', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - true, - ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid', 'type' => '', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost', 'isTrustedServer' => false]]], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote', 'isTrustedServer' => false], 'uuid' => 'test', 'name' => 'test']]]], - false, - true, - ], - [ - 'test@remote', - [ - [ - 'UID' => 'uid', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote', 'isTrustedServer' => false], 'uuid' => 'test', 'name' => 'test']]]], - false, - true, - ], - [ - 'username@localhost', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => '2', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - true, - ['remotes' => [], 'exact' => ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid1', 'type' => '', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost', 'isTrustedServer' => false]]]]], - true, - true, - ], - [ - 'username@localhost', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - false, - ['remotes' => [], 'exact' => ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid1', 'type' => '', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost', 'isTrustedServer' => false]]]]], - true, - true, - ], - // contact with space - [ - 'user name@localhost', - [ - [ - 'UID' => 'uid1', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid3', - 'FN' => 'User Name @ Localhost', - 'CLOUD' => [ - 'user name@localhost', - ], - ], - ], - false, - ['remotes' => [], 'exact' => ['remotes' => [['name' => 'User Name @ Localhost', 'label' => 'User Name @ Localhost (user name@localhost)', 'uuid' => 'uid3', 'type' => '', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'user name@localhost', 'server' => 'localhost', 'isTrustedServer' => false]]]]], - true, - true, - ], - // remote with space, no contact - [ - 'user space@remote', - [ - [ - 'UID' => 'uid3', - 'FN' => 'User3 @ Localhost', - ], - [ - 'UID' => 'uid2', - 'FN' => 'User2 @ Localhost', - 'CLOUD' => [ - ], - ], - [ - 'UID' => 'uid1', - 'FN' => 'User @ Localhost', - 'CLOUD' => [ - 'username@localhost', - ], - ], - ], - false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'user space (remote)', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'user space@remote', 'server' => 'remote', 'isTrustedServer' => false], 'uuid' => 'user space', 'name' => 'user space']]]], - false, - true, - ], - ]; - } - - public static function dataTestSplitUserRemote(): array { - $userPrefix = ['user@name', 'username']; - $protocols = ['', 'http://', 'https://']; - $remotes = [ - 'localhost', - 'local.host', - 'dev.local.host', - 'dev.local.host/path', - 'dev.local.host/at@inpath', - '127.0.0.1', - '::1', - '::192.0.2.128', - '::192.0.2.128/at@inpath', - ]; - - $testCases = []; - foreach ($userPrefix as $user) { - foreach ($remotes as $remote) { - foreach ($protocols as $protocol) { - $baseUrl = $user . '@' . $protocol . $remote; - - if ($protocol === 'https://') { - // https:// protocol is not expected in the final result - $protocol = ''; - } - - $testCases[] = [$baseUrl, $user, $protocol . $remote]; - $testCases[] = [$baseUrl . '/', $user, $protocol . $remote]; - $testCases[] = [$baseUrl . '/index.php', $user, $protocol . $remote]; - $testCases[] = [$baseUrl . '/index.php/s/token', $user, $protocol . $remote]; - } - } - } - return $testCases; - } - - public static function dataTestSplitUserRemoteError(): array { - return [ - // Invalid path - ['user@'], - - // Invalid user - ['@server'], - ['us/er@server'], - ['us:er@server'], - - // Invalid splitting - ['user'], - [''], - ['us/erserver'], - ['us:erserver'], - ]; - } -} --- a/tests/lib/Collaboration/Collaborators/SearchResultTest.php +++ /dev/null @@ -1,86 +0,0 @@ -container = $this->createMock(IContainer::class); - - $this->search = new Search($this->container, $this->createMock(IEventDispatcher::class)); - } - - public static function dataAddResultSet(): array { - return [ - [[], ['exact' => []]], - [['users' => ['exact' => null, 'loose' => []]], ['exact' => ['users' => []], 'users' => []]], - [['groups' => ['exact' => null, 'loose' => ['l1']]], ['exact' => ['groups' => []], 'groups' => ['l1']]], - [['users' => ['exact' => ['e1'], 'loose' => []]], ['exact' => ['users' => ['e1']], 'users' => []]], - ]; - } - - /** - * @param array $toAdd - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataAddResultSet')] - public function testAddResultSet(array $toAdd, array $expected): void { - $result = new SearchResult(); - - foreach ($toAdd as $type => $results) { - $result->addResultSet(new SearchResultType($type), $results['loose'], $results['exact']); - } - - $this->assertEquals($expected, $result->asArray()); - } - - public static function dataHasResult(): array { - $result = ['value' => ['shareWith' => 'l1']]; - return [ - [[],'users', 'n1', false], - [['users' => ['exact' => null, 'loose' => [$result]]], 'users', 'l1', true], - [['users' => ['exact' => null, 'loose' => [$result]]], 'users', 'l2', false], - [['users' => ['exact' => null, 'loose' => [$result]]], 'groups', 'l1', false], - [['users' => ['exact' => [$result], 'loose' => []]], 'users', 'l1', true], - [['users' => ['exact' => [$result], 'loose' => []]], 'users', 'l2', false], - [['users' => ['exact' => [$result], 'loose' => []]], 'groups', 'l1', false], - ]; - } - - /** - * @param array $toAdd - * @param string $type - * @param string $id - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataHasResult')] - public function testHasResult(array $toAdd, $type, $id, $expected): void { - $result = new SearchResult(); - - foreach ($toAdd as $addType => $results) { - $result->addResultSet(new SearchResultType($addType), $results['loose'], $results['exact']); - } - - $this->assertSame($expected, $result->hasResult(new SearchResultType($type), $id)); - } -} --- a/tests/lib/Collaboration/Collaborators/SearchTest.php +++ /dev/null @@ -1,265 +0,0 @@ -container = $this->createMock(IContainer::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - - $this->search = new Search($this->container, $this->eventDispatcher); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchSharees')] - public function testSearch( - string $searchTerm, - array $shareTypes, - int $page, - int $perPage, - array $mockedUserResult, - array $mockedGroupsResult, - array $mockedRemotesResult, - array $mockedMailResult, - array $expected, - bool $expectedMoreResults, - ): void { - $searchResult = new SearchResult(); - - $userPlugin = $this->createMock(ISearchPlugin::class); - $userPlugin->expects($this->any()) - ->method('search') - ->willReturnCallback(function () use ($searchResult, $mockedUserResult, $expectedMoreResults) { - $type = new SearchResultType('users'); - $searchResult->addResultSet($type, $mockedUserResult); - return $expectedMoreResults; - }); - - $groupPlugin = $this->createMock(ISearchPlugin::class); - $groupPlugin->expects($this->any()) - ->method('search') - ->willReturnCallback(function () use ($searchResult, $mockedGroupsResult, $expectedMoreResults) { - $type = new SearchResultType('groups'); - $searchResult->addResultSet($type, $mockedGroupsResult); - return $expectedMoreResults; - }); - - $remotePlugin = $this->createMock(ISearchPlugin::class); - $remotePlugin->expects($this->any()) - ->method('search') - ->willReturnCallback(function () use ($searchResult, $mockedRemotesResult, $expectedMoreResults) { - if ($mockedRemotesResult !== null) { - $type = new SearchResultType('remotes'); - $searchResult->addResultSet($type, $mockedRemotesResult['results'], $mockedRemotesResult['exact']); - if ($mockedRemotesResult['exactIdMatch'] === true) { - $searchResult->markExactIdMatch($type); - } - } - return $expectedMoreResults; - }); - - $mailPlugin = $this->createMock(ISearchPlugin::class); - $mailPlugin->expects($this->any()) - ->method('search') - ->willReturnCallback(function () use ($searchResult, $mockedMailResult, $expectedMoreResults) { - $type = new SearchResultType('emails'); - $searchResult->addResultSet($type, $mockedMailResult); - return $expectedMoreResults; - }); - - $this->container->expects($this->any()) - ->method('resolve') - ->willReturnCallback(function ($class) use ($searchResult) { - if ($class === SearchResult::class) { - return $searchResult; - } - return null; - }); - - $this->container->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($class) use ($userPlugin, $groupPlugin, $remotePlugin, $mailPlugin) { - if ($class === 'user') { - return $userPlugin; - } elseif ($class === 'group') { - return $groupPlugin; - } elseif ($class === 'remote') { - return $remotePlugin; - } elseif ($class === 'mail') { - return $mailPlugin; - } - return null; - }); - - $this->search->registerPlugin(['shareType' => 'SHARE_TYPE_USER', 'class' => 'user']); - $this->search->registerPlugin(['shareType' => 'SHARE_TYPE_GROUP', 'class' => 'group']); - $this->search->registerPlugin(['shareType' => 'SHARE_TYPE_REMOTE', 'class' => 'remote']); - $this->search->registerPlugin(['shareType' => 'SHARE_TYPE_EMAIL', 'class' => 'mail']); - - [$results, $moreResults] = $this->search->search($searchTerm, $shareTypes, false, $perPage, $perPage * ($page - 1)); - - $this->assertEquals($expected, $results); - $this->assertSame($expectedMoreResults, $moreResults); - } - - public static function dataSearchSharees(): array { - return [ - // #0 - [ - 'test', [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_REMOTE], 1, 2, [], [], ['results' => [], 'exact' => [], 'exactIdMatch' => false], - [], - [ - 'exact' => ['users' => [], 'groups' => [], 'remotes' => []], - 'users' => [], - 'groups' => [], - 'remotes' => [], - ], - false - ], - // #1 - [ - 'test', [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_REMOTE], 1, 2, [], [], ['results' => [], 'exact' => [], 'exactIdMatch' => false], - [], - [ - 'exact' => ['users' => [], 'groups' => [], 'remotes' => []], - 'users' => [], - 'groups' => [], - 'remotes' => [], - ], - false - ], - // #2 - [ - 'test', [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_REMOTE], 1, 2, [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], [ - ['label' => 'testgroup1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'testgroup1']], - ], [ - 'results' => [['label' => 'testz@remote', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote']]], 'exact' => [], 'exactIdMatch' => false, - ], - [], - [ - 'exact' => ['users' => [], 'groups' => [], 'remotes' => []], - 'users' => [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], - 'groups' => [ - ['label' => 'testgroup1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'testgroup1']], - ], - 'remotes' => [ - ['label' => 'testz@remote', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote']], - ], - ], true, - ], - // #3 No groups requested - [ - 'test', [IShare::TYPE_USER, IShare::TYPE_REMOTE], 1, 2, [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], [], [ - 'results' => [['label' => 'testz@remote', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote']]], 'exact' => [], 'exactIdMatch' => false - ], - [], - [ - 'exact' => ['users' => [], 'remotes' => []], - 'users' => [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], - 'remotes' => [ - ['label' => 'testz@remote', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote']], - ], - ], false, - ], - // #4 Share type restricted to user - Only one user - [ - 'test', [IShare::TYPE_USER], 1, 2, [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], [], [], [], - [ - 'exact' => ['users' => []], - 'users' => [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], - ], false, - ], - // #5 Share type restricted to user - Multipage result - [ - 'test', [IShare::TYPE_USER], 1, 2, [ - ['label' => 'test 1', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ['label' => 'test 2', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2']], - ], [], [], [], - [ - 'exact' => ['users' => []], - 'users' => [ - ['label' => 'test 1', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ['label' => 'test 2', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test2']], - ], - ], true, - ], - // #6 Mail shares filtered out in favor of remote shares - [ - 'test', // search term - [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL], // plugins - 1, // page - 10, // per page - [ // user result - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], - [ // group result - ['label' => 'testgroup1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'testgroup1']], - ], - [ // remote result - 'results' => [ - ['label' => 'testz@remote.tld', 'uuid' => 'f3d78140-abcc-46df-b58d-c7cc1176aadf','value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote.tld']] - ], - 'exact' => [], - 'exactIdMatch' => false, - ], - [ // mail result - ['label' => 'test Two', 'uuid' => 'b2321e9e-31af-43ac-a406-583fb26d1964', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test2@remote.tld']], - ['label' => 'test One', 'uuid' => 'f3d78140-abcc-46df-b58d-c7cc1176aadf', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'testz@remote.tld']], - ], - [ // expected result - 'exact' => ['users' => [], 'groups' => [], 'remotes' => [], 'emails' => []], - 'users' => [ - ['label' => 'test One', 'value' => ['shareType' => IShare::TYPE_USER, 'shareWith' => 'test1']], - ], - 'groups' => [ - ['label' => 'testgroup1', 'value' => ['shareType' => IShare::TYPE_GROUP, 'shareWith' => 'testgroup1']], - ], - 'remotes' => [ - ['label' => 'testz@remote.tld', 'uuid' => 'f3d78140-abcc-46df-b58d-c7cc1176aadf', 'value' => ['shareType' => IShare::TYPE_REMOTE, 'shareWith' => 'testz@remote.tld']], - ], - 'emails' => [ - // one passes, another is filtered out - ['label' => 'test Two', 'uuid' => 'b2321e9e-31af-43ac-a406-583fb26d1964', 'value' => ['shareType' => IShare::TYPE_EMAIL, 'shareWith' => 'test2@remote.tld']] - ] - ], - false, // expected more results indicator - ], - ]; - } -} --- a/tests/lib/Collaboration/Resources/ProviderManagerTest.php +++ /dev/null @@ -1,99 +0,0 @@ -serverContainer = $this->createMock(ContainerInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->providerManager = new class($this->serverContainer, $this->logger) extends ProviderManager { - public function countProviders(): int { - return count($this->providers); - } - }; - } - - public function testRegisterResourceProvider(): void { - $this->providerManager->registerResourceProvider('AwesomeResourceProvider'); - $this->assertSame(1, $this->providerManager->countProviders()); - } - - public function testGetResourceProvidersNoProvider(): void { - $this->assertCount(0, $this->providerManager->getResourceProviders()); - } - - public function testGetResourceProvidersValidProvider(): void { - $this->serverContainer->expects($this->once()) - ->method('get') - ->with($this->equalTo(ResourceProvider::class)) - ->willReturn($this->createMock(ResourceProvider::class)); - - $this->providerManager->registerResourceProvider(ResourceProvider::class); - $resourceProviders = $this->providerManager->getResourceProviders(); - - $this->assertCount(1, $resourceProviders); - $this->assertInstanceOf(ResourceProvider::class, $resourceProviders[0]); - } - - public function testGetResourceProvidersInvalidProvider(): void { - $this->serverContainer->expects($this->once()) - ->method('get') - ->with($this->equalTo('InvalidResourceProvider')) - ->willThrowException(new QueryException('A meaningful error message')); - - $this->logger->expects($this->once()) - ->method('error'); - - $this->providerManager->registerResourceProvider('InvalidResourceProvider'); - $resourceProviders = $this->providerManager->getResourceProviders(); - - $this->assertCount(0, $resourceProviders); - } - - public function testGetResourceProvidersValidAndInvalidProvider(): void { - $this->serverContainer->expects($this->exactly(2)) - ->method('get') - ->willReturnCallback(function (string $service) { - if ($service === 'InvalidResourceProvider') { - throw new QueryException('A meaningful error message'); - } - if ($service === ResourceProvider::class) { - return $this->createMock(ResourceProvider::class); - } - }); - - $this->logger->expects($this->once()) - ->method('error'); - - $this->providerManager->registerResourceProvider('InvalidResourceProvider'); - $this->providerManager->registerResourceProvider(ResourceProvider::class); - $resourceProviders = $this->providerManager->getResourceProviders(); - - $this->assertCount(1, $resourceProviders); - } -} --- a/tests/lib/Command/AsyncBusTestCase.php +++ /dev/null @@ -1,114 +0,0 @@ -state; - } -} - -class FilesystemCommand implements ICommand { - use FileAccess; - - #[\Override] - public function handle() { - AsyncBusTestCase::$lastCommand = 'FileAccess'; - } -} - -abstract class AsyncBusTestCase extends TestCase { - /** - * Basic way to check output from a command - * - * @var string - */ - public static $lastCommand; - - /** - * @var IBus - */ - private $bus; - - public static function DummyCommand() { - self::$lastCommand = 'static'; - } - - /** - * @return IBus - */ - protected function getBus() { - if (!$this->bus instanceof IBus) { - $this->bus = $this->createBus(); - } - return $this->bus; - } - - /** - * @return IBus - */ - abstract protected function createBus(); - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - self::$lastCommand = ''; - } - - public function testSimpleCommand(): void { - $command = new SimpleCommand(); - $this->getBus()->push($command); - $this->runJobs(); - $this->assertEquals('SimpleCommand', self::$lastCommand); - } - - public function testStateFullCommand(): void { - $command = new StateFullCommand('foo'); - $this->getBus()->push($command); - $this->runJobs(); - $this->assertEquals('foo', self::$lastCommand); - } - - public function testFileFileAccessCommand(): void { - $this->getBus()->push(new FilesystemCommand()); - $this->assertEquals('', self::$lastCommand); - $this->runJobs(); - $this->assertEquals('FileAccess', self::$lastCommand); - } - - public function testFileFileAccessCommandSync(): void { - $this->getBus()->requireSync('\OC\Command\FileAccess'); - $this->getBus()->push(new FilesystemCommand()); - $this->assertEquals('FileAccess', self::$lastCommand); - self::$lastCommand = ''; - $this->runJobs(); - $this->assertEquals('', self::$lastCommand); - } - - abstract protected function runJobs(); -} --- a/tests/lib/Command/BackgroundModeTest.php +++ /dev/null @@ -1,62 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -namespace Test\Command; - -use OC\Core\Command\Background\Mode; -use OCP\IAppConfig; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Tester\CommandTester; -use Test\TestCase; - -#[\PHPUnit\Framework\Attributes\Group('DB')] -class BackgroundModeTest extends TestCase { - private IAppConfig $appConfig; - - private Mode $command; - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->appConfig = $this->createMock(IAppConfig::class); - - $inputDefinition = new InputDefinition([ - new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), - ]); - - $this->command = new Mode($this->appConfig); - $this->command->setDefinition($inputDefinition); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataModeCommand')] - public function testModeCommand(string $mode): void { - $this->appConfig->expects($this->once()) - ->method('setValueString') - ->with('core', 'backgroundjobs_mode', $mode); - - $commandTester = new CommandTester($this->command); - $commandTester->execute(['command' => 'background:' . $mode]); - - $commandTester->assertCommandIsSuccessful(); - - $output = $commandTester->getDisplay(); - $this->assertStringContainsString($mode, $output); - } - - public static function dataModeCommand(): array { - return [ - 'ajax' => ['ajax'], - 'cron' => ['cron'], - 'webcron' => ['webcron'], - ]; - } -} --- a/tests/lib/Command/CronBusTest.php +++ /dev/null @@ -1,40 +0,0 @@ -jobList = new DummyJobList(); - } - - #[\Override] - protected function createBus() { - return new CronBus($this->jobList); - } - - #[\Override] - protected function runJobs() { - $jobs = $this->jobList->getAll(); - foreach ($jobs as $job) { - $job->start($this->jobList); - } - } -} --- a/tests/lib/Command/Integrity/SignAppTest.php +++ /dev/null @@ -1,272 +0,0 @@ -checker = $this->createMock(Checker::class); - $this->fileAccessHelper = $this->createMock(FileAccessHelper::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->signApp = new SignApp( - $this->checker, - $this->fileAccessHelper, - $this->urlGenerator - ); - } - - public function testExecuteWithMissingPath(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', null], - ['privateKey', 'PrivateKey'], - ['certificate', 'Certificate'], - ]); - - $calls = [ - 'This command requires the --path, --privateKey and --certificate.', - '*', - '*', - ]; - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message) use (&$calls): void { - $expected = array_shift($calls); - if ($expected === '*') { - $this->assertNotEmpty($message); - } else { - $this->assertEquals($expected, $message); - } - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithMissingPrivateKey(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', null], - ['certificate', 'Certificate'], - ]); - - $calls = [ - 'This command requires the --path, --privateKey and --certificate.', - '*', - '*', - ]; - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message) use (&$calls): void { - $expected = array_shift($calls); - if ($expected === '*') { - $this->assertNotEmpty($message); - } else { - $this->assertEquals($expected, $message); - } - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithMissingCertificate(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', 'PrivateKey'], - ['certificate', null], - ]); - - $calls = [ - 'This command requires the --path, --privateKey and --certificate.', - '*', - '*', - ]; - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message) use (&$calls): void { - $expected = array_shift($calls); - if ($expected === '*') { - $this->assertNotEmpty($message); - } else { - $this->assertEquals($expected, $message); - } - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithNotExistingPrivateKey(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', false], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Private key "privateKey" does not exists.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithNotExistingCertificate(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', false], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Certificate "certificate" does not exists.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithException(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', \OC::$SERVERROOT . '/tests/data/integritycheck/core.crt'], - ]); - - $this->checker - ->expects($this->once()) - ->method('writeAppSignature') - ->willThrowException(new \Exception('My error message')); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Error: My error message', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecute(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['path', 'AppId'], - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', \OC::$SERVERROOT . '/tests/data/integritycheck/core.crt'], - ]); - - $this->checker - ->expects($this->once()) - ->method('writeAppSignature'); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Successfully signed "AppId"', $message); - }); - - $this->assertSame(0, self::invokePrivate($this->signApp, 'execute', [$inputInterface, $outputInterface])); - } -} --- a/tests/lib/Command/Integrity/SignCoreTest.php +++ /dev/null @@ -1,213 +0,0 @@ -checker = $this->createMock(Checker::class); - $this->fileAccessHelper = $this->createMock(FileAccessHelper::class); - $this->signCore = new SignCore( - $this->checker, - $this->fileAccessHelper - ); - } - - public function testExecuteWithMissingPrivateKey(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', null], - ['certificate', 'certificate'], - ['path', 'certificate'], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('--privateKey, --certificate and --path are required.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithMissingCertificate(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', 'privateKey'], - ['certificate', null], - ['path', 'certificate'], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('--privateKey, --certificate and --path are required.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithNotExistingPrivateKey(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ['path', 'certificate'], - ]); - - $this->fileAccessHelper - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', false], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Private key "privateKey" does not exists.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithNotExistingCertificate(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ['path', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', false], - ]); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Certificate "certificate" does not exists.', $message); - }); - - $this->assertSame(1, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecuteWithException(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ['path', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.crt')], - ]); - - $this->checker - ->expects($this->once()) - ->method('writeCoreSignature') - ->willThrowException(new \Exception('My exception message')); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Error: My exception message', $message); - }); - - $this->assertEquals(1, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } - - public function testExecute(): void { - $inputInterface = $this->createMock(InputInterface::class); - $outputInterface = $this->createMock(OutputInterface::class); - - $inputInterface - ->expects($this->exactly(3)) - ->method('getOption') - ->willReturnMap([ - ['privateKey', 'privateKey'], - ['certificate', 'certificate'], - ['path', 'certificate'], - ]); - - $this->fileAccessHelper - ->expects($this->any()) - ->method('file_get_contents') - ->willReturnMap([ - ['privateKey', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.key')], - ['certificate', file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/core.crt')], - ]); - - $this->checker - ->expects($this->once()) - ->method('writeCoreSignature'); - - $outputInterface - ->expects($this->any()) - ->method('writeln') - ->willReturnCallback(function (string $message): void { - $this->assertEquals('Successfully signed "core"', $message); - }); - - $this->assertEquals(0, self::invokePrivate($this->signCore, 'execute', [$inputInterface, $outputInterface])); - } -} --- a/tests/lib/Comments/CommentTest.php +++ /dev/null @@ -1,267 +0,0 @@ - 'users', 'id' => 'alice']; - $creationDT = new \DateTime(); - $latestChildDT = new \DateTime('yesterday'); - $object = ['type' => 'files', 'id' => 'file64']; - $referenceId = sha1('referenceId'); - $metaData = ['last_edit_actor_id' => 'admin']; - - $comment - ->setId($id) - ->setParentId($parentId) - ->setTopmostParentId($topMostParentId) - ->setChildrenCount($childrenCount) - ->setMessage($message) - ->setVerb($verb) - ->setActor($actor['type'], $actor['id']) - ->setCreationDateTime($creationDT) - ->setLatestChildDateTime($latestChildDT) - ->setObject($object['type'], $object['id']) - ->setReferenceId($referenceId) - ->setMetaData($metaData); - - $this->assertSame($id, $comment->getId()); - $this->assertSame($parentId, $comment->getParentId()); - $this->assertSame($topMostParentId, $comment->getTopmostParentId()); - $this->assertSame($childrenCount, $comment->getChildrenCount()); - $this->assertSame($message, $comment->getMessage()); - $this->assertSame($verb, $comment->getVerb()); - $this->assertSame($actor['type'], $comment->getActorType()); - $this->assertSame($actor['id'], $comment->getActorId()); - $this->assertSame($creationDT, $comment->getCreationDateTime()); - $this->assertSame($latestChildDT, $comment->getLatestChildDateTime()); - $this->assertSame($object['type'], $comment->getObjectType()); - $this->assertSame($object['id'], $comment->getObjectId()); - $this->assertSame($referenceId, $comment->getReferenceId()); - $this->assertSame($metaData, $comment->getMetaData()); - } - - public function testSetIdIllegalInput(): void { - $this->expectException(IllegalIDChangeException::class); - - $comment = new Comment(); - - $comment->setId('c23'); - $comment->setId('c17'); - } - - /** - * @throws IllegalIDChangeException - */ - public function testResetId(): void { - $comment = new Comment(); - $comment->setId('c23'); - $comment->setId(''); - - $this->assertSame('', $comment->getId()); - } - - public static function simpleSetterProvider(): array { - return [ - ['Id', true], - ['TopmostParentId', true], - ['ParentId', true], - ['Message', true], - ['Verb', true], - ['Verb', ''], - ['ChildrenCount', true], - ]; - } - - #[DataProvider(methodName: 'simpleSetterProvider')] - public function testSimpleSetterInvalidInput($field, $input): void { - $this->expectException(\InvalidArgumentException::class); - - $comment = new Comment(); - $setter = 'set' . $field; - - $comment->$setter($input); - } - - public static function roleSetterProvider(): array { - return [ - ['Actor', true, true], - ['Actor', 'users', true], - ['Actor', true, 'alice'], - ['Actor', ' ', ' '], - ['Object', true, true], - ['Object', 'files', true], - ['Object', true, 'file64'], - ['Object', ' ', ' '], - ]; - } - - #[DataProvider(methodName: 'roleSetterProvider')] - public function testSetRoleInvalidInput($role, $type, $id): void { - $this->expectException(\InvalidArgumentException::class); - - $comment = new Comment(); - $setter = 'set' . $role; - $comment->$setter($type, $id); - } - - public function testSetUberlongMessage(): void { - $this->expectException(MessageTooLongException::class); - - $comment = new Comment(); - $msg = str_pad('', IComment::MAX_MESSAGE_LENGTH + 1, 'x'); - $comment->setMessage($msg); - } - - public static function mentionsProvider(): array { - return [ - [ - '@alice @bob look look, a cook!', - [['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - ], - [ - 'no mentions in this message', - [] - ], - [ - '@alice @bob look look, a duplication @alice test @bob!', - [['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - ], - [ - '@alice is the author, notify @bob, nevertheless mention her!', - [['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - /* author: */ 'alice' - ], - [ - '@foobar and @barfoo you should know, @foo@bar.com is valid' - . ' and so is @bar@foo.org@foobar.io I hope that clarifies everything.' - . ' cc @23452-4333-54353-2342 @yolo!' - . ' however the most important thing to know is that www.croissant.com/@oil is not valid' - . ' and won\'t match anything at all', - [ - ['type' => 'user', 'id' => 'bar@foo.org@foobar.io'], - ['type' => 'user', 'id' => '23452-4333-54353-2342'], - ['type' => 'user', 'id' => 'foo@bar.com'], - ['type' => 'user', 'id' => 'foobar'], - ['type' => 'user', 'id' => 'barfoo'], - ['type' => 'user', 'id' => 'yolo'], - ], - ], - [ - '@@chef is also a valid mention, no matter how strange it looks', - [['type' => 'user', 'id' => '@chef']], - ], - [ - 'Also @"user with spaces" are now supported', - [['type' => 'user', 'id' => 'user with spaces']], - ], - [ - 'Also @"guest/0123456789abcdef" are now supported', - [['type' => 'guest', 'id' => 'guest/0123456789abcdef']], - ], - [ - 'Also @"group/My Group ID 321" are now supported', - [['type' => 'group', 'id' => 'My Group ID 321']], - ], - [ - 'Welcome federation @"federated_group/My Group ID 321" @"federated_team/Former Cirle" @"federated_user/cloudId@http://example.tld:8080/nextcloud"! Now freshly supported', - [ - ['type' => 'federated_user', 'id' => 'cloudId@http://example.tld:8080/nextcloud'], - ['type' => 'federated_group', 'id' => 'My Group ID 321'], - ['type' => 'federated_team', 'id' => 'Former Cirle'], - ], - ], - [ - 'Emails are supported since 30.0.2 right? @"email/aa23d315de327cfc330f0401ea061005b2b0cdd45ec8346f12664dd1f34cb886"', - [ - ['type' => 'email', 'id' => 'aa23d315de327cfc330f0401ea061005b2b0cdd45ec8346f12664dd1f34cb886'], - ], - ], - [ - 'Mention @alice but not `@bob` inside inline code', - [['type' => 'user', 'id' => 'alice']], - ], - [ - 'Mention @alice but not `Hello @bob there` inside inline code', - [['type' => 'user', 'id' => 'alice']], - ], - [ - "Mention ` user @alice\nAs it's just 2 ` accents", - [['type' => 'user', 'id' => 'alice']], - ], - [ - 'Mention @alice and @bob but not `Hello @bob there` inside inline code', - [['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - ], - [ - "Mention @alice but not in fenced code block\n```\n@bob @charlie\n```\nend", - [['type' => 'user', 'id' => 'alice']], - ], - [ - "Mention @alice but not in tilde code block\n~~~\n@bob\n~~~\nend", - [['type' => 'user', 'id' => 'alice']], - ], - [ - 'No mentions at all in `@alice` and `@bob`', - [], - ], - [ - "@alice\n```\n@bob\n```\n@charlie", - [['type' => 'user', 'id' => 'charlie'], ['type' => 'user', 'id' => 'alice']], - ], - [ - "@alice\n```\n@bob\n```\n@charlie", - [['type' => 'user', 'id' => 'charlie'], ['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - null, - false, - ], - [ - 'Mention @alice and `also @bob as end of text only applies to code blocks', - [['type' => 'user', 'id' => 'alice'], ['type' => 'user', 'id' => 'bob']], - ], - [ - "Mention @alice but not in unclosed fenced code block\n```\n@bob\n@charlie", - [['type' => 'user', 'id' => 'alice']], - ], - [ - "Mention @alice but not in unclosed tilde code block\n~~~\n@bob", - [['type' => 'user', 'id' => 'alice']], - ], - ]; - } - - #[DataProvider(methodName: 'mentionsProvider')] - public function testMentions(string $message, array $expectedMentions, ?string $author = null, bool $markdown = true): void { - $comment = new Comment(); - $comment->setMessage($message); - if (!is_null($author)) { - $comment->setActor('user', $author); - } - $mentions = $comment->getMentions($markdown); - $this->assertSame($expectedMentions, $mentions); - } -} --- a/tests/lib/Comments/FakeFactory.php +++ /dev/null @@ -1,25 +0,0 @@ - new Comment(), 'replies' => []]; - } - - #[\Override] - public function getForObject( - $objectType, - $objectId, - $limit = 0, - $offset = 0, - ?\DateTime $notOlderThan = null, - ): array { - return []; - } - - #[\Override] - public function getForObjectSince( - string $objectType, - string $objectId, - int $lastKnownCommentId, - string $sortDirection = 'asc', - int $limit = 30, - bool $includeLastKnown = false, - string $topmostParentId = '', - ): array { - return []; - } - - #[\Override] - public function getCommentsWithVerbForObjectSinceComment( - string $objectType, - string $objectId, - array $verbs, - int $lastKnownCommentId, - string $sortDirection = 'asc', - int $limit = 30, - bool $includeLastKnown = false, - string $topmostParentId = '', - ): array { - return []; - } - - #[\Override] - public function getNumberOfCommentsForObject($objectType, $objectId, ?\DateTime $notOlderThan = null, $verb = ''): int { - return 0; - } - - #[\Override] - public function getNumberOfCommentsForObjects(string $objectType, array $objectIds, ?\DateTime $notOlderThan = null, string $verb = ''): array { - return array_fill_keys($objectIds, 0); - } - - #[\Override] - public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array { - return []; - } - - #[\Override] - public function create($actorType, $actorId, $objectType, $objectId): IComment { - return new Comment(); - } - - #[\Override] - public function delete($id): bool { - return false; - } - - #[\Override] - public function getReactionComment(int $parentId, string $actorType, string $actorId, string $reaction): IComment { - return new Comment(); - } - - #[\Override] - public function retrieveAllReactions(int $parentId): array { - return []; - } - - #[\Override] - public function retrieveAllReactionsWithSpecificReaction(int $parentId, string $reaction): array { - return []; - } - - #[\Override] - public function supportReactions(): bool { - return false; - } - - #[\Override] - public function save(IComment $comment): bool { - return false; - } - - #[\Override] - public function deleteReferencesOfActor($actorType, $actorId): bool { - return false; - } - - #[\Override] - public function deleteCommentsAtObject($objectType, $objectId): bool { - return false; - } - - #[\Override] - public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user): bool { - return false; - } - - #[\Override] - public function getReadMark($objectType, $objectId, IUser $user): bool { - return false; - } - - #[\Override] - public function deleteReadMarksFromUser(IUser $user): bool { - return false; - } - - #[\Override] - public function deleteReadMarksOnObject($objectType, $objectId): bool { - return false; - } - - #[\Override] - public function registerEventHandler(\Closure $closure): void { - } - - #[\Override] - public function registerDisplayNameResolver($type, \Closure $closure): void { - } - - #[\Override] - public function resolveDisplayName($type, $id): string { - return ''; - } - - #[\Override] - public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user): array { - return []; - } - - #[\Override] - public function getNumberOfUnreadCommentsForObjects(string $objectType, array $objectIds, IUser $user, $verb = ''): array { - return []; - } - - #[\Override] - public function load(): void { - } - - #[\Override] - public function searchForObjects(string $search, string $objectType, array $objectIds, string $verb, int $offset, int $limit = 50): array { - return []; - } - - #[\Override] - public function getNumberOfCommentsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, string $verb = ''): int { - return 0; - } - - #[\Override] - public function getNumberOfCommentsWithVerbsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, array $verbs): int { - return 0; - } - - #[\Override] - public function getLastCommentBeforeDate(string $objectType, string $objectId, \DateTime $beforeDate, string $verb = ''): int { - return 0; - } - - #[\Override] - public function getLastCommentDateByActor(string $objectType, string $objectId, string $verb, string $actorType, array $actors): array { - return []; - } - - #[\Override] - public function deleteCommentsExpiredAtObject(string $objectType, string $objectId = ''): bool { - return true; - } -} --- a/tests/lib/Comments/ManagerTest.php +++ /dev/null @@ -1,1385 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - - /** @psalm-suppress DeprecatedMethod */ - $sql = $this->connection->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*comments`'); - $this->connection->prepare($sql)->execute(); - /** @psalm-suppress DeprecatedMethod */ - $sql = $this->connection->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*reactions`'); - $this->connection->prepare($sql)->execute(); - } - - protected function addDatabaseEntry(?string $parentId, ?string $topmostParentId, ?\DateTimeInterface $creationDT = null, ?\DateTimeInterface $latestChildDT = null, $objectId = null, ?\DateTimeInterface $expireDate = null): string { - $creationDT ??= new \DateTime(); - $latestChildDT ??= new \DateTime('yesterday'); - $objectId ??= 'file64'; - - $qb = $this->connection->getQueryBuilder(); - $qb - ->insert('comments') - ->values([ - 'parent_id' => $qb->createNamedParameter($parentId), - 'topmost_parent_id' => $qb->createNamedParameter($topmostParentId), - 'children_count' => $qb->createNamedParameter(2), - 'actor_type' => $qb->createNamedParameter('users'), - 'actor_id' => $qb->createNamedParameter('alice'), - 'message' => $qb->createNamedParameter('nice one'), - 'verb' => $qb->createNamedParameter('comment'), - 'creation_timestamp' => $qb->createNamedParameter($creationDT, IQueryBuilder::PARAM_DATETIME_MUTABLE), - 'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, IQueryBuilder::PARAM_DATETIME_MUTABLE), - 'object_type' => $qb->createNamedParameter('files'), - 'object_id' => $qb->createNamedParameter($objectId), - 'expire_date' => $qb->createNamedParameter($expireDate, IQueryBuilder::PARAM_DATETIME_MUTABLE), - 'reference_id' => $qb->createNamedParameter('referenceId'), - 'meta_data' => $qb->createNamedParameter(json_encode(['last_edit_actor_id' => 'admin'])), - ]) - ->executeStatement(); - - return (string)$qb->getLastInsertId(); - } - - protected function getManager(): Manager { - /** @psalm-suppress DeprecatedInterface No way around at the moment */ - return new Manager( - $this->connection, - $this->createMock(LoggerInterface::class), - $this->createMock(IConfig::class), - $this->createMock(ITimeFactory::class), - new EmojiHelper($this->connection), - $this->createMock(IInitialStateService::class), - $this->rootFolder, - $this->createMock(IEventDispatcher::class), - ); - } - - public function testGetCommentNotFound(): void { - $this->expectException(NotFoundException::class); - - $manager = $this->getManager(); - $manager->get('22'); - } - - public function testGetCommentNotFoundInvalidInput(): void { - $this->expectException(\InvalidArgumentException::class); - - $manager = $this->getManager(); - $manager->get('unexisting22'); - } - - public function testGetComment(): void { - $manager = $this->getManager(); - - $creationDT = new \DateTime('yesterday'); - $latestChildDT = new \DateTime(); - - $qb = $this->connection->getQueryBuilder(); - $qb - ->insert('comments') - ->values([ - 'parent_id' => $qb->createNamedParameter('2'), - 'topmost_parent_id' => $qb->createNamedParameter('1'), - 'children_count' => $qb->createNamedParameter(2), - 'actor_type' => $qb->createNamedParameter('users'), - 'actor_id' => $qb->createNamedParameter('alice'), - 'message' => $qb->createNamedParameter('nice one'), - 'verb' => $qb->createNamedParameter('comment'), - 'creation_timestamp' => $qb->createNamedParameter($creationDT, 'datetime'), - 'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, 'datetime'), - 'object_type' => $qb->createNamedParameter('files'), - 'object_id' => $qb->createNamedParameter('file64'), - 'reference_id' => $qb->createNamedParameter('referenceId'), - 'meta_data' => $qb->createNamedParameter(json_encode(['last_edit_actor_id' => 'admin'])), - ]) - ->executeStatement(); - - $id = (string)$qb->getLastInsertId(); - - $comment = $manager->get($id); - $this->assertInstanceOf(IComment::class, $comment); - $this->assertSame($id, $comment->getId()); - $this->assertSame('2', $comment->getParentId()); - $this->assertSame('1', $comment->getTopmostParentId()); - $this->assertSame(2, $comment->getChildrenCount()); - $this->assertSame('users', $comment->getActorType()); - $this->assertSame('alice', $comment->getActorId()); - $this->assertSame('nice one', $comment->getMessage()); - $this->assertSame('comment', $comment->getVerb()); - $this->assertSame('files', $comment->getObjectType()); - $this->assertSame('file64', $comment->getObjectId()); - $this->assertEquals($creationDT->getTimestamp(), $comment->getCreationDateTime()->getTimestamp()); - $this->assertEquals($latestChildDT->getTimestamp(), $comment->getLatestChildDateTime()->getTimestamp()); - $this->assertEquals('referenceId', $comment->getReferenceId()); - $this->assertEquals(['last_edit_actor_id' => 'admin'], $comment->getMetaData()); - } - - public function testGetTreeNotFound(): void { - $this->expectException(NotFoundException::class); - - $manager = $this->getManager(); - $manager->getTree('22'); - } - - public function testGetTreeNotFoundInvalidIpnut(): void { - $this->expectException(\InvalidArgumentException::class); - - $manager = $this->getManager(); - $manager->getTree('unexisting22'); - } - - public function testGetTree(): void { - $headId = $this->addDatabaseEntry('0', '0'); - - $this->addDatabaseEntry($headId, $headId, new \DateTime('-3 hours')); - $this->addDatabaseEntry($headId, $headId, new \DateTime('-2 hours')); - $id = $this->addDatabaseEntry($headId, $headId, new \DateTime('-1 hour')); - - $manager = $this->getManager(); - $tree = $manager->getTree($headId); - - // Verifying the root comment - $this->assertArrayHasKey('comment', $tree); - $this->assertInstanceOf(IComment::class, $tree['comment']); - $this->assertSame($headId, $tree['comment']->getId()); - $this->assertArrayHasKey('replies', $tree); - $this->assertCount(3, $tree['replies']); - - // one level deep - foreach ($tree['replies'] as $reply) { - $this->assertInstanceOf(IComment::class, $reply['comment']); - $this->assertSame((string)$id, $reply['comment']->getId()); - $this->assertCount(0, $reply['replies']); - $id--; - } - } - - public function testGetTreeNoReplies(): void { - $id = $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - $tree = $manager->getTree($id); - - // Verifying the root comment - $this->assertArrayHasKey('comment', $tree); - $this->assertInstanceOf(IComment::class, $tree['comment']); - $this->assertSame($id, $tree['comment']->getId()); - $this->assertArrayHasKey('replies', $tree); - $this->assertCount(0, $tree['replies']); - } - - public function testGetTreeWithLimitAndOffset(): void { - $headId = $this->addDatabaseEntry('0', '0'); - - $this->addDatabaseEntry($headId, $headId, new \DateTime('-3 hours')); - $this->addDatabaseEntry($headId, $headId, new \DateTime('-2 hours')); - $this->addDatabaseEntry($headId, $headId, new \DateTime('-1 hour')); - $idToVerify = $this->addDatabaseEntry($headId, $headId, new \DateTime()); - - $manager = $this->getManager(); - - for ($offset = 0; $offset < 3; $offset += 2) { - $tree = $manager->getTree($headId, 2, $offset); - - // Verifying the root comment - $this->assertArrayHasKey('comment', $tree); - $this->assertInstanceOf(IComment::class, $tree['comment']); - $this->assertSame($headId, $tree['comment']->getId()); - $this->assertArrayHasKey('replies', $tree); - $this->assertCount(2, $tree['replies']); - - // one level deep - foreach ($tree['replies'] as $reply) { - $this->assertInstanceOf(IComment::class, $reply['comment']); - $this->assertSame((string)$idToVerify, (string)$reply['comment']->getId()); - $this->assertCount(0, $reply['replies']); - $idToVerify--; - } - } - } - - public function testGetForObject(): void { - $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - $comments = $manager->getForObject('files', 'file64'); - - $this->assertIsArray($comments); - $this->assertCount(1, $comments); - $this->assertInstanceOf(IComment::class, $comments[0]); - $this->assertSame('nice one', $comments[0]->getMessage()); - } - - public function testGetForObjectWithLimitAndOffset(): void { - $this->addDatabaseEntry('0', '0', new \DateTime('-6 hours')); - $this->addDatabaseEntry('0', '0', new \DateTime('-5 hours')); - $this->addDatabaseEntry('1', '1', new \DateTime('-4 hours')); - $this->addDatabaseEntry('0', '0', new \DateTime('-3 hours')); - $this->addDatabaseEntry('2', '2', new \DateTime('-2 hours')); - $this->addDatabaseEntry('2', '2', new \DateTime('-1 hours')); - $idToVerify = $this->addDatabaseEntry('3', '1', new \DateTime()); - - $manager = $this->getManager(); - $offset = 0; - do { - $comments = $manager->getForObject('files', 'file64', 3, $offset); - - $this->assertIsArray($comments); - foreach ($comments as $key => $comment) { - $this->assertInstanceOf(IComment::class, $comment); - $this->assertSame('nice one', $comment->getMessage()); - $this->assertSame((string)$idToVerify, $comment->getId(), 'ID wrong for comment ' . $key . ' on offset: ' . $offset); - $idToVerify--; - } - $offset += 3; - } while (count($comments) > 0); - } - - public function testGetForObjectWithDateTimeConstraint(): void { - $this->addDatabaseEntry('0', '0', new \DateTime('-6 hours')); - $this->addDatabaseEntry('0', '0', new \DateTime('-5 hours')); - $id1 = $this->addDatabaseEntry('0', '0', new \DateTime('-3 hours')); - $id2 = $this->addDatabaseEntry('2', '2', new \DateTime('-2 hours')); - - $manager = $this->getManager(); - $comments = $manager->getForObject('files', 'file64', 0, 0, new \DateTime('-4 hours')); - - $this->assertCount(2, $comments); - $this->assertSame($id2, $comments[0]->getId()); - $this->assertSame($id1, $comments[1]->getId()); - } - - public function testGetForObjectWithLimitAndOffsetAndDateTimeConstraint(): void { - $this->addDatabaseEntry('0', '0', new \DateTime('-7 hours')); - $this->addDatabaseEntry('0', '0', new \DateTime('-6 hours')); - $this->addDatabaseEntry('1', '1', new \DateTime('-5 hours')); - $this->addDatabaseEntry('0', '0', new \DateTime('-3 hours')); - $this->addDatabaseEntry('2', '2', new \DateTime('-2 hours')); - $this->addDatabaseEntry('2', '2', new \DateTime('-1 hours')); - $idToVerify = $this->addDatabaseEntry('3', '1', new \DateTime()); - - $manager = $this->getManager(); - $offset = 0; - do { - $comments = $manager->getForObject('files', 'file64', 3, $offset, new \DateTime('-4 hours')); - - $this->assertIsArray($comments); - foreach ($comments as $comment) { - $this->assertInstanceOf(IComment::class, $comment); - $this->assertSame('nice one', $comment->getMessage()); - $this->assertSame((string)$idToVerify, $comment->getId()); - $this->assertGreaterThanOrEqual(4, $comment->getId()); - $idToVerify--; - } - $offset += 3; - } while (count($comments) > 0); - } - - public function testGetNumberOfCommentsForObject(): void { - for ($i = 1; $i < 5; $i++) { - $this->addDatabaseEntry('0', '0'); - } - - $manager = $this->getManager(); - - $amount = $manager->getNumberOfCommentsForObject('untype', '00'); - $this->assertSame(0, $amount); - - $amount = $manager->getNumberOfCommentsForObject('files', 'file64'); - $this->assertSame(4, $amount); - } - - public function testGetNumberOfUnreadCommentsForFolder(): void { - $folder = $this->createMock(Folder::class); - $fileIds = range(1111, 1114); - $children = array_map(function (int $id) { - $file = $this->createMock(Folder::class); - $file->method('getId') - ->willReturn($id); - return $file; - }, $fileIds); - $folder->method('getId')->willReturn(1000); - $folder->method('getDirectoryListing')->willReturn($children); - $this->rootFolder->method('getFirstNodeById') - ->with($folder->getId()) - ->willReturn($folder); - - // 2 comment for 1111 with 1 before read marker - // 2 comments for 1112 with no read marker - // 1 comment for 1113 before read marker - // 1 comment for 1114 with no read marker - $this->addDatabaseEntry('0', '0', null, null, $fileIds[1]); - for ($i = 0; $i < 4; $i++) { - $this->addDatabaseEntry('0', '0', null, null, $fileIds[$i]); - } - $this->addDatabaseEntry('0', '0', (new \DateTime())->modify('-2 days'), null, $fileIds[0]); - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('comment_test'); - - $manager = $this->getManager(); - - $manager->setReadMark('files', (string)$fileIds[0], (new \DateTime())->modify('-1 days'), $user); - $manager->setReadMark('files', (string)$fileIds[2], (new \DateTime()), $user); - - $amount = $manager->getNumberOfUnreadCommentsForFolder($folder->getId(), $user); - $this->assertEquals([ - $fileIds[0] => 1, - $fileIds[1] => 2, - $fileIds[3] => 1, - ], $amount); - } - - #[DataProvider(methodName: 'dataGetForObjectSince')] - public function testGetForObjectSince(?int $lastKnown, string $order, int $limit, int $resultFrom, int $resultTo): void { - $ids = []; - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - $comments = $manager->getForObjectSince('files', 'file64', ($lastKnown === null ? 0 : (int)$ids[$lastKnown]), $order, $limit); - - $expected = array_slice($ids, $resultFrom, $resultTo - $resultFrom + 1); - if ($order === 'desc') { - $expected = array_reverse($expected); - } - - $this->assertSame($expected, array_map(static fn (IComment $c): string => $c->getId(), $comments)); - } - - public static function dataGetForObjectSince(): array { - return [ - [null, 'asc', 20, 0, 4], - [null, 'asc', 2, 0, 1], - [null, 'desc', 20, 0, 4], - [null, 'desc', 2, 3, 4], - [1, 'asc', 20, 2, 4], - [1, 'asc', 2, 2, 3], - [3, 'desc', 20, 0, 2], - [3, 'desc', 2, 1, 2], - ]; - } - - public static function invalidCreateArgsProvider(): array { - return [ - ['', 'aId-1', 'oType-1', 'oId-1'], - ['aType-1', '', 'oType-1', 'oId-1'], - ['aType-1', 'aId-1', '', 'oId-1'], - ['aType-1', 'aId-1', 'oType-1', ''], - [1, 'aId-1', 'oType-1', 'oId-1'], - ['aType-1', 1, 'oType-1', 'oId-1'], - ['aType-1', 'aId-1', 1, 'oId-1'], - ['aType-1', 'aId-1', 'oType-1', 1], - ]; - } - - #[DataProvider(methodName: 'invalidCreateArgsProvider')] - public function testCreateCommentInvalidArguments(string|int $aType, string|int $aId, string|int $oType, string|int $oId): void { - $this->expectException(\InvalidArgumentException::class); - - $manager = $this->getManager(); - $manager->create($aType, $aId, $oType, $oId); - } - - public function testCreateComment(): void { - $actorType = 'bot'; - $actorId = 'bob'; - $objectType = 'weather'; - $objectId = 'bielefeld'; - - $comment = $this->getManager()->create($actorType, $actorId, $objectType, $objectId); - $this->assertInstanceOf(IComment::class, $comment); - $this->assertSame($actorType, $comment->getActorType()); - $this->assertSame($actorId, $comment->getActorId()); - $this->assertSame($objectType, $comment->getObjectType()); - $this->assertSame($objectId, $comment->getObjectId()); - } - - public function testDelete(): void { - $this->expectException(NotFoundException::class); - - $manager = $this->getManager(); - - $done = $manager->delete('404'); - $this->assertFalse($done); - - $done = $manager->delete('%'); - $this->assertFalse($done); - - $done = $manager->delete(''); - $this->assertFalse($done); - - $id = $this->addDatabaseEntry('0', '0'); - $comment = $manager->get($id); - $this->assertInstanceOf(IComment::class, $comment); - $done = $manager->delete($id); - $this->assertTrue($done); - $manager->get($id); - } - - #[DataProvider(methodName: 'providerTestSave')] - public function testSave(string $message, string $actorId, string $verb, ?string $parentId, ?string $id = ''): IComment { - $manager = $this->getManager(); - $comment = new Comment(); - $comment - ->setId($id) - ->setActor('users', $actorId) - ->setObject('files', 'file64') - ->setMessage($message) - ->setVerb($verb); - if ($parentId) { - $comment->setParentId($parentId); - } - - $saveSuccessful = $manager->save($comment); - $this->assertTrue($saveSuccessful, 'Comment saving was not successful'); - $this->assertNotEquals('', $comment->getId(), 'Comment ID should not be empty'); - $this->assertNotEquals('0', $comment->getId(), 'Comment ID should not be string \'0\''); - $this->assertNotNull($comment->getCreationDateTime(), 'Comment creation date should not be null'); - - $loadedComment = $manager->get($comment->getId()); - $this->assertSame($comment->getMessage(), $loadedComment->getMessage(), 'Comment message should match'); - $this->assertEquals($comment->getCreationDateTime()->getTimestamp(), $loadedComment->getCreationDateTime()->getTimestamp(), 'Comment creation date should match'); - return $comment; - } - - public static function providerTestSave(): array { - return [ - ['very beautiful, I am impressed!', 'alice', 'comment', null], - ]; - } - - public function testSaveUpdate(): void { - $manager = $this->getManager(); - $comment = new Comment(); - $comment - ->setActor('users', 'alice') - ->setObject('files', 'file64') - ->setMessage('very beautiful, I am impressed!') - ->setVerb('comment') - ->setExpireDate(new \DateTime('+2 hours')); - - $manager->save($comment); - - $loadedComment = $manager->get($comment->getId()); - // Compare current object with database values - $this->assertSame($comment->getMessage(), $loadedComment->getMessage()); - $this->assertSame( - $comment->getExpireDate()->format('Y-m-d H:i:s'), - $loadedComment->getExpireDate()->format('Y-m-d H:i:s') - ); - - // Preserve the original comment to compare after update - $original = clone $comment; - - // Update values - $comment->setMessage('very beautiful, I am really so much impressed!') - ->setExpireDate(new \DateTime('+1 hours')); - $manager->save($comment); - - $loadedComment = $manager->get($comment->getId()); - // Compare current object with database values - $this->assertSame($comment->getMessage(), $loadedComment->getMessage()); - $this->assertSame( - $comment->getExpireDate()->format('Y-m-d H:i:s'), - $loadedComment->getExpireDate()->format('Y-m-d H:i:s') - ); - - // Compare original object with database values - $this->assertNotSame($original->getMessage(), $loadedComment->getMessage()); - $this->assertNotSame( - $original->getExpireDate()->format('Y-m-d H:i:s'), - $loadedComment->getExpireDate()->format('Y-m-d H:i:s') - ); - } - - public function testSaveUpdateException(): void { - $manager = $this->getManager(); - $comment = new Comment(); - $comment - ->setActor('users', 'alice') - ->setObject('files', 'file64') - ->setMessage('very beautiful, I am impressed!') - ->setVerb('comment'); - - $manager->save($comment); - - $manager->delete($comment->getId()); - - $comment->setMessage('very beautiful, I am really so much impressed!'); - $this->expectException(NotFoundException::class); - $manager->save($comment); - } - - public function testSaveIncomplete(): void { - - $manager = $this->getManager(); - $comment = new Comment(); - $comment->setMessage('from no one to nothing'); - - $this->expectException(\UnexpectedValueException::class); - $manager->save($comment); - } - - public function testSaveAsChild(): void { - $id = $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - - for ($i = 0; $i < 3; $i++) { - $comment = new Comment(); - $comment - ->setActor('users', 'alice') - ->setObject('files', 'file64') - ->setParentId($id) - ->setMessage('full ack') - ->setVerb('comment') - // setting the creation time avoids using sleep() while making sure to test with different timestamps - ->setCreationDateTime(new \DateTime('+' . $i . ' minutes')); - - $manager->save($comment); - - $this->assertSame($id, $comment->getTopmostParentId()); - $parentComment = $manager->get($id); - $this->assertSame($i + 1, $parentComment->getChildrenCount()); - $this->assertEquals($comment->getCreationDateTime()->getTimestamp(), $parentComment->getLatestChildDateTime()->getTimestamp()); - } - } - - public static function invalidActorArgsProvider(): array { - return - [ - ['', ''], - [1, 'alice'], - ['users', 1], - ]; - } - - #[DataProvider(methodName: 'invalidActorArgsProvider')] - public function testDeleteReferencesOfActorInvalidInput(string|int $type, string|int $id): void { - $this->expectException(\InvalidArgumentException::class); - - $manager = $this->getManager(); - $manager->deleteReferencesOfActor($type, $id); - } - - public function testDeleteReferencesOfActor(): void { - $ids = []; - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - - // just to make sure they are really set, with correct actor data - $comment = $manager->get($ids[1]); - $this->assertSame('users', $comment->getActorType()); - $this->assertSame('alice', $comment->getActorId()); - - $wasSuccessful = $manager->deleteReferencesOfActor('users', 'alice'); - $this->assertTrue($wasSuccessful); - - foreach ($ids as $id) { - $comment = $manager->get($id); - $this->assertSame(ICommentsManager::DELETED_USER, $comment->getActorType()); - $this->assertSame(ICommentsManager::DELETED_USER, $comment->getActorId()); - } - - // actor info is gone from DB, but when database interaction is alright, - // we still expect to get true back - $wasSuccessful = $manager->deleteReferencesOfActor('users', 'alice'); - $this->assertTrue($wasSuccessful); - } - - public function testDeleteReferencesOfActorWithUserManagement(): void { - $user = Server::get(IUserManager::class)->createUser('xenia', 'NotAnEasyPassword123456+'); - $this->assertInstanceOf(IUser::class, $user); - - $manager = Server::get(ICommentsManager::class); - $comment = $manager->create('users', $user->getUID(), 'files', 'file64'); - $comment - ->setMessage('Most important comment I ever left on the Internet.') - ->setVerb('comment'); - $status = $manager->save($comment); - $this->assertTrue($status); - - $commentID = $comment->getId(); - $user->delete(); - - $comment = $manager->get($commentID); - $this->assertSame(ICommentsManager::DELETED_USER, $comment->getActorType()); - $this->assertSame(ICommentsManager::DELETED_USER, $comment->getActorId()); - } - - public static function invalidObjectArgsProvider(): array { - return - [ - ['', ''], - [1, 'file64'], - ['files', 1], - ]; - } - - #[DataProvider(methodName: 'invalidObjectArgsProvider')] - public function testDeleteCommentsAtObjectInvalidInput(string|int $type, string|int $id): void { - $this->expectException(\InvalidArgumentException::class); - - $manager = $this->getManager(); - $manager->deleteCommentsAtObject($type, $id); - } - - public function testDeleteCommentsAtObject(): void { - $ids = []; - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - $ids[] = $this->addDatabaseEntry('0', '0'); - - $manager = $this->getManager(); - - // just to make sure they are really set, with correct actor data - $comment = $manager->get($ids[1]); - $this->assertSame('files', $comment->getObjectType()); - $this->assertSame('file64', $comment->getObjectId()); - - $wasSuccessful = $manager->deleteCommentsAtObject('files', 'file64'); - $this->assertTrue($wasSuccessful); - - $verified = 0; - foreach ($ids as $id) { - try { - $manager->get($id); - } catch (NotFoundException) { - $verified++; - } - } - $this->assertSame(3, $verified); - - // actor info is gone from DB, but when database interaction is alright, - // we still expect to get true back - $wasSuccessful = $manager->deleteCommentsAtObject('files', 'file64'); - $this->assertTrue($wasSuccessful); - } - - public function testDeleteCommentsExpiredAtObjectTypeAndId(): void { - $ids = []; - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('+2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('+2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('+2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('-2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('-2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, null, new \DateTime('-2 hours')); - - /** @psalm-suppress DeprecatedInterface No way around at the moment */ - $manager = new Manager( - $this->connection, - $this->createMock(LoggerInterface::class), - $this->createMock(IConfig::class), - Server::get(ITimeFactory::class), - new EmojiHelper($this->connection), - $this->createMock(IInitialStateService::class), - $this->rootFolder, - $this->createMock(IEventDispatcher::class) - ); - - // just to make sure they are really set, with correct actor data - $comment = $manager->get($ids[1]); - $this->assertSame('files', $comment->getObjectType()); - $this->assertSame('file64', $comment->getObjectId()); - - $deleted = $manager->deleteCommentsExpiredAtObject('files', 'file64'); - $this->assertTrue($deleted); - - $deleted = 0; - $exists = 0; - foreach ($ids as $id) { - try { - $manager->get($id); - $exists++; - } catch (NotFoundException) { - $deleted++; - } - } - $this->assertSame(3, $exists); - $this->assertSame(3, $deleted); - - // actor info is gone from DB, but when database interaction is alright, - // we still expect to get true back - $deleted = $manager->deleteCommentsExpiredAtObject('files', 'file64'); - $this->assertFalse($deleted); - } - - public function testDeleteCommentsExpiredAtObjectType(): void { - $ids = []; - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file1', new \DateTime('-2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file2', new \DateTime('-2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file3', new \DateTime('-2 hours')); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file3', new \DateTime()); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file3', new \DateTime()); - $ids[] = $this->addDatabaseEntry('0', '0', null, null, 'file3', new \DateTime()); - - /** @psalm-suppress DeprecatedInterface No way around at the moment */ - $manager = new Manager( - $this->connection, - $this->createMock(LoggerInterface::class), - $this->createMock(IConfig::class), - Server::get(ITimeFactory::class), - new EmojiHelper($this->connection), - $this->createMock(IInitialStateService::class), - $this->rootFolder, - $this->createMock(IEventDispatcher::class) - ); - - $deleted = $manager->deleteCommentsExpiredAtObject('files'); - $this->assertTrue($deleted); - - $deleted = 0; - $exists = 0; - foreach ($ids as $id) { - try { - $manager->get($id); - $exists++; - } catch (NotFoundException) { - $deleted++; - } - } - $this->assertSame(0, $exists); - $this->assertSame(6, $deleted); - - // actor info is gone from DB, but when database interaction is alright, - // we still expect to get true back - $deleted = $manager->deleteCommentsExpiredAtObject('files'); - $this->assertFalse($deleted); - } - - public function testSetMarkRead(): void { - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('alice'); - - $dateTimeSet = new \DateTime(); - - $manager = $this->getManager(); - $manager->setReadMark('robot', '36', $dateTimeSet, $user); - - $dateTimeGet = $manager->getReadMark('robot', '36', $user); - - $this->assertEquals($dateTimeSet->getTimestamp(), $dateTimeGet->getTimestamp()); - } - - public function testSetMarkReadUpdate(): void { - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('alice'); - - $dateTimeSet = new \DateTime('yesterday'); - - $manager = $this->getManager(); - $manager->setReadMark('robot', '36', $dateTimeSet, $user); - - $dateTimeSet = new \DateTime('today'); - $manager->setReadMark('robot', '36', $dateTimeSet, $user); - - $dateTimeGet = $manager->getReadMark('robot', '36', $user); - - $this->assertEquals($dateTimeSet, $dateTimeGet); - } - - public function testReadMarkDeleteUser(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('alice'); - - $dateTimeSet = new \DateTime(); - - $manager = $this->getManager(); - $manager->setReadMark('robot', '36', $dateTimeSet, $user); - - $manager->deleteReadMarksFromUser($user); - $dateTimeGet = $manager->getReadMark('robot', '36', $user); - - $this->assertNull($dateTimeGet); - } - - public function testReadMarkDeleteObject(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('alice'); - - $dateTimeSet = new \DateTime(); - - $manager = $this->getManager(); - $manager->setReadMark('robot', '36', $dateTimeSet, $user); - - $manager->deleteReadMarksOnObject('robot', '36'); - $dateTimeGet = $manager->getReadMark('robot', '36', $user); - - $this->assertNull($dateTimeGet); - } - - public function testSendEvent(): void { - /** @psalm-suppress DeprecatedInterface Test for deprecated interface */ - $handler1 = $this->createMock(ICommentsEventHandler::class); - $handler1->expects($this->exactly(4)) - ->method('handle'); - - /** @psalm-suppress DeprecatedInterface Test for deprecated interface */ - $handler2 = $this->createMock(ICommentsEventHandler::class); - $handler1->expects($this->exactly(4)) - ->method('handle'); - - $manager = $this->getManager(); - $manager->registerEventHandler(function () use ($handler1) { - return $handler1; - }); - $manager->registerEventHandler(function () use ($handler2) { - return $handler2; - }); - - $comment = new Comment(); - $comment - ->setActor('users', 'alice') - ->setObject('files', 'file64') - ->setMessage('very beautiful, I am impressed!') - ->setVerb('comment'); - - // Add event - $manager->save($comment); - - // Update event - $comment->setMessage('Different topic'); - $manager->save($comment); - - // Delete event - $manager->delete($comment->getId()); - } - - public function testResolveDisplayName(): void { - $manager = $this->getManager(); - - $planetClosure = function ($name) { - return ucfirst($name); - }; - - $galaxyClosure = function ($name) { - return strtoupper($name); - }; - - $manager->registerDisplayNameResolver('planet', $planetClosure); - $manager->registerDisplayNameResolver('galaxy', $galaxyClosure); - - $this->assertSame('Neptune', $manager->resolveDisplayName('planet', 'neptune')); - $this->assertSame('SOMBRERO', $manager->resolveDisplayName('galaxy', 'sombrero')); - } - - public function testRegisterResolverDuplicate(): void { - $this->expectException(\OutOfBoundsException::class); - - $manager = $this->getManager(); - - $planetClosure = static fn (string $name): string => ucfirst($name); - $manager->registerDisplayNameResolver('planet', $planetClosure); - $manager->registerDisplayNameResolver('planet', $planetClosure); - } - - public function testResolveDisplayNameUnregisteredType(): void { - $this->expectException(\OutOfBoundsException::class); - - $manager = $this->getManager(); - - $planetClosure = static fn (string $name): string => ucfirst($name); - $manager->registerDisplayNameResolver('planet', $planetClosure); - $manager->resolveDisplayName('galaxy', 'sombrero'); - } - - public function testResolveDisplayNameDirtyResolver(): void { - $manager = $this->getManager(); - - $planetClosure = static fn (): null => null; - $manager->registerDisplayNameResolver('planet', $planetClosure); - $this->assertIsString($manager->resolveDisplayName('planet', 'neptune')); - } - - private function skipIfNotSupport4ByteUTF(): void { - if (!$this->getManager()->supportReactions()) { - $this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8'); - } - } - - #[DataProvider(methodName: 'providerTestReactionAddAndDelete')] - public function testReactionAddAndDelete(array $comments, array $reactionsExpected): void { - $this->skipIfNotSupport4ByteUTF(); - $manager = $this->getManager(); - - $processedComments = $this->proccessComments($comments); - $comment = end($processedComments); - if ($comment->getParentId()) { - $parent = $manager->get($comment->getParentId()); - $this->assertEqualsCanonicalizing($reactionsExpected, $parent->getReactions()); - } - } - - public static function providerTestReactionAddAndDelete(): array { - return[ - [ - [ - ['message', 'alice', 'comment', null], - ], [], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ], ['👍' => 1], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'alice', 'reaction', 'message#alice'], - ], ['👍' => 1], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ], ['👍' => 2], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction_deleted', 'message#alice'], - ], ['👍' => 1], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ['👍', 'alice', 'reaction_deleted', 'message#alice'], - ['👍', 'frank', 'reaction_deleted', 'message#alice'], - ], [], - ], - ]; - } - - /** - * @param array $data - * @return array - */ - private function proccessComments(array $data): array { - $this->connection->beginTransaction(); - /** @var array $comments */ - $comments = []; - foreach ($data as $comment) { - [$message, $actorId, $verb, $parentText] = $comment; - $parentId = null; - if ($parentText) { - $parentId = $comments[$parentText]->getId(); - } - $id = ''; - if ($verb === 'reaction_deleted') { - $id = $comments[$message . '#' . $actorId]->getId(); - } - $comment = $this->testSave($message, $actorId, $verb, $parentId, $id); - $comments[$comment->getMessage() . '#' . $comment->getActorId()] = $comment; - } - $this->connection->commit(); - return $comments; - } - - #[DataProvider(methodName: 'providerTestRetrieveAllReactions')] - public function testRetrieveAllReactions(array $comments, array $expected): void { - $this->skipIfNotSupport4ByteUTF(); - $manager = $this->getManager(); - - $processedComments = $this->proccessComments($comments); - $comment = reset($processedComments); - $this->invokePrivate($this, 'chunkSize', [50]); - $all = $manager->retrieveAllReactions((int)$comment->getId()); - $actual = array_map(static function (IComment $row): array { - return [ - $row->getActorId(), - $row->getMessage(), - ]; - }, $all); - - usort($actual, static fn (array $a, array $b): int => $a[1] <=> $b[1]); - usort($expected, static fn (array $a, array $b): int => $a[1] <=> $b[1]); - - $this->assertEqualsCanonicalizing($expected, $actual); - } - - public static function providerTestRetrieveAllReactions(): array { - return [ - [ - [ - ['message', 'alice', 'comment', null], - ], - [], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ], - [ - ['👍', 'alice'], - ['👍', 'frank'], - ], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ], - [ - ['👍', 'alice'], - ['👍', 'frank'], - ], - ], - [# 60 reactions to cover chunk size when retrieve comments of reactions. - [ - ['message', 'alice', 'comment', null], - ['😀', 'alice', 'reaction', 'message#alice'], - ['😃', 'alice', 'reaction', 'message#alice'], - ['😄', 'alice', 'reaction', 'message#alice'], - ['😁', 'alice', 'reaction', 'message#alice'], - ['😆', 'alice', 'reaction', 'message#alice'], - ['😅', 'alice', 'reaction', 'message#alice'], - ['😂', 'alice', 'reaction', 'message#alice'], - ['🤣', 'alice', 'reaction', 'message#alice'], - ['🥲', 'alice', 'reaction', 'message#alice'], - ['🥹', 'alice', 'reaction', 'message#alice'], - ['☺️', 'alice', 'reaction', 'message#alice'], - ['😊', 'alice', 'reaction', 'message#alice'], - ['😇', 'alice', 'reaction', 'message#alice'], - ['🙂', 'alice', 'reaction', 'message#alice'], - ['🙃', 'alice', 'reaction', 'message#alice'], - ['😉', 'alice', 'reaction', 'message#alice'], - ['😌', 'alice', 'reaction', 'message#alice'], - ['😍', 'alice', 'reaction', 'message#alice'], - ['🥰', 'alice', 'reaction', 'message#alice'], - ['😘', 'alice', 'reaction', 'message#alice'], - ['😗', 'alice', 'reaction', 'message#alice'], - ['😙', 'alice', 'reaction', 'message#alice'], - ['😚', 'alice', 'reaction', 'message#alice'], - ['😋', 'alice', 'reaction', 'message#alice'], - ['😛', 'alice', 'reaction', 'message#alice'], - ['😝', 'alice', 'reaction', 'message#alice'], - ['😜', 'alice', 'reaction', 'message#alice'], - ['🤪', 'alice', 'reaction', 'message#alice'], - ['🤨', 'alice', 'reaction', 'message#alice'], - ['🧐', 'alice', 'reaction', 'message#alice'], - ], - [ - ['😀', 'alice'], - ['😃', 'alice'], - ['😄', 'alice'], - ['😁', 'alice'], - ['😆', 'alice'], - ['😅', 'alice'], - ['😂', 'alice'], - ['🤣', 'alice'], - ['🥲', 'alice'], - ['🥹', 'alice'], - ['☺️', 'alice'], - ['😊', 'alice'], - ['😇', 'alice'], - ['🙂', 'alice'], - ['🙃', 'alice'], - ['😉', 'alice'], - ['😌', 'alice'], - ['😍', 'alice'], - ['🥰', 'alice'], - ['😘', 'alice'], - ['😗', 'alice'], - ['😙', 'alice'], - ['😚', 'alice'], - ['😋', 'alice'], - ['😛', 'alice'], - ['😝', 'alice'], - ['😜', 'alice'], - ['🤪', 'alice'], - ['🤨', 'alice'], - ['🧐', 'alice'], - ], - ], - ]; - } - - #[DataProvider(methodName: 'providerTestRetrieveAllReactionsWithSpecificReaction')] - public function testRetrieveAllReactionsWithSpecificReaction(array $comments, string $reaction, array $expected): void { - $this->skipIfNotSupport4ByteUTF(); - $manager = $this->getManager(); - - $processedComments = $this->proccessComments($comments); - $comment = reset($processedComments); - $all = $manager->retrieveAllReactionsWithSpecificReaction((int)$comment->getId(), $reaction); - $actual = array_map(static function (IComment $row): array { - return [ - $row->getActorId(), - $row->getMessage(), - ]; - }, $all); - $this->assertEqualsCanonicalizing($expected, $actual); - } - - public static function providerTestRetrieveAllReactionsWithSpecificReaction(): array { - return [ - [ - [ - ['message', 'alice', 'comment', null], - ], - '👎', - [], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ], - '👍', - [ - ['👍', 'alice'], - ['👍', 'frank'], - ], - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ['👎', 'alice', 'reaction', 'message#alice'], - ['👍', 'frank', 'reaction', 'message#alice'], - ], - '👎', - [ - ['👎', 'alice'], - ], - ], - ]; - } - - #[DataProvider(methodName: 'providerTestGetReactionComment')] - public function testGetReactionComment(array $comments, array $expected, bool $notFound): void { - $this->skipIfNotSupport4ByteUTF(); - $manager = $this->getManager(); - - $processedComments = $this->proccessComments($comments); - - $keys = ['message', 'actorId', 'verb', 'parent']; - $expected = array_combine($keys, $expected); - - if ($notFound) { - $this->expectException(NotFoundException::class); - } - $comment = $processedComments[$expected['message'] . '#' . $expected['actorId']]; - $actual = $manager->getReactionComment((int)$comment->getParentId(), $comment->getActorType(), $comment->getActorId(), $comment->getMessage()); - if (!$notFound) { - $this->assertEquals($expected['message'], $actual->getMessage()); - $this->assertEquals($expected['actorId'], $actual->getActorId()); - $this->assertEquals($expected['verb'], $actual->getVerb()); - $this->assertEquals($processedComments[$expected['parent']]->getId(), $actual->getParentId()); - } - } - - public static function providerTestGetReactionComment(): array { - return [ - [ - [ - ['message', 'Matthew', 'comment', null], - ['👍', 'Matthew', 'reaction', 'message#Matthew'], - ['👍', 'Mark', 'reaction', 'message#Matthew'], - ['👍', 'Luke', 'reaction', 'message#Matthew'], - ['👍', 'John', 'reaction', 'message#Matthew'], - ], - ['👍', 'Matthew', 'reaction', 'message#Matthew'], - false, - ], - [ - [ - ['message', 'Matthew', 'comment', null], - ['👍', 'Matthew', 'reaction', 'message#Matthew'], - ['👍', 'Mark', 'reaction', 'message#Matthew'], - ['👍', 'Luke', 'reaction', 'message#Matthew'], - ['👍', 'John', 'reaction', 'message#Matthew'], - ], - ['👍', 'Mark', 'reaction', 'message#Matthew'], - false, - ], - [ - [ - ['message', 'Matthew', 'comment', null], - ['👎', 'Matthew', 'reaction', 'message#Matthew'], - ], - ['👎', 'Matthew', 'reaction', 'message#Matthew'], - false, - ], - [ - [ - ['message', 'Matthew', 'comment', null], - ['👎', 'Matthew', 'reaction', 'message#Matthew'], - ['👎', 'Matthew', 'reaction_deleted', 'message#Matthew'], - ], - ['👎', 'Matthew', 'reaction', 'message#Matthew'], - true, - ], - ]; - } - - #[DataProvider(methodName: 'providerTestReactionMessageSize')] - public function testReactionMessageSize(string $reactionString, bool $valid): void { - $this->skipIfNotSupport4ByteUTF(); - if (!$valid) { - $this->expectException(\UnexpectedValueException::class); - } - - $manager = $this->getManager(); - $comment = new Comment(); - $comment->setMessage($reactionString) - ->setVerb('reaction') - ->setActor('users', 'alice') - ->setObject('files', 'file64'); - $status = $manager->save($comment); - $this->assertTrue($status); - } - - public static function providerTestReactionMessageSize(): array { - return [ - ['a', false], - ['1', false], - ['👍', true], - ['👍👍', false], - ['👍🏽', true], - ['👨🏽‍💻', true], - ['👨🏽‍💻👍', false], - ]; - } - - #[DataProvider(methodName: 'providerTestReactionsSummarizeOrdered')] - public function testReactionsSummarizeOrdered(array $comments, array $expected, bool $isFullMatch): void { - $this->skipIfNotSupport4ByteUTF(); - $manager = $this->getManager(); - - $processedComments = $this->proccessComments($comments); - $comment = end($processedComments); - $actual = $manager->get($comment->getParentId()); - - if ($isFullMatch) { - $this->assertSame($expected, $actual->getReactions()); - } else { - $subResult = array_slice($actual->getReactions(), 0, count($expected)); - $this->assertSame($expected, $subResult); - } - } - - public static function providerTestReactionsSummarizeOrdered(): array { - return [ - [ - [ - ['message', 'alice', 'comment', null], - ['👍', 'alice', 'reaction', 'message#alice'], - ], - ['👍' => 1], - true, - ], - [ - [ - ['message', 'alice', 'comment', null], - ['👎', 'John', 'reaction', 'message#alice'], - ['💼', 'Luke', 'reaction', 'message#alice'], - ['📋', 'Luke', 'reaction', 'message#alice'], - ['🚀', 'Luke', 'reaction', 'message#alice'], - ['🖤', 'Luke', 'reaction', 'message#alice'], - ['😜', 'Luke', 'reaction', 'message#alice'], - ['🌖', 'Luke', 'reaction', 'message#alice'], - ['💖', 'Luke', 'reaction', 'message#alice'], - ['📥', 'Luke', 'reaction', 'message#alice'], - ['🐉', 'Luke', 'reaction', 'message#alice'], - ['☕', 'Luke', 'reaction', 'message#alice'], - ['🐄', 'Luke', 'reaction', 'message#alice'], - ['🐕', 'Luke', 'reaction', 'message#alice'], - ['🐈', 'Luke', 'reaction', 'message#alice'], - ['🛂', 'Luke', 'reaction', 'message#alice'], - ['🕸', 'Luke', 'reaction', 'message#alice'], - ['🏰', 'Luke', 'reaction', 'message#alice'], - ['⚙️', 'Luke', 'reaction', 'message#alice'], - ['🚨', 'Luke', 'reaction', 'message#alice'], - ['👥', 'Luke', 'reaction', 'message#alice'], - ['👍', 'Paul', 'reaction', 'message#alice'], - ['👍', 'Peter', 'reaction', 'message#alice'], - ['💜', 'Matthew', 'reaction', 'message#alice'], - ['💜', 'Mark', 'reaction', 'message#alice'], - ['💜', 'Luke', 'reaction', 'message#alice'], - ], - [ - '💜' => 3, - '👍' => 2, - ], - false, - ], - ]; - } -} --- a/tests/lib/Config/LexiconTest.php +++ /dev/null @@ -1,262 +0,0 @@ -getRegistrationContext()?->registerConfigLexicon(TestConfigLexicon_I::APPID, TestConfigLexicon_I::class); - $bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_N::APPID, TestLexicon_N::class); - $bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_W::APPID, TestLexicon_W::class); - $bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_E::APPID, TestLexicon_E::class); - - $this->appConfig = Server::get(IAppConfig::class); - $this->userConfig = Server::get(IUserConfig::class); - $this->configManager = Server::get(ConfigManager::class); - $this->appManager = Server::get(IAppManager::class); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - - $this->appConfig->deleteApp(TestConfigLexicon_I::APPID); - $this->appConfig->deleteApp(TestLexicon_N::APPID); - $this->appConfig->deleteApp(TestLexicon_W::APPID); - $this->appConfig->deleteApp(TestLexicon_E::APPID); - $this->appConfig->deleteApp(TestLexicon_UserIndexed::APPID); - $this->appConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID); - - $this->userConfig->deleteApp(TestConfigLexicon_I::APPID); - $this->userConfig->deleteApp(TestLexicon_N::APPID); - $this->userConfig->deleteApp(TestLexicon_W::APPID); - $this->userConfig->deleteApp(TestLexicon_E::APPID); - $this->userConfig->deleteApp(TestLexicon_UserIndexed::APPID); - $this->userConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID); - } - - public function testAppLexiconSetCorrect() { - $this->assertSame(true, $this->appConfig->setValueString(TestLexicon_E::APPID, 'key1', 'new_value')); - $this->assertSame(true, $this->appConfig->isLazy(TestLexicon_E::APPID, 'key1')); - $this->assertSame(true, $this->appConfig->isSensitive(TestLexicon_E::APPID, 'key1')); - $this->appConfig->deleteKey(TestLexicon_E::APPID, 'key1'); - } - - public function testAppLexiconGetCorrect() { - $this->assertSame('abcde', $this->appConfig->getValueString(TestLexicon_E::APPID, 'key1', 'default')); - } - - public function testAppLexiconSetIncorrectValueType() { - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->setValueInt(TestLexicon_E::APPID, 'key1', -1); - } - - public function testAppLexiconGetIncorrectValueType() { - $this->expectException(AppConfigTypeConflictException::class); - $this->appConfig->getValueInt(TestLexicon_E::APPID, 'key1'); - } - - public function testAppLexiconIgnore() { - $this->appConfig->setValueString(TestConfigLexicon_I::APPID, 'key_ignore', 'new_value'); - $this->assertSame('new_value', $this->appConfig->getValueString(TestConfigLexicon_I::APPID, 'key_ignore', '')); - } - - public function testAppLexiconNotice() { - $this->appConfig->setValueString(TestLexicon_N::APPID, 'key_notice', 'new_value'); - $this->assertSame('new_value', $this->appConfig->getValueString(TestLexicon_N::APPID, 'key_notice', '')); - } - - public function testAppLexiconWarning() { - $this->appConfig->setValueString(TestLexicon_W::APPID, 'key_warning', 'new_value'); - $this->assertSame('', $this->appConfig->getValueString(TestLexicon_W::APPID, 'key_warning', '')); - } - - public function testAppLexiconSetException() { - $this->expectException(AppConfigUnknownKeyException::class); - $this->appConfig->setValueString(TestLexicon_E::APPID, 'key_exception', 'new_value'); - $this->assertSame('', $this->appConfig->getValueString(TestLexicon_E::APPID, 'key3', '')); - } - - public function testAppLexiconGetException() { - $this->expectException(AppConfigUnknownKeyException::class); - $this->appConfig->getValueString(TestLexicon_E::APPID, 'key_exception'); - } - - public function testUserLexiconSetCorrect() { - $this->assertSame(true, $this->userConfig->setValueString('user1', TestLexicon_E::APPID, 'key1', 'new_value')); - $this->assertSame(true, $this->userConfig->isLazy('user1', TestLexicon_E::APPID, 'key1')); - $this->assertSame(true, $this->userConfig->isSensitive('user1', TestLexicon_E::APPID, 'key1')); - $this->userConfig->deleteKey(TestLexicon_E::APPID, 'key1'); - } - - public function testUserLexiconGetCorrect() { - $this->assertSame('abcde', $this->userConfig->getValueString('user1', TestLexicon_E::APPID, 'key1', 'default')); - } - - public function testUserLexiconSetIncorrectValueType() { - $this->expectException(TypeConflictException::class); - $this->userConfig->setValueInt('user1', TestLexicon_E::APPID, 'key1', -1); - } - - public function testUserLexiconGetIncorrectValueType() { - $this->expectException(TypeConflictException::class); - $this->userConfig->getValueInt('user1', TestLexicon_E::APPID, 'key1'); - } - - public function testUserLexiconIgnore() { - $this->userConfig->setValueString('user1', TestConfigLexicon_I::APPID, 'key_ignore', 'new_value'); - $this->assertSame('new_value', $this->userConfig->getValueString('user1', TestConfigLexicon_I::APPID, 'key_ignore', '')); - } - - public function testUserLexiconNotice() { - $this->userConfig->setValueString('user1', TestLexicon_N::APPID, 'key_notice', 'new_value'); - $this->assertSame('new_value', $this->userConfig->getValueString('user1', TestLexicon_N::APPID, 'key_notice', '')); - } - - public function testUserLexiconWarning() { - $this->userConfig->setValueString('user1', TestLexicon_W::APPID, 'key_warning', 'new_value'); - $this->assertSame('', $this->userConfig->getValueString('user1', TestLexicon_W::APPID, 'key_warning', '')); - } - - public function testUserLexiconSetException() { - $this->expectException(UnknownKeyException::class); - $this->userConfig->setValueString('user1', TestLexicon_E::APPID, 'key_exception', 'new_value'); - $this->assertSame('', $this->userConfig->getValueString('user1', TestLexicon_E::APPID, 'key5', '')); - } - - public function testUserLexiconGetException() { - $this->expectException(UnknownKeyException::class); - $this->userConfig->getValueString('user1', TestLexicon_E::APPID, 'key_exception'); - } - - public function testAppConfigLexiconRenameSetNewValue() { - $this->assertSame(12345, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 994); - $this->assertSame(994, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); - } - - public function testAppConfigLexiconRenameSetOldValuePreMigration() { - $this->appConfig->ignoreLexiconAliases(true); - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 993); - $this->appConfig->ignoreLexiconAliases(false); - $this->assertSame(12345, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); - } - - public function testAppConfigLexiconRenameSetOldValuePostMigration() { - $this->appConfig->ignoreLexiconAliases(true); - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 994); - $this->appConfig->ignoreLexiconAliases(false); - $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); - $this->assertSame(994, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); - } - - public function testAppConfigLexiconRenameGetNewValue() { - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 981); - $this->assertSame(981, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); - } - - public function testAppConfigLexiconRenameGetOldValuePreMigration() { - $this->appConfig->ignoreLexiconAliases(true); - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 984); - $this->assertSame(123, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); - $this->appConfig->ignoreLexiconAliases(false); - } - - public function testAppConfigLexiconRenameGetOldValuePostMigration() { - $this->appConfig->ignoreLexiconAliases(true); - $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 987); - $this->appConfig->ignoreLexiconAliases(false); - $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); - $this->assertSame(987, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); - } - - public function testAppConfigLexiconRenameInvertBoolean() { - $this->appConfig->ignoreLexiconAliases(true); - $this->appConfig->setValueBool(TestConfigLexicon_I::APPID, 'old_key4', true); - $this->appConfig->ignoreLexiconAliases(false); - $this->assertSame(true, $this->appConfig->getValueBool(TestConfigLexicon_I::APPID, 'key4')); - $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); - $this->assertSame(false, $this->appConfig->getValueBool(TestConfigLexicon_I::APPID, 'key4')); - } - - public function testLexiconIndexedUpdate() { - $this->userConfig->setValueString('user1', TestLexicon_UserIndexed::APPID, 'key1', 'abcd'); - $this->userConfig->setValueString('user2', TestLexicon_UserIndexed::APPID, 'key1', '1234', flags: 64); - $this->userConfig->setValueString('user3', TestLexicon_UserIndexed::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED); - $this->userConfig->setValueString('user4', TestLexicon_UserIndexed::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED); - - $bootstrapCoordinator = Server::get(Coordinator::class); - $bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexed::APPID, TestLexicon_UserIndexed::class); - $this->userConfig->clearCacheAll(); - - $this->configManager->updateLexiconEntries(TestLexicon_UserIndexed::APPID); - - $this->assertTrue($this->userConfig->isIndexed('user1', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertTrue($this->userConfig->isIndexed('user2', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertTrue($this->userConfig->isIndexed('user3', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertTrue($this->userConfig->isIndexed('user4', TestLexicon_UserIndexed::APPID, 'key1')); - - $this->assertSame(2, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertSame(66, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertSame(2, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexed::APPID, 'key1')); - $this->assertSame(66, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexed::APPID, 'key1')); - } - - public function testLexiconIndexedUpdateRemove() { - $this->userConfig->setValueString('user1', TestLexicon_UserIndexedRemove::APPID, 'key1', 'abcd'); - $this->userConfig->setValueString('user2', TestLexicon_UserIndexedRemove::APPID, 'key1', '1234', flags: 64); - $this->userConfig->setValueString('user3', TestLexicon_UserIndexedRemove::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED); - $this->userConfig->setValueString('user4', TestLexicon_UserIndexedRemove::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED); - - $bootstrapCoordinator = Server::get(Coordinator::class); - $bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexedRemove::APPID, TestLexicon_UserIndexedRemove::class); - $this->userConfig->clearCacheAll(); - - $this->configManager->updateLexiconEntries(TestLexicon_UserIndexedRemove::APPID); - - $this->assertFalse($this->userConfig->isIndexed('user1', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertFalse($this->userConfig->isIndexed('user2', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertFalse($this->userConfig->isIndexed('user3', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertFalse($this->userConfig->isIndexed('user4', TestLexicon_UserIndexedRemove::APPID, 'key1')); - - $this->assertSame(0, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertSame(64, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertSame(0, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexedRemove::APPID, 'key1')); - $this->assertSame(64, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexedRemove::APPID, 'key1')); - } -} --- a/tests/lib/Config/TestConfigLexicon_I.php +++ /dev/null @@ -1,44 +0,0 @@ - match ($p) { - Preset::FAMILY => 'family', - Preset::CLUB, Preset::MEDIUM => 'club+medium', - default => 'none', - }, 'test key'), - ]; - } - - #[\Override] - public function getUserConfigs(): array { - return [ - new Entry('key1', ValueType::STRING, 'abcde', 'test key', true, IUserConfig::FLAG_SENSITIVE), - new Entry('key2', ValueType::INT, 12345, 'test key', false), - new Entry('key3', ValueType::STRING, fn (Preset $p): string => match ($p) { - Preset::FAMILY => 'family', - Preset::CLUB, Preset::MEDIUM => 'club+medium', - default => 'none', - }, 'test key'), - ]; - } -} --- a/tests/lib/Config/TestLexicon_N.php +++ /dev/null @@ -1,42 +0,0 @@ -connection = $this->createMock(IDBConnection::class); - $this->config = $this->createMock(IConfig::class); - $this->configManager = $this->createMock(ConfigManager::class); - $this->presetManager = $this->createMock(PresetManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - } - - private function getUserConfig(): UserConfig { - return new UserConfig( - $this->connection, - $this->config, - $this->configManager, - $this->presetManager, - $this->logger, - $this->crypto, - $this->dispatcher, - ); - } - - private function createInvalidFieldNameException(): DBException { - $driverException = $this->createMock(\Doctrine\DBAL\Driver\Exception::class); - $dbalException = new InvalidFieldNameException($driverException, null); - return DbalException::wrap($dbalException); - } - - private function createMockQueryBuilder(): IQueryBuilder&MockObject { - $expression = $this->createMock(IExpressionBuilder::class); - $qb = $this->createMock(IQueryBuilder::class); - $qb->method('from')->willReturn($qb); - $qb->method('select')->willReturn($qb); - $qb->method('addSelect')->willReturn($qb); - $qb->method('where')->willReturn($qb); - $qb->method('andWhere')->willReturn($qb); - $qb->method('set')->willReturn($qb); - $qb->method('expr')->willReturn($expression); - $qb->method('insert')->willReturn($qb); - $qb->method('setValue')->willReturn($qb); - $qb->method('createNamedParameter')->willReturn('?'); - return $qb; - } - - /** - * Test that loadConfig retries without new columns on InvalidFieldNameException. - */ - public function testLoadConfigFallsBackOnMissingColumns(): void { - $exception = $this->createInvalidFieldNameException(); - - $successResult = $this->createMock(IResult::class); - $successResult->method('fetchAllAssociative')->willReturn([ - ['appid' => 'settings', 'configkey' => 'email', 'configvalue' => 'user@example.com'], - ]); - - $qb = $this->createMockQueryBuilder(); - $qb->method('executeQuery') - ->willReturnOnConsecutiveCalls( - $this->throwException($exception), - $successResult, - ); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $userConfig = $this->getUserConfig(); - - $value = $userConfig->getValueString('user1', 'settings', 'email'); - $this->assertSame('user@example.com', $value); - } - - /** - * Test that non-INVALID_FIELD_NAME exceptions are re-thrown. - */ - public function testLoadConfigRethrowsOtherExceptions(): void { - $driverException = $this->createMock(\Doctrine\DBAL\Driver\Exception::class); - $dbalException = new \Doctrine\DBAL\Exception\SyntaxErrorException($driverException, null); - $exception = DbalException::wrap($dbalException); - - $qb = $this->createMockQueryBuilder(); - $qb->method('executeQuery')->willThrowException($exception); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $userConfig = $this->getUserConfig(); - - $this->expectException(DBException::class); - $userConfig->getValueString('user1', 'settings', 'email'); - } - - /** - * Test that insert omits new columns when migration is not completed. - */ - public function testInsertOmitsNewColumnsInFallbackMode(): void { - $exception = $this->createInvalidFieldNameException(); - - $loadResult = $this->createMock(IResult::class); - $loadResult->method('fetchAllAssociative')->willReturn([]); - - $qb = $this->createMockQueryBuilder(); - - $callCount = 0; - $qb->method('executeQuery') - ->willReturnCallback(function () use ($exception, $loadResult, &$callCount) { - $callCount++; - if ($callCount === 1) { - throw $exception; - } - return $loadResult; - }); - - $qb->expects(self::once())->method('insert')->with('preferences')->willReturn($qb); - $qb->method('executeStatement')->willReturn(1); - - $setColumns = []; - $qb->method('setValue') - ->willReturnCallback(function (string $column) use ($qb, &$setColumns) { - $setColumns[] = $column; - return $qb; - }); - - $this->connection->method('getQueryBuilder')->willReturn($qb); - - $userConfig = $this->getUserConfig(); - $userConfig->setValueString('user1', 'settings', 'email', 'user@example.com'); - - $this->assertContains('userid', $setColumns); - $this->assertContains('appid', $setColumns); - $this->assertContains('configkey', $setColumns); - $this->assertContains('configvalue', $setColumns); - $this->assertNotContains('lazy', $setColumns, 'lazy column should be omitted in fallback mode'); - $this->assertNotContains('type', $setColumns, 'type column should be omitted in fallback mode'); - $this->assertNotContains('flags', $setColumns, 'flags column should be omitted in fallback mode'); - $this->assertNotContains('indexed', $setColumns, 'indexed column should be omitted in fallback mode'); - } -} --- a/tests/lib/Config/UserConfigTest.php +++ /dev/null @@ -1,1696 +0,0 @@ ->> [userId => [appId => prefKey, prefValue, valueType, lazy, sensitive]]] - */ - private array $basePreferences - = [ - 'user1' - => [ - 'app1' => [ - 'key1' => ['key1', 'value1'], - 'key22' => ['key22', '31'], - 'fast_string' => ['fast_string', 'f_value', ValueType::STRING], - 'lazy_string' => ['lazy_string', 'l_value', ValueType::STRING, true], - 'fast_string_sensitive' => [ - 'fast_string_sensitive', 'fs_value', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE - ], - 'lazy_string_sensitive' => [ - 'lazy_string_sensitive', 'ls_value', ValueType::STRING, true, UserConfig::FLAG_SENSITIVE - ], - 'fast_int' => ['fast_int', 11, ValueType::INT], - 'lazy_int' => ['lazy_int', 12, ValueType::INT, true], - 'fast_int_sensitive' => ['fast_int_sensitive', 2024, ValueType::INT, false, UserConfig::FLAG_SENSITIVE], - 'lazy_int_sensitive' => ['lazy_int_sensitive', 2048, ValueType::INT, true, UserConfig::FLAG_SENSITIVE], - 'fast_float' => ['fast_float', 3.14, ValueType::FLOAT], - 'lazy_float' => ['lazy_float', 3.14159, ValueType::FLOAT, true], - 'fast_float_sensitive' => [ - 'fast_float_sensitive', 1.41, ValueType::FLOAT, false, UserConfig::FLAG_SENSITIVE - ], - 'lazy_float_sensitive' => [ - 'lazy_float_sensitive', 1.4142, ValueType::FLOAT, true, UserConfig::FLAG_SENSITIVE - ], - 'fast_array' => ['fast_array', ['year' => 2024], ValueType::ARRAY], - 'lazy_array' => ['lazy_array', ['month' => 'October'], ValueType::ARRAY, true], - 'fast_array_sensitive' => [ - 'fast_array_sensitive', ['password' => 'pwd'], ValueType::ARRAY, false, UserConfig::FLAG_SENSITIVE - ], - 'lazy_array_sensitive' => [ - 'lazy_array_sensitive', ['password' => 'qwerty'], ValueType::ARRAY, true, UserConfig::FLAG_SENSITIVE - ], - 'fast_boolean' => ['fast_boolean', true, ValueType::BOOL], - 'fast_boolean_0' => ['fast_boolean_0', false, ValueType::BOOL], - 'lazy_boolean' => ['lazy_boolean', true, ValueType::BOOL, true], - 'lazy_boolean_0' => ['lazy_boolean_0', false, ValueType::BOOL, true], - ], - 'app2' => [ - 'key2' => ['key2', 'value2a', ValueType::STRING, false, 0, true], - 'key3' => ['key3', 'value3', ValueType::STRING, true], - 'key4' => ['key4', 'value4', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE], - 'key8' => ['key8', 11, ValueType::INT, false, 0, true], - 'key9' => ['key9', 'value9a', ValueType::STRING], - ], - 'app3' => [ - 'key1' => ['key1', 'value123'], - 'key3' => ['key3', 'value3'], - 'key8' => ['key8', 12, ValueType::INT, false, UserConfig::FLAG_SENSITIVE, true], - 'key9' => ['key9', 'value9b', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE], - 'key10' => ['key10', true, ValueType::BOOL, false, 0, true], - ], - 'only-lazy' => [ - 'key1' => ['key1', 'value456', ValueType::STRING, true, 0, true], - 'key2' => ['key2', 'value2c', ValueType::STRING, true, UserConfig::FLAG_SENSITIVE], - 'key3' => ['key3', 42, ValueType::INT, true], - 'key4' => ['key4', 17.42, ValueType::FLOAT, true], - 'key5' => ['key5', true, ValueType::BOOL, true], - ] - ], - 'user2' - => [ - 'app1' => [ - '1' => ['1', 'value1'], - '2' => ['2', 'value2', ValueType::STRING, true, UserConfig::FLAG_SENSITIVE], - '3' => ['3', 17, ValueType::INT, true], - '4' => ['4', 42, ValueType::INT, false, UserConfig::FLAG_SENSITIVE], - '5' => ['5', 17.42, ValueType::FLOAT, false], - '6' => ['6', true, ValueType::BOOL, false], - ], - 'app2' => [ - 'key2' => ['key2', 'value2b', ValueType::STRING, false, 0, true], - 'key3' => ['key3', 'value3', ValueType::STRING, true], - 'key4' => ['key4', 'value4', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE], - 'key8' => ['key8', 12, ValueType::INT, false, 0, true], - ], - 'app3' => [ - 'key10' => ['key10', false, ValueType::BOOL, false, 0, true], - ], - 'only-lazy' => [ - 'key1' => ['key1', 'value1', ValueType::STRING, true, 0, true] - ] - ], - 'user3' - => [ - 'app2' => [ - 'key2' => ['key2', 'value2c', ValueType::MIXED, false, 0, true], - 'key3' => ['key3', 'value3', ValueType::STRING, true, ], - 'key4' => ['key4', 'value4', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE], - 'fast_string_sensitive' => [ - 'fast_string_sensitive', 'fs_value', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE - ], - 'lazy_string_sensitive' => [ - 'lazy_string_sensitive', 'ls_value', ValueType::STRING, true, UserConfig::FLAG_SENSITIVE - ], - ], - 'only-lazy' => [ - 'key3' => ['key3', 'value3', ValueType::STRING, true] - ] - ], - 'user4' - => [ - 'app2' => [ - 'key1' => ['key1', 'value1'], - 'key2' => ['key2', 'value2A', ValueType::MIXED, false, 0, true], - 'key3' => ['key3', 'value3', ValueType::STRING, true,], - 'key4' => ['key4', 'value4', ValueType::STRING, false, UserConfig::FLAG_SENSITIVE], - ], - 'app3' => [ - 'key10' => ['key10', true, ValueType::BOOL, false, 0, true], - ], - 'only-lazy' => [ - 'key1' => ['key1', 123, ValueType::INT, true, 0, true] - ] - ], - 'user5' - => [ - 'app1' => [ - 'key1' => ['key1', 'value1'] - ], - 'app2' => [ - 'key8' => ['key8', 12, ValueType::INT, false, 0, true] - ], - 'only-lazy' => [ - 'key1' => ['key1', 'value1', ValueType::STRING, true, 0, true] - ] - ], - ]; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->connection = Server::get(IDBConnection::class); - $this->config = Server::get(IConfig::class); - $this->configManager = Server::get(ConfigManager::class); - $this->presetManager = Server::get(PresetManager::class); - $this->logger = Server::get(LoggerInterface::class); - $this->crypto = Server::get(ICrypto::class); - $this->dispatcher = Server::get(IEventDispatcher::class); - - // storing current preferences and emptying the data table - $sql = $this->connection->getQueryBuilder(); - $sql->select('*') - ->from('preferences'); - $result = $sql->executeQuery(); - $this->originalPreferences = $result->fetchAllAssociative(); - $result->closeCursor(); - - $sql = $this->connection->getQueryBuilder(); - $sql->delete('preferences'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('preferences') - ->values( - [ - 'userid' => $sql->createParameter('userid'), - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'type' => $sql->createParameter('type'), - 'lazy' => $sql->createParameter('lazy'), - 'flags' => $sql->createParameter('flags'), - 'indexed' => $sql->createParameter('indexed') - ] - ); - - foreach ($this->basePreferences as $userId => $userData) { - foreach ($userData as $appId => $appData) { - foreach ($appData as $key => $row) { - $value = $row[1]; - $type = ($row[2] ?? ValueType::MIXED)->value; - - if ($type === ValueType::ARRAY->value) { - $value = json_encode($value); - } - - if ($type === ValueType::BOOL->value && $value === false) { - $value = '0'; - } - - $flags = $row[4] ?? 0; - if ((UserConfig::FLAG_SENSITIVE & $flags) !== 0) { - if (!isset($this->basePreferences[$userId][$appId][$key]['encrypted'])) { - $value = self::invokePrivate(UserConfigEntry::class, 'ENCRYPTION_PREFIX') - . $this->crypto->encrypt((string)$value); - $this->basePreferences[$userId][$appId][$key]['encrypted'] = $value; - } else { - $value = $this->basePreferences[$userId][$appId][$key]['encrypted']; - } - } else { - $indexed = (($row[5] ?? false) === true) ? $value : ''; - } - - $sql->setParameters( - [ - 'userid' => $userId, - 'appid' => $appId, - 'configkey' => $row[0], - 'configvalue' => $value, - 'type' => $type, - 'lazy' => (($row[3] ?? false) === true) ? 1 : 0, - 'flags' => $flags, - 'indexed' => $indexed ?? '' - ] - )->executeStatement(); - } - } - } - } - - #[\Override] - protected function tearDown(): void { - $sql = $this->connection->getQueryBuilder(); - $sql->delete('preferences'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('preferences') - ->values( - [ - 'userid' => $sql->createParameter('userid'), - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'lazy' => $sql->createParameter('lazy'), - 'type' => $sql->createParameter('type'), - ] - ); - - foreach ($this->originalPreferences as $key => $configs) { - $sql->setParameter('userid', $configs['userid']) - ->setParameter('appid', $configs['appid']) - ->setParameter('configkey', $configs['configkey']) - ->setParameter('configvalue', $configs['configvalue']) - ->setParameter('lazy', ($configs['lazy'] === '1') ? '1' : '0') - ->setParameter('type', $configs['type']); - $sql->executeStatement(); - } - - parent::tearDown(); - } - - /** - * @param array $preLoading preload the 'fast' cache for a list of users) - * - * @return IUserConfig - */ - private function generateUserConfig(array $preLoading = []): IUserConfig { - $userConfig = new UserConfig( - $this->connection, - $this->config, - $this->configManager, - $this->presetManager, - $this->logger, - $this->crypto, - $this->dispatcher - ); - $msg = ' generateUserConfig() failed to confirm cache status'; - - // confirm cache status - $status = $userConfig->statusCache(); - $this->assertSame([], $status['fastCache'], $msg); - $this->assertSame([], $status['lazyCache'], $msg); - foreach ($preLoading as $preLoadUser) { - // simple way to initiate the load of non-lazy preferences values in cache - $userConfig->getValueString($preLoadUser, 'core', 'preload'); - - // confirm cache status - $status = $userConfig->statusCache(); - $this->assertTrue(isset($status['fastCache'][$preLoadUser]), $msg); - $this->assertFalse(isset($status['lazyCache'][$preLoadUser]), $msg); - - $apps = array_values(array_diff(array_keys($this->basePreferences[$preLoadUser]), ['only-lazy'])); - $this->assertEqualsCanonicalizing($apps, array_keys($status['fastCache'][$preLoadUser]), $msg); - $this->assertSame([], $status['lazyCache'][$preLoadUser] ?? [], $msg); - } - - return $userConfig; - } - - public function testGetUserIdsEmpty(): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing(array_keys($this->basePreferences), $userConfig->getUserIds()); - } - - public function testGetUserIds(): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing(['user1', 'user2', 'user5'], $userConfig->getUserIds('app1')); - } - - public function testGetApps(): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing( - array_keys($this->basePreferences['user1']), $userConfig->getApps('user1') - ); - } - - public function testGetKeys(): void { - $userConfig = $this->generateUserConfig(['user1']); - $this->assertEqualsCanonicalizing( - array_keys($this->basePreferences['user1']['app1']), $userConfig->getKeys('user1', 'app1') - ); - } - - public static function providerHasKey(): array { - return [ - ['user1', 'app1', 'key1', false, true], - ['user0', 'app1', 'key1', false, false], - ['user1', 'app1', 'key1', true, false], - ['user1', 'app1', 'key0', false, false], - ['user1', 'app1', 'key0', true, false], - ['user1', 'app1', 'fast_string_sensitive', false, true], - ['user1', 'app1', 'lazy_string_sensitive', true, true], - ['user2', 'only-lazy', 'key1', false, false], - ['user2', 'only-lazy', 'key1', true, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerHasKey')] - public function testHasKey(string $userId, string $appId, string $key, ?bool $lazy, bool $result): void { - $userConfig = $this->generateUserConfig(); - $this->assertEquals($result, $userConfig->hasKey($userId, $appId, $key, $lazy)); - } - - public static function providerIsSensitive(): array { - return [ - ['user1', 'app1', 'key1', false, false, false], - ['user0', 'app1', 'key1', false, false, true], - ['user1', 'app1', 'key1', true, false, true], - ['user1', 'app1', 'key1', null, false, false], - ['user1', 'app1', 'key0', false, false, true], - ['user1', 'app1', 'key0', true, false, true], - ['user1', 'app1', 'fast_string_sensitive', false, true, false], - ['user1', 'app1', 'lazy_string_sensitive', true, true, false], - ['user1', 'app1', 'fast_string_sensitive', true, true, true], - ['user1', 'app1', 'lazy_string_sensitive', false, true, true], - ['user1', 'app1', 'lazy_string_sensitive', null, true, false], - ['user2', 'only-lazy', 'key1', false, false, true], - ['user2', 'only-lazy', 'key1', true, false, false], - ['user2', 'only-lazy', 'key1', null, false, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerIsSensitive')] - public function testIsSensitive( - string $userId, - string $appId, - string $key, - ?bool $lazy, - bool $result, - bool $exception, - ): void { - $userConfig = $this->generateUserConfig(); - if ($exception) { - $this->expectException(UnknownKeyException::class); - } - - $this->assertEquals($result, $userConfig->isSensitive($userId, $appId, $key, $lazy)); - } - - public static function providerIsLazy(): array { - return [ - ['user1', 'app1', 'key1', false, false], - ['user0', 'app1', 'key1', false, true], - ['user1', 'app1', 'key0', false, true], - ['user1', 'app1', 'key0', false, true], - ['user1', 'app1', 'fast_string_sensitive', false, false], - ['user1', 'app1', 'lazy_string_sensitive', true, false], - ['user2', 'only-lazy', 'key1', true, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerIsLazy')] - public function testIsLazy( - string $userId, - string $appId, - string $key, - bool $result, - bool $exception, - ): void { - $userConfig = $this->generateUserConfig(); - if ($exception) { - $this->expectException(UnknownKeyException::class); - } - - $this->assertEquals($result, $userConfig->isLazy($userId, $appId, $key)); - } - - public static function providerGetValues(): array { - return [ - [ - 'user1', 'app1', '', true, - [ - 'fast_array' => ['year' => 2024], - 'fast_array_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_boolean' => true, - 'fast_boolean_0' => false, - 'fast_float' => 3.14, - 'fast_float_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_int' => 11, - 'fast_int_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_string' => 'f_value', - 'fast_string_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'key1' => 'value1', - 'key22' => '31', - 'lazy_array' => ['month' => 'October'], - 'lazy_array_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'lazy_boolean' => true, - 'lazy_boolean_0' => false, - 'lazy_float' => 3.14159, - 'lazy_float_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'lazy_int' => 12, - 'lazy_int_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'lazy_string' => 'l_value', - 'lazy_string_sensitive' => '***REMOVED SENSITIVE VALUE***', - ] - ], - [ - 'user1', 'app1', '', false, - [ - 'fast_array' => ['year' => 2024], - 'fast_array_sensitive' => ['password' => 'pwd'], - 'fast_boolean' => true, - 'fast_boolean_0' => false, - 'fast_float' => 3.14, - 'fast_float_sensitive' => 1.41, - 'fast_int' => 11, - 'fast_int_sensitive' => 2024, - 'fast_string' => 'f_value', - 'fast_string_sensitive' => 'fs_value', - 'key1' => 'value1', - 'key22' => '31', - 'lazy_array' => ['month' => 'October'], - 'lazy_array_sensitive' => ['password' => 'qwerty'], - 'lazy_boolean' => true, - 'lazy_boolean_0' => false, - 'lazy_float' => 3.14159, - 'lazy_float_sensitive' => 1.4142, - 'lazy_int' => 12, - 'lazy_int_sensitive' => 2048, - 'lazy_string' => 'l_value', - 'lazy_string_sensitive' => 'ls_value' - ] - ], - [ - 'user1', 'app1', 'fast_', true, - [ - 'fast_array' => ['year' => 2024], - 'fast_array_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_boolean' => true, - 'fast_boolean_0' => false, - 'fast_float' => 3.14, - 'fast_float_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_int' => 11, - 'fast_int_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'fast_string' => 'f_value', - 'fast_string_sensitive' => '***REMOVED SENSITIVE VALUE***', - ] - ], - [ - 'user1', 'app1', 'fast_', false, - [ - 'fast_array' => ['year' => 2024], - 'fast_array_sensitive' => ['password' => 'pwd'], - 'fast_boolean' => true, - 'fast_boolean_0' => false, - 'fast_float' => 3.14, - 'fast_float_sensitive' => 1.41, - 'fast_int' => 11, - 'fast_int_sensitive' => 2024, - 'fast_string' => 'f_value', - 'fast_string_sensitive' => 'fs_value', - ] - ], - [ - 'user1', 'app1', 'key1', true, - [ - 'key1' => 'value1', - ] - ], - [ - 'user2', 'app1', '', false, - [ - '1' => 'value1', - '4' => 42, - '5' => 17.42, - '6' => true, - '2' => 'value2', - '3' => 17, - ] - ], - [ - 'user2', 'app1', '', true, - [ - '1' => 'value1', - '4' => '***REMOVED SENSITIVE VALUE***', - '5' => 17.42, - '6' => true, - '2' => '***REMOVED SENSITIVE VALUE***', - '3' => 17, - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValues')] - public function testGetValues( - string $userId, - string $appId, - string $prefix, - bool $filtered, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertJsonStringEqualsJsonString( - json_encode($result), json_encode($userConfig->getValues($userId, $appId, $prefix, $filtered)) - ); - } - - public static function providerGetAllValues(): array { - return [ - [ - 'user2', false, - [ - 'app1' => [ - '1' => 'value1', - '4' => 42, - '5' => 17.42, - '6' => true, - '2' => 'value2', - '3' => 17, - ], - 'app2' => [ - 'key2' => 'value2b', - 'key3' => 'value3', - 'key4' => 'value4', - 'key8' => 12, - ], - 'app3' => [ - 'key10' => false, - ], - 'only-lazy' => [ - 'key1' => 'value1', - ] - ], - ], - [ - 'user2', true, - [ - 'app1' => [ - '1' => 'value1', - '4' => '***REMOVED SENSITIVE VALUE***', - '5' => 17.42, - '6' => true, - '2' => '***REMOVED SENSITIVE VALUE***', - '3' => 17, - ], - 'app2' => [ - 'key2' => 'value2b', - 'key3' => 'value3', - 'key4' => '***REMOVED SENSITIVE VALUE***', - 'key8' => 12, - ], - 'app3' => [ - 'key10' => false, - ], - 'only-lazy' => [ - 'key1' => 'value1', - ] - ], - ], - [ - 'user3', true, - [ - 'app2' => [ - 'key2' => 'value2c', - 'key3' => 'value3', - 'key4' => '***REMOVED SENSITIVE VALUE***', - 'fast_string_sensitive' => '***REMOVED SENSITIVE VALUE***', - 'lazy_string_sensitive' => '***REMOVED SENSITIVE VALUE***', - ], - 'only-lazy' => [ - 'key3' => 'value3', - ] - ], - ], - [ - 'user3', false, - [ - 'app2' => [ - 'key2' => 'value2c', - 'key3' => 'value3', - 'key4' => 'value4', - 'fast_string_sensitive' => 'fs_value', - 'lazy_string_sensitive' => 'ls_value', - ], - 'only-lazy' => [ - 'key3' => 'value3', - ] - ], - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAllValues')] - public function testGetAllValues( - string $userId, - bool $filtered, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing($result, $userConfig->getAllValues($userId, $filtered)); - } - - public static function providerSearchValuesByApps(): array { - return [ - [ - 'user1', 'key1', false, null, - [ - 'app1' => 'value1', - 'app3' => 'value123' - ] - ], - [ - 'user1', 'key1', true, null, - [ - 'only-lazy' => 'value456' - ] - ], - [ - 'user1', 'key8', false, null, - [ - 'app2' => 11, - 'app3' => 12, - ] - ], - [ - 'user1', 'key9', false, ValueType::INT, - [ - 'app2' => 0, - 'app3' => 0, - ] - ] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByApps')] - public function testSearchValuesByApps( - string $userId, - string $key, - bool $lazy, - ?ValueType $typedAs, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEquals($result, $userConfig->getValuesByApps($userId, $key, $lazy, $typedAs)); - } - - public static function providerSearchValuesByUsers(): array { - return [ - [ - 'app2', 'key2', null, null, - [ - 'user1' => 'value2a', - 'user2' => 'value2b', - 'user3' => 'value2c', - 'user4' => 'value2A' - ] - ], - [ - 'app2', 'key2', null, ['user1', 'user3'], - [ - 'user1' => 'value2a', - 'user3' => 'value2c', - ] - ], - [ - 'app2', 'key2', ValueType::INT, ['user1', 'user3'], - [ - 'user1' => 0, - 'user3' => 0, - ] - ], - [ - 'app2', 'key8', ValueType::INT, null, - [ - 'user1' => 11, - 'user2' => 12, - 'user5' => 12, - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByUsers')] - public function testSearchValuesByUsers( - string $app, - string $key, - ?ValueType $typedAs, - ?array $userIds, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing( - $result, $userConfig->getValuesByUsers($app, $key, $typedAs, $userIds) - ); - } - - public static function providerSearchValuesByValueString(): array { - return [ - ['app2', 'key2', 'value2a', false, ['user1']], - ['app2', 'key2', 'value2A', false, ['user4']], - ['app2', 'key2', 'value2A', true, ['user1', 'user4']] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByValueString')] - public function testSearchUsersByValueString( - string $app, - string $key, - string|array $value, - bool $ci, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing($result, iterator_to_array($userConfig->searchUsersByValueString($app, $key, $value, $ci))); - } - - public static function providerSearchValuesByValueInt(): array { - return [ - ['app3', 'key8', 12, []], // sensitive value, cannot search - ['app2', 'key8', 12, ['user2', 'user5']], // sensitive value, cannot search - ['only-lazy', 'key1', 123, ['user4']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByValueInt')] - public function testSearchUsersByValueInt( - string $app, - string $key, - int $value, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing($result, iterator_to_array($userConfig->searchUsersByValueInt($app, $key, $value))); - } - - public static function providerSearchValuesByValues(): array { - return [ - ['app2', 'key2', ['value2a', 'value2b'], ['user1', 'user2']], - ['app2', 'key2', ['value2a', 'value2c'], ['user1', 'user3']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByValues')] - public function testSearchUsersByValues( - string $app, - string $key, - array $values, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing($result, iterator_to_array($userConfig->searchUsersByValues($app, $key, $values))); - } - - public static function providerSearchValuesByValueBool(): array { - return [ - ['app3', 'key10', true, ['user1', 'user4']], - ['app3', 'key10', false, ['user2']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSearchValuesByValueBool')] - public function testSearchUsersByValueBool( - string $app, - string $key, - bool $value, - array $result, - ): void { - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing($result, iterator_to_array($userConfig->searchUsersByValueBool($app, $key, $value))); - } - - private array $getValueMixed = [ - [['user1'], 'user1', 'app1', 'key0', 'default_because_unknown_key', true, 'default_because_unknown_key'], - [['user1'], 'user1', 'app1', 'key0', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [['user1'], 'user1', 'app1', 'fast_string', 'default_because_unknown_key', false, 'f_value'], - [['user1'], 'user1', 'app1', 'fast_string', 'default_because_unknown_key', true, 'f_value'], - [['user1'], 'user1', 'app1', 'lazy_string', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [['user1'], 'user1', 'app1', 'lazy_string', 'default_because_unknown_key', true, 'l_value'], - [['user1'], 'user1', 'app1', 'fast_string_sensitive', 'default_because_unknown_key', false, 'fs_value'], - [['user1'], 'user1', 'app1', 'fast_string_sensitive', 'default_because_unknown_key', true, 'fs_value'], - [['user1'], 'user1', 'app1', 'lazy_string_sensitive', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [['user1'], 'user1', 'app1', 'lazy_string_sensitive', 'default_because_unknown_key', true, 'ls_value'], - - [null, 'user1', 'app1', 'key0', 'default_because_unknown_key', true, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'key0', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'fast_string', 'default_because_unknown_key', false, 'f_value'], - // because non-lazy are already loaded - [null, 'user1', 'app1', 'fast_string', 'default_because_unknown_key', true, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'lazy_string', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'lazy_string', 'default_because_unknown_key', true, 'l_value'], - [null, 'user1', 'app1', 'fast_string_sensitive', 'default_because_unknown_key', false, 'fs_value'], - [null, 'user1', 'app1', 'fast_string_sensitive', 'default_because_unknown_key', true, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'default_because_unknown_key', false, 'default_because_unknown_key'], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'default_because_unknown_key', true, 'ls_value'], - ]; - - public function testGetValueMixed(): void { - foreach ($this->getValueMixed as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueMixed($userId, $app, $key, $default, $lazy)); - } - } - - public function testGetValueString(): void { - foreach ($this->getValueMixed as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueString($userId, $app, $key, $default, $lazy)); - } - } - - private array $getValueInt = [ - [['user1'], 'user1', 'app1', 'key0', 54321, true, 54321], - [['user1'], 'user1', 'app1', 'key0', 54321, false, 54321], - [['user1'], 'user1', 'app1', 'fast_int', 54321, false, 11], - [['user1'], 'user1', 'app1', 'fast_int', 54321, true, 11], - [['user1'], 'user1', 'app1', 'fast_int_sensitive', 54321, false, 2024], - [['user1'], 'user1', 'app1', 'fast_int_sensitive', 54321, true, 2024], - [['user1'], 'user1', 'app1', 'lazy_int', 54321, false, 54321], - [['user1'], 'user1', 'app1', 'lazy_int', 54321, true, 12], - [['user1'], 'user1', 'app1', 'lazy_int_sensitive', 54321, false, 54321], - [['user1'], 'user1', 'app1', 'lazy_int_sensitive', 54321, true, 2048], - [null, 'user1', 'app1', 'key0', 54321, true, 54321], - [null, 'user1', 'app1', 'key0', 54321, false, 54321], - [null, 'user1', 'app1', 'key22', 54321, false, 31], - [null, 'user1', 'app1', 'fast_int', 54321, false, 11], - [null, 'user1', 'app1', 'fast_int', 54321, true, 54321], - [null, 'user1', 'app1', 'fast_int_sensitive', 54321, false, 2024], - [null, 'user1', 'app1', 'fast_int_sensitive', 54321, true, 54321], - [null, 'user1', 'app1', 'lazy_int', 54321, false, 54321], - [null, 'user1', 'app1', 'lazy_int', 54321, true, 12], - [null, 'user1', 'app1', 'lazy_int_sensitive', 54321, false, 54321], - [null, 'user1', 'app1', 'lazy_int_sensitive', 54321, true, 2048], - ]; - - public function testGetValueInt(): void { - foreach ($this->getValueInt as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueInt($userId, $app, $key, $default, $lazy)); - } - } - - private array $getValueFloat = [ - [['user1'], 'user1', 'app1', 'key0', 54.321, true, 54.321], - [null, 'user1', 'app1', 'key0', 54.321, true, 54.321], - [['user1'], 'user1', 'app1', 'key0', 54.321, false, 54.321], - [null, 'user1', 'app1', 'key0', 54.321, false, 54.321], - [['user1'], 'user1', 'app1', 'fast_float', 54.321, false, 3.14], - [null, 'user1', 'app1', 'fast_float', 54.321, false, 3.14], - [['user1'], 'user1', 'app1', 'fast_float', 54.321, true, 3.14], - [null, 'user1', 'app1', 'fast_float', 54.321, true, 54.321], - [['user1'], 'user1', 'app1', 'fast_float_sensitive', 54.321, false, 1.41], - [null, 'user1', 'app1', 'fast_float_sensitive', 54.321, false, 1.41], - [['user1'], 'user1', 'app1', 'fast_float_sensitive', 54.321, true, 1.41], - [null, 'user1', 'app1', 'fast_float_sensitive', 54.321, true, 54.321], - [['user1'], 'user1', 'app1', 'lazy_float', 54.321, false, 54.321], - [null, 'user1', 'app1', 'lazy_float', 54.321, false, 54.321], - [['user1'], 'user1', 'app1', 'lazy_float', 54.321, true, 3.14159], - [null, 'user1', 'app1', 'lazy_float', 54.321, true, 3.14159], - [['user1'], 'user1', 'app1', 'lazy_float_sensitive', 54.321, false, 54.321], - [null, 'user1', 'app1', 'lazy_float_sensitive', 54.321, false, 54.321], - [['user1'], 'user1', 'app1', 'lazy_float_sensitive', 54.321, true, 1.4142], - [null, 'user1', 'app1', 'lazy_float_sensitive', 54.321, true, 1.4142], - ]; - - public function testGetValueFloat(): void { - foreach ($this->getValueFloat as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueFloat($userId, $app, $key, $default, $lazy)); - } - } - - private array $getValueBool = [ - [['user1'], 'user1', 'app1', 'key0', false, true, false], - [null, 'user1', 'app1', 'key0', false, true, false], - [['user1'], 'user1', 'app1', 'key0', true, true, true], - [null, 'user1', 'app1', 'key0', true, true, true], - [['user1'], 'user1', 'app1', 'key0', false, false, false], - [null, 'user1', 'app1', 'key0', false, false, false], - [['user1'], 'user1', 'app1', 'key0', true, false, true], - [null, 'user1', 'app1', 'key0', true, false, true], - [['user1'], 'user1', 'app1', 'fast_boolean', false, false, true], - [null, 'user1', 'app1', 'fast_boolean', false, false, true], - [['user1'], 'user1', 'app1', 'fast_boolean_0', false, false, false], - [null, 'user1', 'app1', 'fast_boolean_0', false, false, false], - [['user1'], 'user1', 'app1', 'fast_boolean', true, false, true], - [null, 'user1', 'app1', 'fast_boolean', true, false, true], - [['user1'], 'user1', 'app1', 'fast_boolean_0', true, false, false], - [null, 'user1', 'app1', 'fast_boolean_0', true, false, false], - [['user1'], 'user1', 'app1', 'fast_boolean', false, true, true], - [null, 'user1', 'app1', 'fast_boolean', false, true, false], - [['user1'], 'user1', 'app1', 'fast_boolean_0', false, true, false], - [null, 'user1', 'app1', 'fast_boolean_0', false, true, false], - [['user1'], 'user1', 'app1', 'fast_boolean', true, true, true], - [null, 'user1', 'app1', 'fast_boolean', true, true, true], - [['user1'], 'user1', 'app1', 'fast_boolean_0', true, true, false], - [null, 'user1', 'app1', 'fast_boolean_0', true, true, true], - [['user1'], 'user1', 'app1', 'lazy_boolean', false, false, false], - [null, 'user1', 'app1', 'lazy_boolean', false, false, false], - [['user1'], 'user1', 'app1', 'lazy_boolean_0', false, false, false], - [null, 'user1', 'app1', 'lazy_boolean_0', false, false, false], - [['user1'], 'user1', 'app1', 'lazy_boolean', true, false, true], - [null, 'user1', 'app1', 'lazy_boolean', true, false, true], - [['user1'], 'user1', 'app1', 'lazy_boolean_0', true, false, true], - [null, 'user1', 'app1', 'lazy_boolean_0', true, false, true], - [['user1'], 'user1', 'app1', 'lazy_boolean', false, true, true], - [null, 'user1', 'app1', 'lazy_boolean', false, true, true], - [['user1'], 'user1', 'app1', 'lazy_boolean_0', false, true, false], - [null, 'user1', 'app1', 'lazy_boolean_0', false, true, false], - [['user1'], 'user1', 'app1', 'lazy_boolean', true, true, true], - [null, 'user1', 'app1', 'lazy_boolean', true, true, true], - [['user1'], 'user1', 'app1', 'lazy_boolean_0', true, true, false], - [null, 'user1', 'app1', 'lazy_boolean_0', true, true, false], - ]; - - public function testGetValueBool(): void { - foreach ($this->getValueBool as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueBool($userId, $app, $key, $default, $lazy)); - } - } - - private array $getValueArray = [ - [['user1'], 'user1', 'app1', 'key0', ['default_because_unknown_key'], true, ['default_because_unknown_key']], - [null, 'user1', 'app1', 'key0', ['default_because_unknown_key'], true, ['default_because_unknown_key']], - [['user1'], 'user1', 'app1', 'key0', ['default_because_unknown_key'], false, ['default_because_unknown_key']], - [null, 'user1', 'app1', 'key0', ['default_because_unknown_key'], false, ['default_because_unknown_key']], - ]; - - public function testGetValueArray(): void { - foreach ($this->getValueArray as $value) { - [$preload, $userId, $app, $key, $default, $lazy, $result] = $value; - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($result, $userConfig->getValueArray($userId, $app, $key, $default, $lazy)); - } - } - - public function testGetValueType(): void { - $getValueType = [ - [null, 'user1', 'app1', 'key1', false, ValueType::MIXED], - [null, 'user1', 'app1', 'key1', true, null, UnknownKeyException::class], - [null, 'user1', 'app1', 'fast_string', true, ValueType::STRING, UnknownKeyException::class], - [['user1'], 'user1', 'app1', 'fast_string', true, ValueType::STRING], - [null, 'user1', 'app1', 'fast_string', false, ValueType::STRING], - [null, 'user1', 'app1', 'lazy_string', true, ValueType::STRING], - [null, 'user1', 'app1', 'lazy_string', false, ValueType::STRING, UnknownKeyException::class], - [ - null, 'user1', 'app1', 'fast_string_sensitive', true, ValueType::STRING, - UnknownKeyException::class - ], - [['user1'], 'user1', 'app1', 'fast_string_sensitive', true, ValueType::STRING], - [null, 'user1', 'app1', 'fast_string_sensitive', false, ValueType::STRING], - [null, 'user1', 'app1', 'lazy_string_sensitive', true, ValueType::STRING], - [ - null, 'user1', 'app1', 'lazy_string_sensitive', false, ValueType::STRING, - UnknownKeyException::class - ], - [null, 'user1', 'app1', 'fast_int', true, ValueType::INT, UnknownKeyException::class], - [['user1'], 'user1', 'app1', 'fast_int', true, ValueType::INT], - [null, 'user1', 'app1', 'fast_int', false, ValueType::INT], - [null, 'user1', 'app1', 'lazy_int', true, ValueType::INT], - [null, 'user1', 'app1', 'lazy_int', false, ValueType::INT, UnknownKeyException::class], - [null, 'user1', 'app1', 'fast_float', true, ValueType::FLOAT, UnknownKeyException::class], - [['user1'], 'user1', 'app1', 'fast_float', true, ValueType::FLOAT], - [null, 'user1', 'app1', 'fast_float', false, ValueType::FLOAT], - [null, 'user1', 'app1', 'lazy_float', true, ValueType::FLOAT], - [null, 'user1', 'app1', 'lazy_float', false, ValueType::FLOAT, UnknownKeyException::class], - [null, 'user1', 'app1', 'fast_boolean', true, ValueType::BOOL, UnknownKeyException::class], - [['user1'], 'user1', 'app1', 'fast_boolean', true, ValueType::BOOL], - [null, 'user1', 'app1', 'fast_boolean', false, ValueType::BOOL], - [null, 'user1', 'app1', 'lazy_boolean', true, ValueType::BOOL], - [null, 'user1', 'app1', 'lazy_boolean', false, ValueType::BOOL, UnknownKeyException::class], - ]; - - foreach ($getValueType as $value) { - [$preload, $userId, $app, $key, $lazy, $result] = $value; - $exception = $value[6] ?? null; - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $type = $userConfig->getValueType($userId, $app, $key, $lazy); - if ($exception === null) { - $this->assertEquals($result->value, $type->value); - } - } - } - - public static function providerSetValueMixed(): array { - return [ - [null, 'user1', 'app1', 'key1', 'value', false, false, true], - [null, 'user1', 'app1', 'key1', '12345', true, false, true], - [null, 'user1', 'app1', 'key1', '12345', true, true, true], - [null, 'user1', 'app1', 'key1', 'value1', false, false, false], - [null, 'user1', 'app1', 'key1', 'value1', true, false, true], - [null, 'user1', 'app1', 'key1', 'value1', false, true, true], - [ - null, 'user1', 'app1', 'fast_string', 'f_value_2', false, false, true, - TypeConflictException::class - ], - [ - null, 'user1', 'app1', 'fast_string', 'f_value', true, false, true, - TypeConflictException::class - ], - [null, 'user1', 'app1', 'fast_string', 'f_value', true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_int', 'n_value', false, false, true, TypeConflictException::class], - [ - null, 'user1', 'app1', 'fast_float', 'n_value', false, false, true, - TypeConflictException::class - ], - [ - null, 'user1', 'app1', 'lazy_string', 'l_value_2', false, false, true, - TypeConflictException::class - ], - [null, 'user1', 'app1', 'lazy_string', 'l_value', true, false, false], - [null, 'user1', 'app1', 'lazy_string', 'l_value', true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_int', 'l_value', false, false, true, TypeConflictException::class], - [ - null, 'user1', 'app1', 'lazy_float', 'l_value', false, false, true, - TypeConflictException::class - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetValueMixed')] - public function testSetValueMixed( - ?array $preload, - string $userId, - string $app, - string $key, - string $value, - bool $lazy, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->setValueMixed($userId, $app, $key, $value, $lazy, ($sensitive) ? 1 : 0); - - if ($exception === null) { - $this->assertEquals($result, $edited); - } - } - - /** - * This test needs to stay! Emails are expected to be lowercase due to performance reasons. - * This way we can skip the expensive casing change on the database. - */ - public function testSetValueMixedWithSettingsEmail(): void { - $userConfig = $this->generateUserConfig(); - - $edited = $userConfig->setValueMixed('user1', 'settings', 'email', 'mixed.CASE@Nextcloud.com'); - $this->assertTrue($edited); - - $actual = $userConfig->getValueMixed('user1', 'settings', 'email'); - $this->assertEquals('mixed.case@nextcloud.com', $actual); - } - - public static function providerSetValueString(): array { - return [ - [null, 'user1', 'app1', 'key1', 'value', false, false, true], - [null, 'user1', 'app1', 'key1', '12345', true, false, true], - [null, 'user1', 'app1', 'key1', '12345', true, true, true], - [null, 'user1', 'app1', 'key1', 'value1', false, false, false], - [null, 'user1', 'app1', 'key1', 'value1', true, false, true], - [null, 'user1', 'app1', 'key1', 'value1', false, true, true], - [null, 'user1', 'app1', 'fast_string', 'f_value_2', false, false, true], - [null, 'user1', 'app1', 'fast_string', 'f_value', false, false, false], - [null, 'user1', 'app1', 'fast_string', 'f_value', true, false, true], - [null, 'user1', 'app1', 'fast_string', 'f_value', true, true, true], - [null, 'user1', 'app1', 'lazy_string', 'l_value_2', false, false, true], - [null, 'user1', 'app1', 'lazy_string', 'l_value', true, false, false], - [null, 'user1', 'app1', 'lazy_string', 'l_value', true, true, true], - [null, 'user1', 'app1', 'fast_string_sensitive', 'fs_value', false, true, false], - [null, 'user1', 'app1', 'fast_string_sensitive', 'fs_value', true, true, true], - [null, 'user1', 'app1', 'fast_string_sensitive', 'fs_value', true, false, true], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'ls_value', false, true, true], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'ls_value', true, true, false], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'ls_value', true, false, false], - [null, 'user1', 'app1', 'lazy_string_sensitive', 'ls_value_2', true, false, true], - [null, 'user1', 'app1', 'fast_int', 'n_value', false, false, true, TypeConflictException::class], - [ - null, 'user1', 'app1', 'fast_float', 'n_value', false, false, true, - TypeConflictException::class - ], - [ - null, 'user1', 'app1', 'fast_float', 'n_value', false, false, true, - TypeConflictException::class - ], - [null, 'user1', 'app1', 'lazy_int', 'n_value', false, false, true, TypeConflictException::class], - [ - null, 'user1', 'app1', 'lazy_boolean', 'n_value', false, false, true, - TypeConflictException::class - ], - [ - null, 'user1', 'app1', 'lazy_float', 'n_value', false, false, true, - TypeConflictException::class - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetValueString')] - public function testSetValueString( - ?array $preload, - string $userId, - string $app, - string $key, - string $value, - bool $lazy, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->setValueString($userId, $app, $key, $value, $lazy, ($sensitive) ? 1 : 0); - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEquals($value, $userConfig->getValueString($userId, $app, $key, $value, $lazy)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($value, $userConfig->getValueString($userId, $app, $key, $value, $lazy)); - } - } - - public static function providerSetValueInt(): array { - return [ - [null, 'user1', 'app1', 'key1', 12345, false, false, true], - [null, 'user1', 'app1', 'key1', 12345, true, false, true], - [null, 'user1', 'app1', 'key1', 12345, true, true, true], - [null, 'user1', 'app1', 'fast_int', 11, false, false, false], - [null, 'user1', 'app1', 'fast_int', 111, false, false, true], - [null, 'user1', 'app1', 'fast_int', 111, true, false, true], - [null, 'user1', 'app1', 'fast_int', 111, false, true, true], - [null, 'user1', 'app1', 'fast_int', 11, true, false, true], - [null, 'user1', 'app1', 'fast_int', 11, false, true, true], - [null, 'user1', 'app1', 'lazy_int', 12, false, false, true], - [null, 'user1', 'app1', 'lazy_int', 121, false, false, true], - [null, 'user1', 'app1', 'lazy_int', 121, true, false, true], - [null, 'user1', 'app1', 'lazy_int', 121, false, true, true], - [null, 'user1', 'app1', 'lazy_int', 12, true, false, false], - [null, 'user1', 'app1', 'lazy_int', 12, false, true, true], - [null, 'user1', 'app1', 'fast_string', 12345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12345, false, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12345, true, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12345, true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', 12345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', 12345, true, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', 12345, true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_float', 12345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_float', 12345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_boolean', 12345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_float', 12345, false, false, true, TypeConflictException::class], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetValueInt')] - public function testSetValueInt( - ?array $preload, - string $userId, - string $app, - string $key, - int $value, - bool $lazy, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->setValueInt($userId, $app, $key, $value, $lazy, ($sensitive) ? 1 : 0); - - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEquals($value, $userConfig->getValueInt($userId, $app, $key, $value, $lazy)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($value, $userConfig->getValueInt($userId, $app, $key, $value, $lazy)); - } - } - - public static function providerSetValueFloat(): array { - return [ - [null, 'user1', 'app1', 'key1', 12.345, false, false, true], - [null, 'user1', 'app1', 'key1', 12.345, true, false, true], - [null, 'user1', 'app1', 'key1', 12.345, true, true, true], - [null, 'user1', 'app1', 'fast_float', 3.14, false, false, false], - [null, 'user1', 'app1', 'fast_float', 3.15, false, false, true], - [null, 'user1', 'app1', 'fast_float', 3.15, true, false, true], - [null, 'user1', 'app1', 'fast_float', 3.15, false, true, true], - [null, 'user1', 'app1', 'fast_float', 3.14, true, false, true], - [null, 'user1', 'app1', 'fast_float', 3.14, false, true, true], - [null, 'user1', 'app1', 'lazy_float', 3.14159, false, false, true], - [null, 'user1', 'app1', 'lazy_float', 3.14158, false, false, true], - [null, 'user1', 'app1', 'lazy_float', 3.14158, true, false, true], - [null, 'user1', 'app1', 'lazy_float', 3.14158, false, true, true], - [null, 'user1', 'app1', 'lazy_float', 3.14159, true, false, false], - [null, 'user1', 'app1', 'lazy_float', 3.14159, false, true, true], - [null, 'user1', 'app1', 'fast_string', 12.345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12.345, false, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12.345, true, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', 12.345, true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', 12.345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', 12.345, true, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_array', 12.345, true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_int', 12.345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_int', 12.345, false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_boolean', 12.345, false, false, true, TypeConflictException::class], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetValueFloat')] - public function testSetValueFloat( - ?array $preload, - string $userId, - string $app, - string $key, - float $value, - bool $lazy, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->setValueFloat($userId, $app, $key, $value, $lazy, ($sensitive) ? 1 : 0); - - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEquals($value, $userConfig->getValueFloat($userId, $app, $key, $value, $lazy)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($value, $userConfig->getValueFloat($userId, $app, $key, $value, $lazy)); - } - } - - public static function providerSetValueArray(): array { - return [ - [null, 'user1', 'app1', 'key1', [], false, false, true], - [null, 'user1', 'app1', 'key1', [], true, false, true], - [null, 'user1', 'app1', 'key1', [], true, true, true], - [null, 'user1', 'app1', 'fast_array', ['year' => 2024], false, false, false], - [null, 'user1', 'app1', 'fast_array', [], false, false, true], - [null, 'user1', 'app1', 'fast_array', [], true, false, true], - [null, 'user1', 'app1', 'fast_array', [], false, true, true], - [null, 'user1', 'app1', 'fast_array', ['year' => 2024], true, false, true], - [null, 'user1', 'app1', 'fast_array', ['year' => 2024], false, true, true], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'October'], false, false, true], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'September'], false, false, true], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'September'], true, false, true], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'September'], false, true, true], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'October'], true, false, false], - [null, 'user1', 'app1', 'lazy_array', ['month' => 'October'], false, true, true], - [null, 'user1', 'app1', 'fast_string', [], false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', [], false, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', [], true, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_string', [], true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', [], false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', [], true, false, false, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_string', [], true, true, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_int', [], false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'fast_int', [], false, false, true, TypeConflictException::class], - [null, 'user1', 'app1', 'lazy_boolean', [], false, false, true, TypeConflictException::class], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerSetValueArray')] - public function testSetValueArray( - ?array $preload, - string $userId, - string $app, - string $key, - array $value, - bool $lazy, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->setValueArray($userId, $app, $key, $value, $lazy, ($sensitive) ? 1 : 0); - - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEqualsCanonicalizing( - $value, $userConfig->getValueArray($userId, $app, $key, $value, $lazy) - ); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEqualsCanonicalizing( - $value, $userConfig->getValueArray($userId, $app, $key, $value, $lazy) - ); - } - } - - public static function providerUpdateSensitive(): array { - return [ - [null, 'user1', 'app1', 'key1', false, false], - [['user1'], 'user1', 'app1', 'key1', false, false], - [null, 'user1', 'app1', 'key1', true, true], - [['user1'], 'user1', 'app1', 'key1', true, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerUpdateSensitive')] - public function testUpdateSensitive( - ?array $preload, - string $userId, - string $app, - string $key, - bool $sensitive, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->updateSensitive($userId, $app, $key, $sensitive); - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key)); - if ($sensitive) { - $statusCache = $userConfig->statusCache(); - $this->assertEquals( - true, - str_starts_with( - ($statusCache['fastCache'][$userId][$app][$key] - ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), - '$UserConfigEncryption$') - ); - } - } - } - - public static function providerUpdateGlobalSensitive(): array { - return [[true], [false]]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerUpdateGlobalSensitive')] - public function testUpdateGlobalSensitive(bool $sensitive): void { - $userConfig = $this->generateUserConfig($preload ?? []); - $app = 'app2'; - if ($sensitive) { - $key = 'key2'; - $value = 'value2a'; - } else { - $key = 'key4'; - $value = 'value4'; - } - - $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key)); - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $userConfig->getValueString($userId, $app, $key); // cache loading for userId - $statusCache = $userConfig->statusCache(); - $this->assertEquals( - !$sensitive, str_starts_with( - ($statusCache['fastCache'][$userId][$app][$key] - ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), - '$UserConfigEncryption$' - ) - ); - } - - $userConfig->updateGlobalSensitive($app, $key, $sensitive); - - $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key)); - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key)); - $statusCache = $userConfig->statusCache(); - // should only work if updateGlobalSensitive drop cache - $this->assertEquals($sensitive, str_starts_with( - ($statusCache['fastCache'][$userId][$app][$key] - ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), - '$UserConfigEncryption$') - ); - } - } - - public static function providerUpdateLazy(): array { - return [ - [null, 'user1', 'app1', 'key1', false, false], - [['user1'], 'user1', 'app1', 'key1', false, false], - [null, 'user1', 'app1', 'key1', true, true], - [['user1'], 'user1', 'app1', 'key1', true, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerUpdateLazy')] - public function testUpdateLazy( - ?array $preload, - string $userId, - string $app, - string $key, - bool $lazy, - bool $result, - ?string $exception = null, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - if ($exception !== null) { - $this->expectException($exception); - } - - $edited = $userConfig->updateLazy($userId, $app, $key, $lazy); - if ($exception !== null) { - return; - } - - $this->assertEquals($result, $edited); - if ($result) { - $this->assertEquals($lazy, $userConfig->isLazy($userId, $app, $key)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals($lazy, $userConfig->isLazy($userId, $app, $key)); - } - } - - public static function providerUpdateGlobalLazy(): array { - return [[true], [false]]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerUpdateGlobalLazy')] - public function testUpdateGlobalLazy(bool $lazy): void { - $userConfig = $this->generateUserConfig($preload ?? []); - $app = 'app2'; - if ($lazy) { - $key = 'key4'; - $value = 'value4'; - } else { - $key = 'key3'; - $value = 'value3'; - } - - $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key, '', !$lazy)); - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $this->assertEquals(!$lazy, $userConfig->isLazy($userId, $app, $key)); - } - - $userConfig->updateGlobalLazy($app, $key, $lazy); - $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key, '', $lazy)); - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $this->assertEquals($lazy, $userConfig->isLazy($userId, $app, $key)); - } - } - - public static function providerGetDetails(): array { - return [ - [ - 'user3', 'app2', 'key2', - [ - 'userId' => 'user3', - 'app' => 'app2', - 'key' => 'key2', - 'value' => 'value2c', - 'type' => 0, - 'lazy' => false, - 'typeString' => 'mixed', - 'sensitive' => false - ] - ], - [ - 'user1', 'app1', 'lazy_int', - [ - 'userId' => 'user1', - 'app' => 'app1', - 'key' => 'lazy_int', - 'value' => 12, - 'type' => 2, - 'lazy' => true, - 'typeString' => 'int', - 'sensitive' => false - ] - ], - [ - 'user1', 'app1', 'fast_float_sensitive', - [ - 'userId' => 'user1', - 'app' => 'app1', - 'key' => 'fast_float_sensitive', - 'value' => 1.41, - 'type' => 3, - 'lazy' => false, - 'typeString' => 'float', - 'sensitive' => true - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetDetails')] - public function testGetDetails(string $userId, string $app, string $key, array $result): void { - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEqualsCanonicalizing($result, $userConfig->getDetails($userId, $app, $key)); - } - - public static function providerDeletePreference(): array { - return [ - [null, 'user1', 'app1', 'key22'], - [['user1'], 'user1', 'app1', 'fast_string_sensitive'], - [null, 'user1', 'app1', 'lazy_array_sensitive'], - [['user2'], 'user1', 'app1', 'lazy_array_sensitive'], - [null, 'user2', 'only-lazy', 'key1'], - [['user2'], 'user2', 'only-lazy', 'key1'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerDeletePreference')] - public function testDeletePreference( - ?array $preload, - string $userId, - string $app, - string $key, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - $lazy = $userConfig->isLazy($userId, $app, $key); - - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals(true, $userConfig->hasKey($userId, $app, $key, $lazy)); - $userConfig->deleteUserConfig($userId, $app, $key); - $this->assertEquals(false, $userConfig->hasKey($userId, $app, $key, $lazy)); - $userConfig = $this->generateUserConfig($preload ?? []); - $this->assertEquals(false, $userConfig->hasKey($userId, $app, $key, $lazy)); - } - - public static function providerDeleteKey(): array { - return [ - [null, 'app2', 'key3'], - [['user1'], 'app2', 'key3'], - [null, 'only-lazy', 'key1'], - [['user2'], 'only-lazy', 'key1'], - [null, 'app2', 'lazy_string_sensitive'], - [['user3', 'user1'], 'app2', 'lazy_string_sensitive'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerDeleteKey')] - public function testDeleteKey( - ?array $preload, - string $app, - string $key, - ): void { - $userConfig = $this->generateUserConfig($preload ?? []); - $userConfig->deleteKey($app, $key); - - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $this->assertEquals(false, $userConfig->hasKey($userId, $app, $key, null)); - $userConfigTemp = $this->generateUserConfig($preload ?? []); - $this->assertEquals(false, $userConfigTemp->hasKey($userId, $app, $key, null)); - } - } - - public function testDeleteApp(): void { - $userConfig = $this->generateUserConfig(); - $userConfig->deleteApp('only-lazy'); - - foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { - $this->assertEquals(false, in_array('only-lazy', $userConfig->getApps($userId))); - $userConfigTemp = $this->generateUserConfig(); - $this->assertEquals(false, in_array('only-lazy', $userConfigTemp->getApps($userId))); - } - } - - public function testDeleteAllPreferences(): void { - $userConfig = $this->generateUserConfig(); - $userConfig->deleteAllUserConfig('user1'); - - $this->assertEqualsCanonicalizing([], $userConfig->getApps('user1')); - $userConfig = $this->generateUserConfig(); - $this->assertEqualsCanonicalizing([], $userConfig->getApps('user1')); - } - - public function testClearCache(): void { - $userConfig = $this->generateUserConfig(['user1', 'user2']); - $userConfig->clearCache('user1'); - $status = $userConfig->statusCache(); - $this->assertTrue(isset($status['fastCache']['user2'])); - $this->assertFalse(isset($status['fastCache']['user1'])); - $this->assertEquals('value2a', $userConfig->getValueString('user1', 'app2', 'key2')); - $status = $userConfig->statusCache(); - $this->assertFalse(isset($status['lazyCache']['user1'])); - $this->assertTrue(isset($status['fastCache']['user1'])); - } - - public function testClearCacheAll(): void { - $userConfig = $this->generateUserConfig(['user1', 'user2']); - $userConfig->clearCacheAll(); - $this->assertEqualsCanonicalizing( - [ - 'fastCache' => [], - 'lazyCache' => [], - ], - $userConfig->statusCache() - ); - } -} --- a/tests/lib/ConfigTest.php +++ /dev/null @@ -1,193 +0,0 @@ -"bar", "beers" => array("Appenzeller", "Guinness", "Kölsch"), "alcohol_free" => false);'; - - /** @var array */ - private $initialConfig = ['foo' => 'bar', 'beers' => ['Appenzeller', 'Guinness', 'Kölsch'], 'alcohol_free' => false]; - /** @var string */ - private $configFile; - /** @var string */ - private $randomTmpDir; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->randomTmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->configFile = $this->randomTmpDir . 'testconfig.php'; - file_put_contents($this->configFile, self::TESTCONTENT); - } - - #[\Override] - protected function tearDown(): void { - unlink($this->configFile); - parent::tearDown(); - } - - private function getConfig(): Config { - return new Config($this->randomTmpDir, 'testconfig.php'); - } - - public function testGetKeys(): void { - $expectedConfig = ['foo', 'beers', 'alcohol_free']; - $this->assertSame($expectedConfig, $this->getConfig()->getKeys()); - } - - public function testGetKeysReturnsEnvironmentKeysIfSet() { - $expectedConfig = ['foo', 'beers', 'alcohol_free', 'taste']; - putenv('NC_taste=great'); - $this->assertSame($expectedConfig, $this->getConfig()->getKeys()); - putenv('NC_taste'); - } - - public function testGetValue(): void { - $config = $this->getConfig(); - $this->assertSame('bar', $config->getValue('foo')); - $this->assertSame(null, $config->getValue('bar')); - $this->assertSame('moo', $config->getValue('bar', 'moo')); - $this->assertSame(false, $config->getValue('alcohol_free', 'someBogusValue')); - $this->assertSame(['Appenzeller', 'Guinness', 'Kölsch'], $config->getValue('beers', 'someBogusValue')); - $this->assertSame(['Appenzeller', 'Guinness', 'Kölsch'], $config->getValue('beers')); - } - - public function testGetValueReturnsEnvironmentValueIfSet(): void { - $config = $this->getConfig(); - $this->assertEquals('bar', $config->getValue('foo')); - - putenv('NC_foo=baz'); - $config = $this->getConfig(); - $this->assertEquals('baz', $config->getValue('foo')); - putenv('NC_foo'); // unset the env variable - } - - public function testGetValueReturnsEnvironmentValueIfSetToZero(): void { - $config = $this->getConfig(); - $this->assertEquals('bar', $config->getValue('foo')); - - putenv('NC_foo=0'); - $config = $this->getConfig(); - $this->assertEquals('0', $config->getValue('foo')); - putenv('NC_foo'); // unset the env variable - } - - public function testGetValueReturnsEnvironmentValueIfSetToFalse(): void { - $config = $this->getConfig(); - $this->assertEquals('bar', $config->getValue('foo')); - - putenv('NC_foo=false'); - $config = $this->getConfig(); - $this->assertEquals('false', $config->getValue('foo')); - putenv('NC_foo'); // unset the env variable - } - - public function testSetValue(): void { - $config = $this->getConfig(); - $config->setValue('foo', 'moo'); - $this->assertSame('moo', $config->getValue('foo')); - - $content = file_get_contents($this->configFile); - $expected = " 'moo',\n 'beers' => \n array (\n 0 => 'Appenzeller',\n " - . " 1 => 'Guinness',\n 2 => 'Kölsch',\n ),\n 'alcohol_free' => false,\n);\n"; - $this->assertEquals($expected, $content); - - $config->setValue('bar', 'red'); - $config->setValue('apps', ['files', 'gallery']); - $this->assertSame('red', $config->getValue('bar')); - $this->assertSame(['files', 'gallery'], $config->getValue('apps')); - - $content = file_get_contents($this->configFile); - - $expected = " 'moo',\n 'beers' => \n array (\n 0 => 'Appenzeller',\n " - . " 1 => 'Guinness',\n 2 => 'Kölsch',\n ),\n 'alcohol_free' => false,\n 'bar' => 'red',\n 'apps' => \n " - . " array (\n 0 => 'files',\n 1 => 'gallery',\n ),\n);\n"; - $this->assertEquals($expected, $content); - } - - public function testSetValues(): void { - $config = $this->getConfig(); - $content = file_get_contents($this->configFile); - $this->assertEquals(self::TESTCONTENT, $content); - - // Changing configs to existing values and deleting non-existing once - // should not rewrite the config.php - $config->setValues([ - 'foo' => 'bar', - 'not_exists' => null, - ]); - - $this->assertSame('bar', $config->getValue('foo')); - $this->assertSame(null, $config->getValue('not_exists')); - $content = file_get_contents($this->configFile); - $this->assertEquals(self::TESTCONTENT, $content); - - $config->setValues([ - 'foo' => 'moo', - 'alcohol_free' => null, - ]); - $this->assertSame('moo', $config->getValue('foo')); - $this->assertSame(null, $config->getValue('not_exists')); - - $content = file_get_contents($this->configFile); - $expected = " 'moo',\n 'beers' => \n array (\n 0 => 'Appenzeller',\n " - . " 1 => 'Guinness',\n 2 => 'Kölsch',\n ),\n);\n"; - $this->assertEquals($expected, $content); - } - - public function testDeleteKey(): void { - $config = $this->getConfig(); - $config->deleteKey('foo'); - $this->assertSame('this_was_clearly_not_set_before', $config->getValue('foo', 'this_was_clearly_not_set_before')); - $content = file_get_contents($this->configFile); - - $expected = " \n array (\n 0 => 'Appenzeller',\n " - . " 1 => 'Guinness',\n 2 => 'Kölsch',\n ),\n 'alcohol_free' => false,\n);\n"; - $this->assertEquals($expected, $content); - } - - public function testConfigMerge(): void { - // Create additional config - $additionalConfig = '"totallyOutdated");'; - $additionalConfigPath = $this->randomTmpDir . 'additionalConfig.testconfig.php'; - file_put_contents($additionalConfigPath, $additionalConfig); - - // Reinstantiate the config to force a read-in of the additional configs - $config = new Config($this->randomTmpDir, 'testconfig.php'); - - // Ensure that the config value can be read and the config has not been modified - $this->assertSame('totallyOutdated', $config->getValue('php53', 'bogusValue')); - $this->assertEquals(self::TESTCONTENT, file_get_contents($this->configFile)); - - // Write a new value to the config - $config->setValue('CoolWebsites', ['demo.owncloud.org', 'owncloud.org', 'owncloud.com']); - $expected = " 'bar',\n 'beers' => \n array (\n 0 => 'Appenzeller',\n " - . " 1 => 'Guinness',\n 2 => 'Kölsch',\n ),\n 'alcohol_free' => false,\n 'php53' => 'totallyOutdated',\n 'CoolWebsites' => \n array (\n " - . " 0 => 'demo.owncloud.org',\n 1 => 'owncloud.org',\n 2 => 'owncloud.com',\n ),\n);\n"; - $this->assertEquals($expected, file_get_contents($this->configFile)); - - // Cleanup - unlink($additionalConfigPath); - } -} --- a/tests/lib/Contacts/ContactsMenu/ActionFactoryTest.php +++ /dev/null @@ -1,48 +0,0 @@ -actionFactory = new ActionFactory(); - } - - public function testNewLinkAction(): void { - $icon = 'icon-test'; - $name = 'Test'; - $href = 'some/url'; - - $action = $this->actionFactory->newLinkAction($icon, $name, $href); - - $this->assertInstanceOf(IAction::class, $action); - $this->assertEquals($name, $action->getName()); - $this->assertEquals(10, $action->getPriority()); - } - - public function testNewEMailAction(): void { - $icon = 'icon-test'; - $name = 'Test'; - $href = 'user@example.com'; - - $action = $this->actionFactory->newEMailAction($icon, $name, $href); - - $this->assertInstanceOf(IAction::class, $action); - $this->assertEquals($name, $action->getName()); - $this->assertEquals(10, $action->getPriority()); - $this->assertEquals('mailto:user@example.com', $action->getHref()); - } -} --- a/tests/lib/Contacts/ContactsMenu/ActionProviderStoreTest.php +++ /dev/null @@ -1,120 +0,0 @@ -serverContainer = $this->createMock(ContainerInterface::class); - $this->appManager = $this->createMock(AppManager::class); - $logger = $this->createMock(LoggerInterface::class); - - $this->actionProviderStore = new ActionProviderStore($this->serverContainer, $this->appManager, $logger); - } - - public function testGetProviders(): void { - $user = $this->createMock(IUser::class); - $provider1 = $this->createMock(ProfileProvider::class); - $provider2 = $this->createMock(LocalTimeProvider::class); - $provider3 = $this->createMock(EMailProvider::class); - $provider4 = $this->createMock(IProvider::class); - - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($user) - ->willReturn(['contacts']); - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('contacts') - ->willReturn([ - 'contactsmenu' => [ - 'OCA\Contacts\Provider1', - ], - ]); - $this->serverContainer->expects($this->exactly(4)) - ->method('get') - ->willReturnMap([ - [ProfileProvider::class, $provider1], - [LocalTimeProvider::class, $provider2], - [EMailProvider::class, $provider3], - ['OCA\Contacts\Provider1', $provider4] - ]); - - $providers = $this->actionProviderStore->getProviders($user); - - $this->assertCount(4, $providers); - $this->assertInstanceOf(ProfileProvider::class, $providers[0]); - $this->assertInstanceOf(LocalTimeProvider::class, $providers[1]); - $this->assertInstanceOf(EMailProvider::class, $providers[2]); - } - - public function testGetProvidersOfAppWithIncompleInfo(): void { - $user = $this->createMock(IUser::class); - $provider1 = $this->createMock(ProfileProvider::class); - $provider2 = $this->createMock(LocalTimeProvider::class); - $provider3 = $this->createMock(EMailProvider::class); - - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($user) - ->willReturn(['contacts']); - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('contacts') - ->willReturn([/* Empty info.xml */]); - $this->serverContainer->expects($this->exactly(3)) - ->method('get') - ->willReturnMap([ - [ProfileProvider::class, $provider1], - [LocalTimeProvider::class, $provider2], - [EMailProvider::class, $provider3], - ]); - - $providers = $this->actionProviderStore->getProviders($user); - - $this->assertCount(3, $providers); - $this->assertInstanceOf(ProfileProvider::class, $providers[0]); - $this->assertInstanceOf(LocalTimeProvider::class, $providers[1]); - $this->assertInstanceOf(EMailProvider::class, $providers[2]); - } - - public function testGetProvidersWithQueryException(): void { - $this->expectException(\Exception::class); - - $user = $this->createMock(IUser::class); - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') - ->with($user) - ->willReturn([]); - $this->serverContainer->expects($this->once()) - ->method('get') - ->willThrowException(new QueryException()); - - $this->actionProviderStore->getProviders($user); - } -} --- a/tests/lib/Contacts/ContactsMenu/Actions/LinkActionTest.php +++ /dev/null @@ -1,91 +0,0 @@ -action = new LinkAction(); - } - - public function testSetIcon(): void { - $icon = 'icon-test'; - - $this->action->setIcon($icon); - $json = $this->action->jsonSerialize(); - - $this->assertArrayHasKey('icon', $json); - $this->assertEquals($json['icon'], $icon); - } - - public function testGetSetName(): void { - $name = 'Jane Doe'; - - $this->assertEmpty($this->action->getName()); - $this->action->setName($name); - $this->assertEquals($name, $this->action->getName()); - } - - public function testGetSetPriority(): void { - $prio = 50; - - $this->assertEquals(10, $this->action->getPriority()); - $this->action->setPriority($prio); - $this->assertEquals($prio, $this->action->getPriority()); - } - - public function testSetHref(): void { - $this->action->setHref('/some/url'); - - $json = $this->action->jsonSerialize(); - $this->assertArrayHasKey('hyperlink', $json); - $this->assertEquals('/some/url', $json['hyperlink']); - } - - public function testJsonSerialize(): void { - $this->action->setIcon('icon-contacts'); - $this->action->setName('Nickie Works'); - $this->action->setPriority(33); - $this->action->setHref('example.com'); - $this->action->setAppId('contacts'); - $expected = [ - 'title' => 'Nickie Works', - 'icon' => 'icon-contacts', - 'hyperlink' => 'example.com', - 'appId' => 'contacts', - ]; - - $json = $this->action->jsonSerialize(); - - $this->assertEquals($expected, $json); - } - - public function testJsonSerializeNoAppName(): void { - $this->action->setIcon('icon-contacts'); - $this->action->setName('Nickie Works'); - $this->action->setPriority(33); - $this->action->setHref('example.com'); - $expected = [ - 'title' => 'Nickie Works', - 'icon' => 'icon-contacts', - 'hyperlink' => 'example.com', - 'appId' => '', - ]; - - $json = $this->action->jsonSerialize(); - - $this->assertEquals($expected, $json); - } -} --- a/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php +++ /dev/null @@ -1,1163 +0,0 @@ -contactsManager = $this->createMock(IManager::class); - $this->statusService = $this->createMock(StatusService::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->profileManager = $this->createMock(ProfileManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->config = $this->createMock(IConfig::class); - $this->knownUserService = $this->createMock(KnownUserService::class); - $this->l10nFactory = $this->createMock(IL10NFactory::class); - $this->contactsStore = new ContactsStore( - $this->contactsManager, - $this->statusService, - $this->config, - $this->profileManager, - $this->userManager, - $this->urlGenerator, - $this->groupManager, - $this->knownUserService, - $this->l10nFactory, - new ShareDisableChecker($this->config, $this->userManager, $this->groupManager), - ); - } - - public function testGetContactsWithoutFilter(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 123, - ], - [ - 'UID' => 567, - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - ], - ]); - $user->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user123'); - - $entries = $this->contactsStore->getContacts($user, ''); - - $this->assertCount(2, $entries); - $this->assertEquals([ - 'darren@roner.au' - ], $entries[1]->getEMailAddresses()); - } - - public function testGetContactsHidesOwnEntry(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user123', - ], - [ - 'UID' => 567, - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - ], - ]); - $user->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user123'); - - $entries = $this->contactsStore->getContacts($user, ''); - - $this->assertCount(1, $entries); - } - - public function testGetContactsWithoutBinaryImage(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->urlGenerator->expects($this->any()) - ->method('linkToRouteAbsolute') - ->with('core.GuestAvatar.getAvatar', $this->anything()) - ->willReturn('https://urlToNcAvatar.test'); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 123, - ], - [ - 'UID' => 567, - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - 'PHOTO' => base64_encode('photophotophoto'), - ], - ]); - $user->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user123'); - - $entries = $this->contactsStore->getContacts($user, ''); - - $this->assertCount(2, $entries); - $this->assertSame('https://urlToNcAvatar.test', $entries[1]->getAvatar()); - } - - public function testGetContactsWithoutAvatarURI(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 123, - ], - [ - 'UID' => 567, - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - 'PHOTO' => 'VALUE=uri:https://photo', - ], - ]); - $user->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user123'); - - $entries = $this->contactsStore->getContacts($user, ''); - - $this->assertCount(2, $entries); - $this->assertEquals('https://photo', $entries[1]->getAvatar()); - } - - public static function dataGetContactsWhenUserIsInExcludeGroups(): array { - return [ - ['yes', '[]', [], ['user123', 'user12345']], - ['yes', '["excludedGroup1"]', [], ['user123', 'user12345']], - ['yes', '["excludedGroup1"]', ['anotherGroup1'], ['user123', 'user12345']], - ['yes', '["excludedGroup1"]', ['anotherGroup1', 'anotherGroup2', 'anotherGroup3'], ['user123', 'user12345']], - ['yes', '["excludedGroup1"]', ['excludedGroup1'], []], - ['yes', '["excludedGroup1"]', ['anotherGroup1', 'excludedGroup1'], []], - ['yes', '["excludedGroup1"]', ['excludedGroup1', 'anotherGroup1', 'anotherGroup2', 'anotherGroup3'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', [], ['user123', 'user12345']], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['anotherGroup1'], ['user123', 'user12345']], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['anotherGroup1', 'anotherGroup2', 'anotherGroup3'], ['user123', 'user12345']], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['excludedGroup1'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['excludedGroup2'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['excludedGroup3'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['excludedGroup1', 'excludedGroup2', 'excludedGroup3'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['anotherGroup1', 'excludedGroup1'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['anotherGroup1', 'excludedGroup2', 'anotherGroup2', 'anotherGroup3'], []], - ['yes', '["excludedGroup1", "excludedGroup2", "excludedGroup3"]', ['excludedGroup3', 'anotherGroup1', 'anotherGroup2', 'anotherGroup3'], []], - ['allow', '[]', [], []], - ['allow', '["allowedGroup1"]', [], []], - ['allow', '["allowedGroup1"]', ['anotherGroup1'], []], - ['allow', '["allowedGroup1"]', ['anotherGroup1', 'anotherGroup2', 'anotherGroup3'], []], - ['allow', '["allowedGroup1"]', ['allowedGroup1'], ['user123', 'user12345']], - ['allow', '["allowedGroup1"]', ['anotherGroup1', 'allowedGroup1'], ['user123', 'user12345']], - ['allow', '["allowedGroup1"]', ['allowedGroup1', 'anotherGroup1', 'anotherGroup2', 'anotherGroup3'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', [], []], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['anotherGroup1'], []], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['anotherGroup1', 'anotherGroup2', 'anotherGroup3'], []], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['allowedGroup1'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['allowedGroup2'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['allowedGroup3'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['allowedGroup1', 'allowedGroup2', 'allowedGroup3'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['anotherGroup1', 'allowedGroup1'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['anotherGroup1', 'allowedGroup2', 'anotherGroup2', 'anotherGroup3'], ['user123', 'user12345']], - ['allow', '["allowedGroup1", "allowedGroup2", "allowedGroup3"]', ['allowedGroup3', 'anotherGroup1', 'anotherGroup2', 'anotherGroup3'], ['user123', 'user12345']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetContactsWhenUserIsInExcludeGroups')] - public function testGetContactsWhenUserIsInExcludeGroups(string $excludeGroups, string $excludeGroupsList, array $currentUserGroupIds, array $expectedUids): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], - ['core', 'shareapi_exclude_groups', 'no', $excludeGroups], - ['core', 'shareapi_only_share_with_group_members', 'no', 'no'], - ['core', 'shareapi_exclude_groups_list', '', $excludeGroupsList], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $this->userManager->method('get') - ->with('user001') - ->willReturn($currentUser); - - $this->groupManager->expects($this->exactly(2)) - ->method('getUserGroupIds') - ->with($this->equalTo($currentUser)) - ->willReturn($currentUserGroupIds); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user123', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user12345', - 'isLocalSystemBook' => true - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(count($expectedUids), $entries); - for ($i = 0; $i < count($expectedUids); $i++) { - $this->assertEquals($expectedUids[$i], $entries[$i]->getProperty('UID')); - } - } - - public function testGetContactsOnlyShareIfInTheSameGroupWhenUserIsInExcludeGroups(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], - ['core', 'shareapi_exclude_groups', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_exclude_groups_list', '', '["group1", "group5", "group6"]'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $this->userManager->method('get') - ->with('user001') - ->willReturn($currentUser); - - $this->groupManager->expects($this->exactly(2)) - ->method('getUserGroupIds') - ->with($this->equalTo($currentUser)) - ->willReturn(['group1', 'group2', 'group3']); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user123', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user12345', - 'isLocalSystemBook' => true - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(0, $entries); - } - - public function testGetContactsOnlyShareIfInTheSameGroup(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $user3 = $this->createMock(IUser::class); - - $calls = [ - [[$currentUser], ['group1', 'group2', 'group3']], - [[$user1], ['group1']], - [[$user2], ['group2', 'group3']], - [[$user3], ['group8', 'group9']], - ]; - $this->groupManager->expects($this->exactly(4)) - ->method('getUserGroupIds') - ->willReturnCallback(function () use (&$calls): array { - $expected = array_shift($calls); - $this->assertEquals($expected[0], func_get_args()); - return $expected[1]; - }); - - $this->userManager->expects($this->exactly(3)) - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ['user3', $user3], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(3, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('contact', $entries[2]->getProperty('UID')); - } - - public function testGetContactsOnlyEnumerateIfInTheSameGroup(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $user3 = $this->createMock(IUser::class); - - $calls = [ - [[$currentUser], ['group1', 'group2', 'group3']], - [[$user1], ['group1']], - [[$user2], ['group2', 'group3']], - [[$user3], ['group8', 'group9']], - ]; - $this->groupManager->expects($this->exactly(4)) - ->method('getUserGroupIds') - ->willReturnCallback(function () use (&$calls): array { - $expected = array_shift($calls); - $this->assertEquals($expected[0], func_get_args()); - return $expected[1]; - }); - - $this->userManager->expects($this->exactly(3)) - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ['user3', $user3], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(3, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('contact', $entries[2]->getProperty('UID')); - } - - public function testGetContactsOnlyEnumerateIfPhoneBookMatch(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'no'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($this->equalTo($currentUser)) - ->willReturn(['group1', 'group2', 'group3']); - - $this->knownUserService->method('isKnownToUser') - ->willReturnMap([ - ['user001', 'user1', true], - ['user001', 'user2', true], - ['user001', 'user3', false], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(3, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('contact', $entries[2]->getProperty('UID')); - } - - public function testGetContactsOnlyEnumerateIfPhoneBookMatchWithOwnGroupsOnly(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $user3 = $this->createMock(IUser::class); - - $calls = [ - [[$currentUser], ['group1', 'group2', 'group3']], - [[$user1], ['group1']], - [[$user2], ['group2', 'group3']], - [[$user3], ['group8', 'group9']], - ]; - $this->groupManager->expects($this->exactly(4)) - ->method('getUserGroupIds') - ->willReturnCallback(function () use (&$calls): array { - $expected = array_shift($calls); - $this->assertEquals($expected[0], func_get_args()); - return $expected[1]; - }); - - $this->userManager->expects($this->exactly(3)) - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ['user3', $user3], - ]); - - $this->knownUserService->method('isKnownToUser') - ->willReturnMap([ - ['user001', 'user1', true], - ['user001', 'user2', true], - ['user001', 'user3', true], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(3, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('contact', $entries[2]->getProperty('UID')); - } - - public function testGetContactsOnlyEnumerateIfPhoneBookOrSameGroup(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'no'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $user1 = $this->createMock(IUser::class); - - $calls = [ - [[$currentUser], ['group1', 'group2', 'group3']], - [[$user1], ['group1']], - ]; - $this->groupManager->expects($this->exactly(2)) - ->method('getUserGroupIds') - ->willReturnCallback(function () use (&$calls): array { - $expected = array_shift($calls); - $this->assertEquals($expected[0], func_get_args()); - return $expected[1]; - }); - - $this->userManager->expects($this->once()) - ->method('get') - ->with('user1') - ->willReturn($user1); - - $this->knownUserService->method('isKnownToUser') - ->willReturnMap([ - ['user001', 'user1', false], - ['user001', 'user2', true], - ['user001', 'user3', true], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(4, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('user3', $entries[2]->getProperty('UID')); - $this->assertEquals('contact', $entries[3]->getProperty('UID')); - } - - public function testGetContactsOnlyEnumerateIfPhoneBookOrSameGroupInOwnGroupsOnly(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], - ['core', 'shareapi_exclude_groups', 'no', 'no'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - /** @var IUser|MockObject $currentUser */ - $currentUser = $this->createMock(IUser::class); - $currentUser->expects($this->exactly(3)) - ->method('getUID') - ->willReturn('user001'); - - $user1 = $this->createMock(IUser::class); - $user2 = $this->createMock(IUser::class); - $user3 = $this->createMock(IUser::class); - - $calls = [ - [[$currentUser], ['group1', 'group2', 'group3']], - [[$user1], ['group1']], - [[$user2], ['group2', 'group3']], - [[$user3], ['group8', 'group9']], - ]; - $this->groupManager->expects($this->exactly(4)) - ->method('getUserGroupIds') - ->willReturnCallback(function () use (&$calls): array { - $expected = array_shift($calls); - $this->assertEquals($expected[0], func_get_args()); - return $expected[1]; - }); - - $this->userManager->expects($this->exactly(3)) - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ['user3', $user3], - ]); - - $this->knownUserService->method('isKnownToUser') - ->willReturnMap([ - ['user001', 'user1', false], - ['user001', 'user2', true], - ['user001', 'user3', true], - ]); - - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo(''), $this->equalTo(['FN', 'EMAIL'])) - ->willReturn([ - [ - 'UID' => 'user1', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user2', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'user3', - 'isLocalSystemBook' => true - ], - [ - 'UID' => 'contact', - ], - ]); - - $entries = $this->contactsStore->getContacts($currentUser, ''); - - $this->assertCount(3, $entries); - $this->assertEquals('user1', $entries[0]->getProperty('UID')); - $this->assertEquals('user2', $entries[1]->getProperty('UID')); - $this->assertEquals('contact', $entries[2]->getProperty('UID')); - } - - public function testGetContactsWithFilter(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'], - ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'], - ]); - - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturn([ - [ - 'UID' => 'a567', - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au', - ], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'john', - 'FN' => 'John Doe', - 'EMAIL' => [ - 'john@example.com', - ], - 'isLocalSystemBook' => true, - ], - [ - 'FN' => 'Anne D', - 'EMAIL' => [ - 'anne@example.com', - ], - 'isLocalSystemBook' => false, - ], - ]); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('user123'); - - // Complete match on UID should match - $entry = $this->contactsStore->getContacts($user, 'a567'); - $this->assertSame(2, count($entry)); - $this->assertEquals([ - 'darren@roner.au' - ], $entry[0]->getEMailAddresses()); - - // Partial match on UID should not match - $entry = $this->contactsStore->getContacts($user, 'a56'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Complete match on email should match - $entry = $this->contactsStore->getContacts($user, 'john@example.com'); - $this->assertSame(2, count($entry)); - $this->assertEquals([ - 'john@example.com' - ], $entry[0]->getEMailAddresses()); - $this->assertEquals([ - 'anne@example.com' - ], $entry[1]->getEMailAddresses()); - - // Partial match on email should not match - $entry = $this->contactsStore->getContacts($user, 'john@example.co'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Match on FN should not match - $entry = $this->contactsStore->getContacts($user, 'Darren Roner'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Don't filter users in local addressbook - $entry = $this->contactsStore->getContacts($user, 'Anne D'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - } - - public function testGetContactsWithFilterWithoutFullMatch(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'], - ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'no'], - ]); - - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->any()) - ->method('search') - ->willReturn([ - [ - 'UID' => 'a567', - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au', - ], - 'isLocalSystemBook' => true, - ], - [ - 'UID' => 'john', - 'FN' => 'John Doe', - 'EMAIL' => [ - 'john@example.com', - ], - 'isLocalSystemBook' => true, - ], - [ - 'FN' => 'Anne D', - 'EMAIL' => [ - 'anne@example.com', - ], - 'isLocalSystemBook' => false, - ], - ]); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('user123'); - - // Complete match on UID should not match - $entry = $this->contactsStore->getContacts($user, 'a567'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Partial match on UID should not match - $entry = $this->contactsStore->getContacts($user, 'a56'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Complete match on email should not match - $entry = $this->contactsStore->getContacts($user, 'john@example.com'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Partial match on email should not match - $entry = $this->contactsStore->getContacts($user, 'john@example.co'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Match on FN should not match - $entry = $this->contactsStore->getContacts($user, 'Darren Roner'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - - // Don't filter users in local addressbook - $entry = $this->contactsStore->getContacts($user, 'Anne D'); - $this->assertSame(1, count($entry)); - $this->assertEquals([ - 'anne@example.com' - ], $entry[0]->getEMailAddresses()); - } - - public function testFindOneUser(): void { - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], - ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], - ['core', 'shareapi_restrict_user_enumeration_full_match', 'yes', 'yes'], - ['core', 'shareapi_exclude_groups', 'no', 'yes'], - ['core', 'shareapi_exclude_groups_list', '', ''], - ['core', 'shareapi_only_share_with_group_members', 'no', 'no'], - ]); - - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo('a567'), $this->equalTo(['UID'])) - ->willReturn([ - [ - 'UID' => 123, - 'isLocalSystemBook' => false - ], - [ - 'UID' => 'a567', - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - 'isLocalSystemBook' => true - ], - ]); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('user123'); - - $entry = $this->contactsStore->findOne($user, 0, 'a567'); - - $this->assertEquals([ - 'darren@roner.au' - ], $entry->getEMailAddresses()); - } - - public function testFindOneEMail(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo('darren@roner.au'), $this->equalTo(['EMAIL'])) - ->willReturn([ - [ - 'UID' => 123, - 'isLocalSystemBook' => false - ], - [ - 'UID' => 'a567', - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au' - ], - 'isLocalSystemBook' => false - ], - ]); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('user123'); - - $entry = $this->contactsStore->findOne($user, 4, 'darren@roner.au'); - - $this->assertEquals([ - 'darren@roner.au' - ], $entry->getEMailAddresses()); - } - - public function testFindOneNotSupportedType(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - - $entry = $this->contactsStore->findOne($user, 42, 'darren@roner.au'); - - $this->assertEquals(null, $entry); - } - - public function testFindOneNoMatches(): void { - /** @var IUser|MockObject $user */ - $user = $this->createMock(IUser::class); - $this->contactsManager->expects($this->once()) - ->method('search') - ->with($this->equalTo('a567'), $this->equalTo(['UID'])) - ->willReturn([ - [ - 'UID' => 123, - 'isLocalSystemBook' => false - ], - [ - 'UID' => 'a567', - 'FN' => 'Darren Roner', - 'EMAIL' => [ - 'darren@roner.au123' - ], - 'isLocalSystemBook' => false - ], - ]); - $user->expects($this->never()) - ->method('getUID'); - - $entry = $this->contactsStore->findOne($user, 0, 'a567'); - - $this->assertEquals(null, $entry); - } - - public function testGetRecentStatusFirst(): void { - $user = $this->createMock(IUser::class); - $status1 = new UserStatus(); - $status1->setUserId('user1'); - $status2 = new UserStatus(); - $status2->setUserId('user2'); - $this->statusService->expects(self::once()) - ->method('findAllRecentStatusChanges') - ->willReturn([ - $status1, - $status2, - ]); - $user1 = $this->createMock(IUser::class); - $user1->method('getCloudId')->willReturn('user1@localcloud'); - $user2 = $this->createMock(IUser::class); - $user2->method('getCloudId')->willReturn('user2@localcloud'); - $this->userManager->expects(self::exactly(2)) - ->method('get') - ->willReturnCallback(function ($uid) use ($user1, $user2) { - return match ($uid) { - 'user1' => $user1, - 'user2' => $user2, - }; - }); - $this->contactsManager - ->expects(self::exactly(3)) - ->method('search') - ->willReturnCallback(function ($uid, $searchProps, $options) { - return match ([$uid, $options['limit'] ?? null]) { - ['user1@localcloud', 1] => [ - [ - 'UID' => 'user1', - 'URI' => 'user1.vcf', - ], - ], - ['user2@localcloud' => [], 1], // Simulate not found - ['', 4] => [ - [ - 'UID' => 'contact1', - 'URI' => 'contact1.vcf', - ], - [ - 'UID' => 'contact2', - 'URI' => 'contact2.vcf', - ], - ], - default => [], - }; - }); - - $contacts = $this->contactsStore->getContacts( - $user, - null, - 5, - ); - - self::assertCount(3, $contacts); - self::assertEquals('user1', $contacts[0]->getProperty('UID')); - self::assertEquals('contact1', $contacts[1]->getProperty('UID')); - self::assertEquals('contact2', $contacts[2]->getProperty('UID')); - } - - public function testPaginateRecentStatus(): void { - $user = $this->createMock(IUser::class); - $status1 = new UserStatus(); - $status1->setUserId('user1'); - $status2 = new UserStatus(); - $status2->setUserId('user2'); - $status3 = new UserStatus(); - $status3->setUserId('user3'); - $this->statusService->expects(self::never()) - ->method('findAllRecentStatusChanges'); - $this->contactsManager - ->expects(self::exactly(2)) - ->method('search') - ->willReturnCallback(function ($uid, $searchProps, $options) { - return match ([$uid, $options['limit'] ?? null, $options['offset'] ?? null]) { - ['', 2, 0] => [ - [ - 'UID' => 'contact1', - 'URI' => 'contact1.vcf', - ], - [ - 'UID' => 'contact2', - 'URI' => 'contact2.vcf', - ], - ], - ['', 2, 3] => [ - [ - 'UID' => 'contact3', - 'URI' => 'contact3.vcf', - ], - ], - default => [], - }; - }); - - $page1 = $this->contactsStore->getContacts( - $user, - null, - 2, - 0, - ); - $page2 = $this->contactsStore->getContacts( - $user, - null, - 2, - 3, - ); - - self::assertCount(2, $page1); - self::assertCount(1, $page2); - } -} --- a/tests/lib/Contacts/ContactsMenu/EntryTest.php +++ /dev/null @@ -1,105 +0,0 @@ -entry = new Entry(); - } - - public function testSetId(): void { - $this->entry->setId(123); - $this->addToAssertionCount(1); - } - - public function testSetGetFullName(): void { - $fn = 'Danette Chaille'; - $this->assertEquals('', $this->entry->getFullName()); - $this->entry->setFullName($fn); - $this->assertEquals($fn, $this->entry->getFullName()); - } - - public function testAddGetEMailAddresses(): void { - $this->assertEmpty($this->entry->getEMailAddresses()); - $this->entry->addEMailAddress('user@example.com'); - $this->assertEquals(['user@example.com'], $this->entry->getEMailAddresses()); - } - - public function testAddAndSortAction(): void { - // Three actions, two with equal priority - $action1 = new LinkAction(); - $action2 = new LinkAction(); - $action3 = new LinkAction(); - $action1->setPriority(10); - $action1->setName('Bravo'); - - $action2->setPriority(0); - $action2->setName('Batman'); - - $action3->setPriority(10); - $action3->setName('Alfa'); - - $this->entry->addAction($action1); - $this->entry->addAction($action2); - $this->entry->addAction($action3); - $sorted = $this->entry->getActions(); - - $this->assertSame($action3, $sorted[0]); - $this->assertSame($action1, $sorted[1]); - $this->assertSame($action2, $sorted[2]); - } - - public function testSetGetProperties(): void { - $props = [ - 'prop1' => 123, - 'prop2' => 'string', - ]; - - $this->entry->setProperties($props); - - $this->assertNull($this->entry->getProperty('doesntexist')); - $this->assertEquals(123, $this->entry->getProperty('prop1')); - $this->assertEquals('string', $this->entry->getProperty('prop2')); - } - - public function testJsonSerialize(): void { - $expectedJson = [ - 'id' => '123', - 'fullName' => 'Guadalupe Frisbey', - 'topAction' => null, - 'actions' => [], - 'lastMessage' => '', - 'avatar' => null, - 'emailAddresses' => ['user@example.com'], - 'profileTitle' => null, - 'profileUrl' => null, - 'status' => null, - 'statusMessage' => null, - 'statusMessageTimestamp' => null, - 'statusIcon' => null, - 'isUser' => false, - 'uid' => null, - ]; - - $this->entry->setId(123); - $this->entry->setFullName('Guadalupe Frisbey'); - $this->entry->addEMailAddress('user@example.com'); - $json = $this->entry->jsonSerialize(); - - $this->assertEquals($expectedJson, $json); - } -} --- a/tests/lib/Contacts/ContactsMenu/ManagerTest.php +++ /dev/null @@ -1,202 +0,0 @@ -contactsStore = $this->createMock(ContactsStore::class); - $this->actionProviderStore = $this->createMock(ActionProviderStore::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->config = $this->createMock(IConfig::class); - - $this->manager = new Manager($this->contactsStore, $this->actionProviderStore, $this->appManager, $this->config); - } - - private function generateTestEntries(): array { - $entries = []; - foreach (range('Z', 'A') as $char) { - $entry = $this->createMock(Entry::class); - $entry->expects($this->any()) - ->method('getFullName') - ->willReturn('Contact ' . $char); - $entries[] = $entry; - } - return $entries; - } - - public function testGetFilteredEntries(): void { - $filter = 'con'; - $user = $this->createMock(IUser::class); - $entries = $this->generateTestEntries(); - $provider = $this->createMock(IProvider::class); - - $this->config->expects($this->exactly(2)) - ->method('getSystemValueInt') - ->willReturnMap([ - ['sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT, 25], - ['sharing.minSearchStringLength', 0, 0], - ]); - $this->contactsStore->expects($this->once()) - ->method('getContacts') - ->with($user, $filter) - ->willReturn($entries); - $this->actionProviderStore->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([$provider]); - $provider->expects($this->exactly(25)) - ->method('process'); - $this->appManager->expects($this->once()) - ->method('isEnabledForUser') - ->with($this->equalTo('contacts'), $user) - ->willReturn(false); - $expected = [ - 'contacts' => array_slice($entries, 0, 25), - 'contactsAppEnabled' => false, - ]; - - $data = $this->manager->getEntries($user, $filter); - - $this->assertEquals($expected, $data); - } - - public function testGetFilteredEntriesLimit(): void { - $filter = 'con'; - $user = $this->createMock(IUser::class); - $entries = $this->generateTestEntries(); - $provider = $this->createMock(IProvider::class); - - $this->config->expects($this->exactly(2)) - ->method('getSystemValueInt') - ->willReturnMap([ - ['sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT, 3], - ['sharing.minSearchStringLength', 0, 0], - ]); - $this->contactsStore->expects($this->once()) - ->method('getContacts') - ->with($user, $filter) - ->willReturn($entries); - $this->actionProviderStore->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([$provider]); - $provider->expects($this->exactly(3)) - ->method('process'); - $this->appManager->expects($this->once()) - ->method('isEnabledForUser') - ->with($this->equalTo('contacts'), $user) - ->willReturn(false); - $expected = [ - 'contacts' => array_slice($entries, 0, 3), - 'contactsAppEnabled' => false, - ]; - - $data = $this->manager->getEntries($user, $filter); - - $this->assertEquals($expected, $data); - } - - public function testGetFilteredEntriesMinSearchStringLength(): void { - $filter = 'con'; - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - - $this->config->expects($this->exactly(2)) - ->method('getSystemValueInt') - ->willReturnMap([ - ['sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT, 3], - ['sharing.minSearchStringLength', 0, 4], - ]); - $this->appManager->expects($this->once()) - ->method('isEnabledForUser') - ->with($this->equalTo('contacts'), $user) - ->willReturn(false); - $expected = [ - 'contacts' => [], - 'contactsAppEnabled' => false, - ]; - - $data = $this->manager->getEntries($user, $filter); - - $this->assertEquals($expected, $data); - } - - public function testFindOne(): void { - $shareTypeFilter = 42; - $shareWithFilter = 'foobar'; - - $user = $this->createMock(IUser::class); - $entry = current($this->generateTestEntries()); - $provider = $this->createMock(IProvider::class); - $this->contactsStore->expects($this->once()) - ->method('findOne') - ->with($user, $shareTypeFilter, $shareWithFilter) - ->willReturn($entry); - $this->actionProviderStore->expects($this->once()) - ->method('getProviders') - ->with($user) - ->willReturn([$provider]); - $provider->expects($this->once()) - ->method('process'); - - $data = $this->manager->findOne($user, $shareTypeFilter, $shareWithFilter); - - $this->assertEquals($entry, $data); - } - - public function testFindOne404(): void { - $shareTypeFilter = 42; - $shareWithFilter = 'foobar'; - - $user = $this->createMock(IUser::class); - $provider = $this->createMock(IProvider::class); - $this->contactsStore->expects($this->once()) - ->method('findOne') - ->with($user, $shareTypeFilter, $shareWithFilter) - ->willReturn(null); - $this->actionProviderStore->expects($this->never()) - ->method('getProviders') - ->with($user) - ->willReturn([$provider]); - $provider->expects($this->never()) - ->method('process'); - - $data = $this->manager->findOne($user, $shareTypeFilter, $shareWithFilter); - - $this->assertEquals(null, $data); - } -} --- a/tests/lib/Contacts/ContactsMenu/Providers/EMailproviderTest.php +++ /dev/null @@ -1,86 +0,0 @@ -actionFactory = $this->createMock(IActionFactory::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - - $this->provider = new EMailProvider($this->actionFactory, $this->urlGenerator); - } - - public function testProcess(): void { - $entry = $this->createMock(IEntry::class); - $action = $this->createMock(ILinkAction::class); - $iconUrl = 'https://example.com/img/actions/icon.svg'; - $this->urlGenerator->expects($this->once()) - ->method('imagePath') - ->willReturn('img/actions/icon.svg'); - $this->urlGenerator->expects($this->once()) - ->method('getAbsoluteURL') - ->with('img/actions/icon.svg') - ->willReturn($iconUrl); - $entry->expects($this->once()) - ->method('getEMailAddresses') - ->willReturn([ - 'user@example.com', - ]); - $this->actionFactory->expects($this->once()) - ->method('newEMailAction') - ->with($this->equalTo($iconUrl), $this->equalTo('user@example.com'), $this->equalTo('user@example.com')) - ->willReturn($action); - $entry->expects($this->once()) - ->method('addAction') - ->with($action); - - $this->provider->process($entry); - } - - public function testProcessEmptyAddress(): void { - $entry = $this->createMock(IEntry::class); - $iconUrl = 'https://example.com/img/actions/icon.svg'; - $this->urlGenerator->expects($this->once()) - ->method('imagePath') - ->willReturn('img/actions/icon.svg'); - $this->urlGenerator->expects($this->once()) - ->method('getAbsoluteURL') - ->with('img/actions/icon.svg') - ->willReturn($iconUrl); - $entry->expects($this->once()) - ->method('getEMailAddresses') - ->willReturn([ - '', - ]); - $this->actionFactory->expects($this->never()) - ->method('newEMailAction'); - $entry->expects($this->never()) - ->method('addAction'); - - $this->provider->process($entry); - } -} --- a/tests/lib/Contacts/ContactsMenu/Providers/LocalTimeProviderTest.php +++ /dev/null @@ -1,194 +0,0 @@ -actionFactory = $this->createMock(IActionFactory::class); - $this->l10nFactory = $this->createMock(IL10NFactory::class); - $this->l = $this->createMock(IL10N::class); - $this->l->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - $this->l->expects($this->any()) - ->method('n') - ->willReturnCallback(function ($text, $textPlural, $n, $parameters = []) { - $formatted = str_replace('%n', (string)$n, $n === 1 ? $text : $textPlural); - return vsprintf($formatted, $parameters); - }); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->dateTimeFormatter = $this->createMock(IDateTimeFormatter::class); - $this->config = $this->createMock(IConfig::class); - $this->userSession = $this->createMock(IUserSession::class); - - $this->provider = new LocalTimeProvider( - $this->actionFactory, - $this->l10nFactory, - $this->urlGenerator, - $this->userManager, - $this->timeFactory, - $this->dateTimeFormatter, - $this->config, - $this->userSession, - ); - } - - public static function dataTestProcess(): array { - return [ - 'no current user' => [ - false, - null, - null, - 'Local time: 10:24', - ], - 'both UTC' => [ - true, - null, - null, - '10:24 • same time', - ], - 'both same time zone' => [ - true, - 'Europe/Berlin', - 'Europe/Berlin', - '11:24 • same time', - ], - '1h behind' => [ - true, - 'Europe/Berlin', - 'Europe/London', - '10:24 • 1h behind', - ], - '4:45h ahead' => [ - true, - 'Europe/Berlin', - 'Asia/Kathmandu', - '16:09 • 4h45m ahead', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestProcess')] - public function testProcess(bool $hasCurrentUser, ?string $currentUserTZ, ?string $targetUserTZ, string $expected): void { - $entry = $this->createMock(IEntry::class); - $entry->expects($this->once()) - ->method('getProperty') - ->with('UID') - ->willReturn('user1'); - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('user1'); - $this->userManager->expects($this->once()) - ->method('get') - ->with('user1') - ->willReturn($user); - - $this->l10nFactory->method('get') - ->with('lib') - ->willReturn($this->l); - - $this->config->method('getSystemValueString') - ->with('default_timezone', 'UTC') - ->willReturn('UTC'); - $this->config - ->method('getUserValue') - ->willReturnMap([ - ['user1', 'core', 'timezone', '', $targetUserTZ], - ['currentUser', 'core', 'timezone', '', $currentUserTZ], - ]); - - if ($hasCurrentUser) { - $currentUser = $this->createMock(IUser::class); - $currentUser->method('getUID') - ->willReturn('currentUser'); - $this->userSession->method('getUser') - ->willReturn($currentUser); - } - - $this->timeFactory->method('getDateTime') - ->willReturnCallback(fn ($time, $tz) => (new \DateTime('2023-01-04 10:24:43', new \DateTimeZone('UTC')))->setTimezone($tz)); - - $this->dateTimeFormatter->method('formatTime') - ->willReturnCallback(fn (\DateTime $time) => $time->format('H:i')); - - $this->urlGenerator->method('imagePath') - ->willReturn('actions/recent.svg'); - $this->urlGenerator->method('getAbsoluteURL') - ->with('actions/recent.svg') - ->willReturn('https://localhost/actions/recent.svg'); - - $action = $this->createMock(ILinkAction::class); - $this->actionFactory->expects($this->once()) - ->method('newLinkAction') - ->with( - 'https://localhost/actions/recent.svg', - $expected, - '#', - 'timezone' - ) - ->willReturn($action); - - $entry->expects($this->once()) - ->method('addAction') - ->with($action); - - $this->provider->process($entry); - } - - public function testProcessNoUser(): void { - $entry = $this->createMock(IEntry::class); - $entry->expects($this->once()) - ->method('getProperty') - ->with('UID') - ->willReturn('user1'); - - $entry->expects($this->never()) - ->method('addAction'); - - $this->provider->process($entry); - } -} --- a/tests/lib/ContactsManagerTest.php +++ /dev/null @@ -1,314 +0,0 @@ -cm = new ContactsManager(); - } - - public static function searchProvider(): array { - $search1 = [ - 0 => [ - 'N' => [0 => '', 1 => 'Jan', 2 => 'Jansen', 3 => '', 4 => '',], - 'UID' => '04ada7f5-01f9-4309-9c82-6b555b2170ed', - 'FN' => 'Jan Jansen', - 'id' => '1', - 'addressbook-key' => 'simple:1', - ], - 0 => [ - 'N' => [0 => '', 1 => 'Tom', 2 => 'Peeters', 3 => '', 4 => '',], - 'UID' => '04ada7f5-01f9-4309-9c82-2345-2345--6b555b2170ed', - 'FN' => 'Tom Peeters', - 'id' => '2', - 'addressbook-key' => 'simple:1', - ], - ]; - - $search2 = [ - 0 => [ - 'N' => [0 => '', 1 => 'fg', 2 => '', 3 => '', 4 => '',], - 'UID' => '04ada234h5jh357f5-01f9-4309-9c82-6b555b2170ed', - 'FN' => 'Jan Rompuy', - 'id' => '1', - 'addressbook-key' => 'simple:2', - ], - 0 => [ - 'N' => [0 => '', 1 => 'fg', 2 => '', 3 => '', 4 => '',], - 'UID' => '04ada7f5-01f9-4309-345kj345j9c82-2345-2345--6b555b2170ed', - 'FN' => 'Tim Peeters', - 'id' => '2', - 'addressbook-key' => 'simple:2', - ], - ]; - - $expectedResult = array_merge($search1, $search2); - return [ - [ - $search1, - $search2, - $expectedResult - ] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')] - public function testSearch($search1, $search2, $expectedResult): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBook $addressbook */ - $addressbook1 = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook1->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - - $addressbook1->expects($this->once()) - ->method('search') - ->willReturn($search1); - - $addressbook1->expects($this->any()) - ->method('getKey') - ->willReturn('simple:1'); - - $addressbook2 = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook2->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - - $addressbook2->expects($this->once()) - ->method('search') - ->willReturn($search2); - - $addressbook2->expects($this->any()) - ->method('getKey') - ->willReturn('simple:2'); - - $this->cm->registerAddressBook($addressbook1); - $this->cm->registerAddressBook($addressbook2); - $result = $this->cm->search(''); - $this->assertEquals($expectedResult, $result); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')] - public function testSearchDisabledAb($search1): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook1 = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook1->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - - $addressbook1->expects($this->once()) - ->method('search') - ->willReturn($search1); - - $addressbook1->expects($this->any()) - ->method('getKey') - ->willReturn('simple:1'); - - $addressbook2 = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook2->expects($this->once()) - ->method('isEnabled') - ->willReturn(false); - - $addressbook2->expects($this->never()) - ->method('search'); - - $this->cm->registerAddressBook($addressbook1); - $this->cm->registerAddressBook($addressbook2); - $result = $this->cm->search(''); - $this->assertEquals($search1, $result); - } - - public function testDeleteHavePermission(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->any()) - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - - $addressbook->expects($this->once()) - ->method('delete') - ->willReturn('returnMe'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->delete(1, $addressbook->getKey()); - $this->assertEquals($result, 'returnMe'); - } - - public function testDeleteNoPermission(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->any()) - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_READ); - - $addressbook->expects($this->never()) - ->method('delete'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->delete(1, $addressbook->getKey()); - $this->assertEquals($result, null); - } - - public function testDeleteNoAddressbook(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->never()) - ->method('delete'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->delete(1, 'noaddressbook'); - $this->assertEquals($result, null); - } - - public function testCreateOrUpdateHavePermission(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->any()) - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - - $addressbook->expects($this->once()) - ->method('createOrUpdate') - ->willReturn('returnMe'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->createOrUpdate([], $addressbook->getKey()); - $this->assertEquals($result, 'returnMe'); - } - - public function testCreateOrUpdateNoPermission(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->any()) - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_READ); - - $addressbook->expects($this->never()) - ->method('createOrUpdate'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->createOrUpdate([], $addressbook->getKey()); - $this->assertEquals($result, null); - } - - public function testCreateOrUpdateNOAdressbook(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->never()) - ->method('createOrUpdate'); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->createOrUpdate([], 'noaddressbook'); - $this->assertEquals($result, null); - } - - public function testIsEnabledIfNot(): void { - $result = $this->cm->isEnabled(); - $this->assertFalse($result); - } - - public function testIsEnabledIfSo(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('addressbookKey'); - - $this->cm->registerAddressBook($addressbook); - $result = $this->cm->isEnabled(); - $this->assertTrue($result); - } - - public function testAddressBookEnumeration(): void { - // create mock for the addressbook - /** @var \PHPUnit\Framework\MockObject\MockObject|IAddressBookEnabled $addressbook */ - $addressbook = $this->getMockBuilder('\OCP\IAddressBookEnabled') - ->disableOriginalConstructor() - ->getMock(); - - // setup return for method calls - $addressbook->expects($this->any()) - ->method('getKey') - ->willReturn('SIMPLE_ADDRESS_BOOK'); - $addressbook->expects($this->any()) - ->method('getDisplayName') - ->willReturn('A very simple Addressbook'); - - // register the address book - $this->cm->registerAddressBook($addressbook); - $all_books = $this->cm->getUserAddressBooks(); - - $this->assertEquals(1, count($all_books)); - $this->assertEquals($addressbook, $all_books['SIMPLE_ADDRESS_BOOK']); - } -} --- a/tests/lib/DB/AdapterTest.php +++ /dev/null @@ -1,211 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->appId = substr(uniqid('test_db_adapter', true), 0, 32); - } - - #[\Override] - public function tearDown(): void { - $qb = $this->connection->getQueryBuilder(); - - $qb->delete('appconfig') - ->from('appconfig') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter($this->appId))) - ->executeStatement(); - parent::tearDown(); - } - - public function testInsertIgnoreOnConflictDuplicate(): void { - $configKey = uniqid('key', true); - $expected = [ - [ - 'configkey' => $configKey, - 'configvalue' => '1', - ] - ]; - $result = $this->connection->insertIgnoreConflict('appconfig', [ - 'appid' => $this->appId, - 'configkey' => $configKey, - 'configvalue' => '1', - ]); - $this->assertEquals(1, $result); - $rows = $this->getRows($configKey); - $this->assertSame($expected, $rows); - - $result = $this->connection->insertIgnoreConflict('appconfig', [ - 'appid' => $this->appId, - 'configkey' => $configKey, - 'configvalue' => '2', - ]); - $this->assertEquals(0, $result); - $rows = $this->getRows($configKey); - $this->assertSame($expected, $rows); - } - - private function getRows(string $configKey): array { - $qb = $this->connection->getQueryBuilder(); - return $qb->select(['configkey', 'configvalue']) - ->from('appconfig') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter($this->appId))) - ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($configKey))) - ->executeQuery() - ->fetchAllAssociative(); - } - - public function fetchAssociative(): void { - $insert = $this->connection->getQueryBuilder(); - $insert->insert('appconfig') - ->values([ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ]) - ->executeStatement(); - - // fetch all associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - - $rows = $result->fetchAllAssociative(); - $this->assertEquals([ - [ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ] - ], $rows); - - // fetch associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $row = $result->fetchAssociative(); - $this->assertEquals([ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ], $row); - - // iterate associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $row = iterator_to_array($result->iterateAssociative()); - $this->assertEquals([ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ], $row); - } - - public function fetchNumeric(): void { - $insert = $this->connection->getQueryBuilder(); - $insert->insert('appconfig') - ->values([ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ]) - ->executeStatement(); - - // fetch all associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - - $rows = $result->fetchAllNumeric(); - $this->assertEquals([ - [ - 0 => $this->appId, - 1 => 'test', - 2 => '1', - ] - ], $rows); - - // fetch associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $row = $result->fetchNumeric(); - $this->assertEquals([ - 0 => $this->appId, - 1 => 'test', - 2 => '1', - ], $row); - - // iterate associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $row = iterator_to_array($result->iterateNumeric()); - $this->assertEquals([ - 0 => $this->appId, - 1 => 'test', - 2 => '1', - ], $row); - } - - public function fetchOne(): void { - $insert = $this->connection->getQueryBuilder(); - $insert->insert('appconfig') - ->values([ - 'appid' => $this->appId, - 'configkey' => 'test', - 'configvalue' => '1', - ]) - ->executeStatement(); - - // fetch all associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - - $rows = $result->fetchFirstColumn(); - $this->assertEquals($this->appId, $rows); - - // fetch associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $row = $result->fetchFirstColumn(); - $this->assertEquals($this->appId, $row); - - // iterate associative - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['configkey', 'configvalue', 'appid']) - ->from('appconfig') - ->executeQuery(); - $rows = iterator_to_array($result->iterateNumeric()); - $this->assertEquals([$this->appId], $rows); - } -} --- a/tests/lib/DB/ConnectionFactoryTest.php +++ /dev/null @@ -1,127 +0,0 @@ - '127.0.0.1']], - ['db.example.org', ['host' => 'db.example.org']], - ['unix', ['host' => 'unix']], - ['[::1]', ['host' => '[::1]']], - ['127.0.0.1:3306', ['host' => '127.0.0.1', 'port' => 3306]], - ['db.example.org:3306', ['host' => 'db.example.org', 'port' => 3306]], - ['unix:3306', ['host' => 'unix', 'port' => 3306]], - ['[::1]:3306', ['host' => '[::1]', 'port' => 3306]], - ['unix:/socket', ['host' => 'unix', 'unix_socket' => '/socket']], - ]; - } - - /** - * @param string $host - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('splitHostFromPortAndSocketData')] - public function testSplitHostFromPortAndSocket($host, array $expected): void { - /** @var SystemConfig $config */ - $config = $this->createMock(SystemConfig::class); - $cacheFactory = $this->createMock(ICacheFactory::class); - $factory = new ConnectionFactory($config, $cacheFactory); - - $this->assertEquals($expected, self::invokePrivate($factory, 'splitHostFromPortAndSocket', [$host])); - } - - /** - * The numeric value of a PDO MySQL attribute, e.g. `SSL_CA`. - * - * The values are not stable across PHP versions, so they must never be hardcoded. - * Since PHP 8.5 the `PDO::MYSQL_ATTR_*` constants are deprecated in favor of - * `Pdo\Mysql::ATTR_*`, and either only exists with the MySQL driver installed. - */ - private function mysqlAttribute(string $name): int { - if (!extension_loaded('pdo_mysql')) { - $this->markTestSkipped('The pdo_mysql extension is required to resolve the PDO attribute values'); - } - if (PHP_VERSION_ID >= 80500 && class_exists(\Pdo\Mysql::class)) { - return (int)constant('Pdo\Mysql::ATTR_' . $name); - } - return (int)constant('PDO::MYSQL_ATTR_' . $name); - } - - public function testMysqlSslConnection(): void { - /** @var SystemConfig|\PHPUnit\Framework\MockObject\MockObject $config */ - $config = $this->createMock(SystemConfig::class); - $config->method('getValue') - ->willReturnCallback(function ($key, $default) { - return match ($key) { - 'dbdriveroptions' => [ - $this->mysqlAttribute('SSL_CA') => 'rootCA.crt', - $this->mysqlAttribute('SSL_CERT') => 'client.crt', - $this->mysqlAttribute('SSL_KEY') => 'client.key', - $this->mysqlAttribute('SSL_VERIFY_SERVER_CERT') => true, - ], - 'dbtype' => 'mysql', - default => $default, - }; - }); - $factory = new ConnectionFactory($config); - - $params = $factory->createConnectionParams(); - - $this->assertEquals('pdo_mysql', $params['driver']); - $this->assertEquals([ - $this->mysqlAttribute('FOUND_ROWS') => true, - $this->mysqlAttribute('SSL_CA') => 'rootCA.crt', - $this->mysqlAttribute('SSL_CERT') => 'client.crt', - $this->mysqlAttribute('SSL_KEY') => 'client.key', - $this->mysqlAttribute('SSL_VERIFY_SERVER_CERT') => true, - ], $params['driverOptions']); - } - - public function testPgsqlSslConnection(): void { - /** @var SystemConfig|\PHPUnit\Framework\MockObject\MockObject $config */ - $config = $this->createMock(SystemConfig::class); - $config->method('getValue') - ->willReturnCallback(function ($key, $default) { - return match ($key) { - 'dbtype' => 'pgsql', - 'dbdriveroptions' => [ - 1 => 'foo', - 3 => 'bar', - ], - 'pgsql_ssl' => [ - 'mode' => 'verify-full', - 'cert' => 'client.crt', - 'key' => 'client.key', - 'crl' => 'client.crl', - 'rootcert' => 'rootCA.crt', - ], - default => $default, - }; - }); - $factory = new ConnectionFactory($config); - - $params = $factory->createConnectionParams(); - - $this->assertEquals('pdo_pgsql', $params['driver']); - $this->assertEquals('verify-full', $params['sslmode']); - $this->assertEquals('rootCA.crt', $params['sslrootcert']); - $this->assertEquals('client.crt', $params['sslcert']); - $this->assertEquals('client.key', $params['sslkey']); - $this->assertEquals('client.crl', $params['sslcrl']); - $this->assertEquals([ - 1 => 'foo', - 3 => 'bar', - ], $params['driverOptions']); - } -} --- a/tests/lib/DB/ConnectionTest.php +++ /dev/null @@ -1,154 +0,0 @@ - 'test', - 'password' => 'topsecret', - 'host' => 'test', - ]; - $adapter = $this->createMock(Adapter::class); - $driver = $this->createMock(Driver::class); - $configuration = $this->createMock(Configuration::class); - $connection = $this->getMockBuilder(Connection::class) - ->onlyMethods(['connectTo']) - ->setConstructorArgs([ - [ - 'adapter' => $adapter, - 'platform' => new MySQLPlatform(), - 'tablePrefix' => 'nctest', - 'primary' => $connectionParams, - 'replica' => [ - $connectionParams, - ], - ], - $driver, - $configuration, - ]) - ->getMock(); - $driverConnection = $this->createMock(DriverConnection::class); - $connection->expects(self::once()) - ->method('connectTo') - ->with('primary') - ->willReturn($driverConnection); - - $connection->ensureConnectedToReplica(); - $connection->ensureConnectedToPrimary(); - $connection->ensureConnectedToReplica(); - } - - public function testClusterConnectsToPrimaryAndReplica(): void { - $connectionParamsPrimary = [ - 'user' => 'test', - 'password' => 'topsecret', - 'host' => 'testprimary', - ]; - $connectionParamsReplica = [ - 'user' => 'test', - 'password' => 'topsecret', - 'host' => 'testreplica', - ]; - $adapter = $this->createMock(Adapter::class); - $driver = $this->createMock(Driver::class); - $configuration = $this->createMock(Configuration::class); - $connection = $this->getMockBuilder(Connection::class) - ->onlyMethods(['connectTo']) - ->setConstructorArgs([ - [ - 'adapter' => $adapter, - 'platform' => new MySQLPlatform(), - 'tablePrefix' => 'nctest', - 'primary' => $connectionParamsPrimary, - 'replica' => [ - $connectionParamsReplica, - ], - ], - $driver, - $configuration, - ]) - ->getMock(); - $driverConnection = $this->createMock(DriverConnection::class); - $connection->expects(self::exactly(2)) - ->method('connectTo') - ->willReturn($driverConnection); - - $connection->ensureConnectedToReplica(); - $connection->ensureConnectedToPrimary(); - $connection->ensureConnectedToReplica(); - } - - public function testSuccessfulQueryResetsConnectivityCheckTimer(): void { - $inner = $this->getInnerConnection(); - - // Ensure the connection is established before touching the timer - $qb = $inner->getQueryBuilder(); - $qb->select('configvalue')->from('appconfig')->setMaxResults(1); - $qb->executeQuery()->closeCursor(); - - $property = $this->backdateLastConnectionCheck($inner); - $before = time(); - - $qb->executeQuery()->closeCursor(); - - // A connectivity probe firing between adjacent operations would reset - // the driver level last insert id on MySQL - self::assertGreaterThanOrEqual($before, max($property->getValue($inner))); - } - - public function testPreparedStatementExecutionResetsConnectivityCheckTimer(): void { - $inner = $this->getInnerConnection(); - - $statement = $inner->prepare('SELECT `configvalue` FROM `*PREFIX*appconfig`', 1); - - $property = $this->backdateLastConnectionCheck($inner); - $before = time(); - - $statement->executeQuery()->free(); - - self::assertGreaterThanOrEqual($before, max($property->getValue($inner))); - } - - private function getInnerConnection(): Connection { - $connection = Server::get(IDBConnection::class); - if (!$connection instanceof ConnectionAdapter) { - self::markTestSkipped('Test requires the real database connection'); - } - - return $connection->getInner(); - } - - /** - * Make the connectivity check timer stale, but by less than the check - * interval: the probe must not fire, so only actual query activity can - * refresh the timer. - */ - private function backdateLastConnectionCheck(Connection $connection): \ReflectionProperty { - $property = new \ReflectionProperty(Connection::class, 'lastConnectionCheck'); - $property->setValue($connection, ['primary' => time() - 20, 'replica' => time() - 20]); - - return $property; - } - -} --- a/tests/lib/DB/Exception/DbalExceptionTest.php +++ /dev/null @@ -1,76 +0,0 @@ -driverException = $this->createMock(TheDriverException::class); - } - - /** - * @param string $class - * @param int $reason - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDriverException')] - public function testDriverException(string $class, int $reason): void { - $result = DbalException::wrap(new $class($this->driverException, null)); - $this->assertSame($reason, $result->getReason()); - } - - public static function dataDriverException(): array { - return [ - [LockWaitTimeoutException::class, DbalException::REASON_LOCK_WAIT_TIMEOUT], - [ForeignKeyConstraintViolationException::class, DbalException::REASON_FOREIGN_KEY_VIOLATION], - [NotNullConstraintViolationException::class, DbalException::REASON_NOT_NULL_CONSTRAINT_VIOLATION], - [UniqueConstraintViolationException::class, DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION], - [ConstraintViolationException::class, DbalException::REASON_CONSTRAINT_VIOLATION], - [DatabaseObjectExistsException::class, DbalException::REASON_DATABASE_OBJECT_EXISTS], - [DatabaseObjectNotFoundException::class, DbalException::REASON_DATABASE_OBJECT_NOT_FOUND], - [DeadlockException::class, DbalException::REASON_DEADLOCK], - [InvalidFieldNameException::class, DbalException::REASON_INVALID_FIELD_NAME], - [NonUniqueFieldNameException::class, DbalException::REASON_NON_UNIQUE_FIELD_NAME], - [SyntaxErrorException::class, DbalException::REASON_SYNTAX_ERROR], - [ServerException::class, DbalException::REASON_SERVER], - [DriverException::class, DbalException::REASON_DRIVER], - ]; - } - - public function testConnectionException(): void { - $result = DbalException::wrap(ConnectionException::noActiveTransaction()); - $this->assertSame(DbalException::REASON_CONNECTION_LOST, $result->getReason()); - } - - public function testInvalidArgumentException(): void { - $result = DbalException::wrap(InvalidArgumentException::fromEmptyCriteria()); - $this->assertSame(DbalException::REASON_INVALID_ARGUMENT, $result->getReason()); - } -} --- a/tests/lib/DB/Middleware/UtcTimezoneMiddlewareDriverTest.php +++ /dev/null @@ -1,109 +0,0 @@ -connection = $this->getRootDbConnection(); - if ($this->connection === null) { - $this->markTestSkipped('No root database credentials provided (DB_ROOT_USER, DB_ROOT_PASSWORD), cannot run test'); - return; - } - - $provider = $this->connection->getDatabaseProvider(); - if ($provider === IDBConnection::PLATFORM_MARIADB || $provider === IDBConnection::PLATFORM_MYSQL) { - $this->connection->executeStatement("SET GLOBAL time_zone = 'America/New_York'"); - } else { - $this->markTestSkipped('This test only works with MySQL/MariaDB'); - } - } - - #[\Override] - protected function tearDown(): void { - if ($this->connection !== null) { - $provider = $this->connection->getDatabaseProvider(); - if ($provider === IDBConnection::PLATFORM_MARIADB || $provider === IDBConnection::PLATFORM_MYSQL) { - $this->connection->executeStatement("SET GLOBAL time_zone = 'SYSTEM'"); - } - $this->connection->close(); - } - - parent::tearDown(); - } - - public function testSqlNowIsInUtc() { - $connection = $this->getDbConnection(); - $result = $connection->executeQuery('SELECT NOW()'); - $data = $result->fetchOne(); - $connection->close(); - - self::assertIsString($data, 'Expected a string from the database'); - - $expected = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); - $received = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $data, new \DateTimeZone('UTC')); - - $diff = abs($received->getTimestamp() - $expected->getTimestamp()); - self::assertLessThan(15 * 60, $diff); // allow up to 15 minutes of difference, to account for slow test environments and time sync issues. - } - - /** - * Get a new database connection. - * This is needed because the setup is changing the global timezone setting, - * but its only applied for new connections. - */ - private function getDbConnection(array $overrides = []): Connection { - $config = Server::get(IConfig::class); - $cf = Server::get(ConnectionFactory::class); - return $cf->getConnection( - $config->getSystemValue('dbtype'), - [ - 'host' => $config->getSystemValue('dbhost'), - 'user' => $config->getSystemValue('dbuser'), - 'password' => $config->getSystemValue('dbpassword'), - 'tablePrefix' => $config->getSystemValue('dbtableprefix'), - 'dbname' => $config->getSystemValue('dbname'), - ...$overrides, - ], - ); - } - /** - * Get the database connection as root user, - * so that we can change the global timezone setting. - */ - private function getRootDbConnection(): ?Connection { - $rootUser = getenv('DB_ROOT_USER') ?: ''; - $rootPassword = getenv('DB_ROOT_PASSWORD') ?: ''; - if ($rootPassword === '' || $rootUser === '') { - return null; - } - - return $this->getDbConnection([ - 'user' => $rootUser, - 'password' => $rootPassword, - ]); - } -} --- a/tests/lib/DB/Middleware/UtcTimezoneMiddlewareTest.php +++ /dev/null @@ -1,24 +0,0 @@ -createMock(\Doctrine\DBAL\Driver::class); - $middleware = new UtcTimezoneMiddleware(); - $wrappedDriver = $middleware->wrap($driver); - - $this->assertInstanceOf(UtcTimezoneMiddlewareDriver::class, $wrappedDriver); - } -} --- a/tests/lib/DB/MigrationServiceTest.php +++ /dev/null @@ -1,1022 +0,0 @@ -db = $this->createMock(Connection::class); - $this->db - ->expects($this->any()) - ->method('getPrefix') - ->willReturn('test_oc_'); - - $this->migrationService = new MigrationService('testing', $this->db); - } - - public function testGetters(): void { - $this->assertEquals('testing', $this->migrationService->getApp()); - $this->assertEquals(\OC::$SERVERROOT . '/apps/testing/lib/Migration', $this->migrationService->getMigrationsDirectory()); - $this->assertEquals('OCA\Testing\Migration', $this->migrationService->getMigrationsNamespace()); - $this->assertEquals('test_oc_migrations', $this->migrationService->getMigrationsTableName()); - } - - public function testCore(): void { - $migrationService = new MigrationService('core', $this->db); - - $this->assertEquals('core', $migrationService->getApp()); - $this->assertEquals(\OC::$SERVERROOT . '/core/Migrations', $migrationService->getMigrationsDirectory()); - $this->assertEquals('OC\Core\Migrations', $migrationService->getMigrationsNamespace()); - $this->assertEquals('test_oc_migrations', $migrationService->getMigrationsTableName()); - } - - public function testExecuteUnknownStep(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Version 20170130180000 is unknown.'); - - $this->migrationService->executeStep('20170130180000'); - } - - public function testUnknownApp(): void { - $this->expectException(AppPathNotFoundException::class); - $this->expectExceptionMessage('Could not find path for unknown_bloody_app'); - - new MigrationService('unknown_bloody_app', $this->db); - } - - public function testExecuteStepWithUnknownClass(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Migration step \'X\' is unknown'); - - $migrationService = $this->getMockBuilder(MigrationService::class) - ->onlyMethods(['findMigrations']) - ->setConstructorArgs(['testing', $this->db]) - ->getMock(); - $migrationService->expects($this->any())->method('findMigrations')->willReturn( - ['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] - ); - $migrationService->executeStep('20170130180000'); - } - - public function testExecuteStepWithSchemaChange(): void { - $schema = $this->createMock(Schema::class); - $this->db->expects($this->any()) - ->method('createSchema') - ->willReturn($schema); - - $this->db->expects($this->once()) - ->method('migrateToSchema'); - - $wrappedSchema = $this->createMock(Schema::class); - $wrappedSchema->expects($this->atLeast(2)) - ->method('getTables') - ->willReturn([]); - $wrappedSchema->expects($this->atLeast(2)) - ->method('getSequences') - ->willReturn([]); - - $schemaResult = $this->createMock(SchemaWrapper::class); - $schemaResult->expects($this->once()) - ->method('getWrappedSchema') - ->willReturn($wrappedSchema); - - $step = $this->createMock(IMigrationStep::class); - $step->expects($this->once()) - ->method('preSchemaChange'); - $step->expects($this->once()) - ->method('changeSchema') - ->willReturn($schemaResult); - $step->expects($this->once()) - ->method('postSchemaChange'); - - $migrationService = $this->getMockBuilder(MigrationService::class) - ->onlyMethods(['createInstance']) - ->setConstructorArgs(['testing', $this->db]) - ->getMock(); - - $migrationService->expects($this->any()) - ->method('createInstance') - ->with('20170130180000') - ->willReturn($step); - - $migrationService->executeStep('20170130180000'); - } - - public function testExecuteStepWithoutSchemaChange(): void { - $schema = $this->createMock(Schema::class); - $this->db->expects($this->any()) - ->method('createSchema') - ->willReturn($schema); - - $this->db->expects($this->never()) - ->method('migrateToSchema'); - - $step = $this->createMock(IMigrationStep::class); - $step->expects($this->once()) - ->method('preSchemaChange'); - $step->expects($this->once()) - ->method('changeSchema') - ->willReturn(null); - $step->expects($this->once()) - ->method('postSchemaChange'); - - $migrationService = $this->getMockBuilder(MigrationService::class) - ->onlyMethods(['createInstance']) - ->setConstructorArgs(['testing', $this->db]) - ->getMock(); - - $migrationService->expects($this->any()) - ->method('createInstance') - ->with('20170130180000') - ->willReturn($step); - - $migrationService->executeStep('20170130180000'); - } - - public static function dataGetMigration(): array { - return [ - ['current', '10000Date20200819121721'], - ['prev', '8000Date20200407115318'], - ['next', '20000Date20240717180417'], - ['latest', '20000Date20240718031959'], - ]; - } - - /** - * @param string $alias - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetMigration')] - public function testGetMigration($alias, $expected): void { - $migrationService = $this->getMockBuilder(MigrationService::class) - ->onlyMethods(['getMigratedVersions', 'findMigrations']) - ->setConstructorArgs(['testing', $this->db]) - ->getMock(); - - $migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( - [ - '8000Date20200407115318', - '10000Date20200819121721', - ] - ); - $migrationService->expects($this->any())->method('findMigrations')->willReturn( - [ - '20000Date20240718031959' => 'D', - '10000Date20200819121721' => 'B', - '8000Date20200407115318' => 'A', - '20000Date20240717180417' => 'C', - ] - ); - - $this->assertSame([ - '8000Date20200407115318', - '10000Date20200819121721', - '20000Date20240717180417', - '20000Date20240718031959', - ], $migrationService->getAvailableVersions()); - - $migration = $migrationService->getMigration($alias); - $this->assertSame($expected, $migration); - } - - #[Group('DB')] - public function testGetMigratedVersionsSortsByVersionThenDate(): void { - /** @var Connection $db */ - $db = Server::get(Connection::class); - $appId = 'migration_sort_' . bin2hex(random_bytes(8)); - - $migrationService = new class('testing', $db, $appId) extends MigrationService { - public function __construct( - string $appName, - Connection $connection, - private string $migrationApp, - ) { - parent::__construct($appName, $connection); - } - - #[\Override] - public function getApp(): string { - return $this->migrationApp; - } - }; - - // Ensure the migrations table exists before inserting the fixtures. - self::assertSame([], $migrationService->getMigratedVersions()); - - $versions = [ - '20000Date20240718031959', - '10000Date20200819121721', - '8000Date20200407115318', - '20000Date20240717180417', - ]; - - try { - foreach ($versions as $version) { - $db->insertIfNotExist('*PREFIX*migrations', [ - 'app' => $appId, - 'version' => $version, - ]); - } - - self::assertSame([ - '8000Date20200407115318', - '10000Date20200819121721', - '20000Date20240717180417', - '20000Date20240718031959', - ], $migrationService->getMigratedVersions()); - } finally { - $qb = $db->getQueryBuilder(); - $qb->delete('migrations') - ->where($qb->expr()->eq( - 'app', - $qb->createNamedParameter($appId), - )) - ->executeStatement(); - } - } - - public function testMigrate(): void { - $migrationService = $this->getMockBuilder(MigrationService::class) - ->onlyMethods(['getMigratedVersions', 'findMigrations', 'executeStep']) - ->setConstructorArgs(['testing', $this->db]) - ->getMock(); - - $migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( - [ - '8000Date20200407115318', - '10000Date20200819121721', - ] - ); - $migrationService->expects($this->any())->method('findMigrations')->willReturn( - [ - '20000Date20240718031959' => 'D', - '10000Date20200819121721' => 'B', - '8000Date20200407115318' => 'A', - '20000Date20240717180417' => 'C', - ] - ); - - $this->assertSame([ - '8000Date20200407115318', - '10000Date20200819121721', - '20000Date20240717180417', - '20000Date20240718031959', - ], $migrationService->getAvailableVersions()); - - $calls = []; - $migrationService - ->expects($this->exactly(2)) - ->method('executeStep') - ->willReturnCallback(function (string $migration) use (&$calls): void { - $calls[] = $migration; - }); - - $migrationService->migrate(); - self::assertSame([ - '20000Date20240717180417', - '20000Date20240718031959', - ], $calls); - } - - #[DataProvider('dataEnsureNamingConstraintsTableName')] - public function testEnsureNamingConstraintsTableName(string $name, int $prefixLength, bool $tableExists, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $table = $this->createMock(Table::class); - $table->expects($this->atLeastOnce()) - ->method('getName') - ->willReturn($name); - $table->expects($this->any()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->any()) - ->method('getIndexes') - ->willReturn([]); - $table->expects($this->any()) - ->method('getForeignKeys') - ->willReturn([]); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects(self::once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willReturnCallback(fn () => match($tableExists) { - false => throw new SchemaException(), - true => $table, - }); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, $prefixLength); - } - - public static function dataEnsureNamingConstraintsTableName(): array { - return [ - 'valid name' => [ - \str_repeat('x', 60), // table name - 3, // prefix length - false, // has this table - false, // throws - ], - 'valid name - long prefix' => [ - \str_repeat('x', 55), - 8, - false, - false, - ], - 'too long but not a new table' => [ - \str_repeat('x', 61), - 3, - true, - false, - ], - 'too long' => [ - \str_repeat('x', 61), - 3, - false, - true, - ], - 'too long with prefix' => [ - \str_repeat('x', 60), - 4, - false, - true, - ], - ]; - } - - #[DataProvider('dataEnsureNamingConstraintsPrimaryDefaultKey')] - public function testEnsureNamingConstraintsPrimaryDefaultKey(string $tableName, int $prefixLength, string $platform, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $this->db->expects(self::atLeastOnce()) - ->method('getDatabaseProvider') - ->willReturn($platform); - - $defaultName = match ($platform) { - IDBConnection::PLATFORM_POSTGRES => $tableName . '_pkey', - IDBConnection::PLATFORM_ORACLE => $tableName . '_seq', - default => 'PRIMARY', - }; - - $index = $this->createMock(Index::class); - $index->expects($this->any()) - ->method('getName') - ->willReturn($defaultName); - $index->expects($this->any()) - ->method('getColumns') - ->willReturn([]); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn($tableName); - - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->once()) - ->method('getIndexes') - ->willReturn([]); - $table->expects($this->once()) - ->method('getForeignKeys') - ->willReturn([]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn($index); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->atMost(1)) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, $prefixLength); - } - - public static function dataEnsureNamingConstraintsPrimaryDefaultKey(): array { - foreach ([IDBConnection::PLATFORM_MYSQL, IDBConnection::PLATFORM_ORACLE, IDBConnection::PLATFORM_POSTGRES, IDBConnection::PLATFORM_SQLITE] as $engine) { - $testcases["$engine valid"] = [ - str_repeat('x', 55), - 3, - $engine, - false, - ]; - $testcases["$engine too long"] = [ - str_repeat('x', 56), // 56 (name) + 3 (prefix) + 5 ('_pkey')= 64 > 63 - 3, - $engine, - true, - ]; - $testcases["$engine too long prefix"] = [ - str_repeat('x', 55), - 4, - $engine, - true, - ]; - } - return $testcases; - } - - #[DataProvider('dataEnsureNamingConstraintsPrimaryCustomKey')] - public function testEnsureNamingConstraintsPrimaryCustomKey(string $name, int $prefixLength, bool $newIndex, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $index = $this->createMock(Index::class); - $index->expects($this->any()) - ->method('getName') - ->willReturn($name); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn('tablename'); - - $table->expects($this->any()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->any()) - ->method('getIndexes') - ->willReturn([]); - $table->expects($this->any()) - ->method('getForeignKeys') - ->willReturn([]); - $table->expects($this->atLeastOnce()) - ->method('getPrimaryKey') - ->willReturn($index); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willReturnCallback(fn () => match($newIndex) { - true => throw new SchemaException(), - false => $table, - }); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, $prefixLength); - } - - public static function dataEnsureNamingConstraintsPrimaryCustomKey(): array { - return [ - 'valid name' => [ - str_repeat('x', 60), - 3, - true, - false, - ], - 'valid name - prefix does not matter' => [ - str_repeat('x', 63), - 3, - true, - false, - ], - 'invalid name - but not new' => [ - str_repeat('x', 64), - 3, - false, - false, - ], - 'too long name' => [ - str_repeat('x', 64), - 3, - true, - true, - ], - ]; - } - - #[DataProvider('dataEnsureNamingConstraints')] - public function testEnsureNamingConstraintsColumnName(string $name, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $column = $this->createMock(Column::class); - $column->expects(self::atLeastOnce()) - ->method('getName') - ->willReturn($name); - - $table = $this->createMock(Table::class); - $table->expects(self::any()) - ->method('getName') - ->willReturn('valid'); - - $table->expects(self::once()) - ->method('getColumns') - ->willReturn([$column]); - $table->expects(self::atMost(1)) - ->method('getIndexes') - ->willReturn([]); - $table->expects(self::atMost(1)) - ->method('getForeignKeys') - ->willReturn([]); - - $schema = $this->createMock(Schema::class); - $schema->expects(self::once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects(self::once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects(self::any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects(self::any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, 3); - } - - #[DataProvider('dataEnsureNamingConstraints')] - public function testEnsureNamingConstraintsIndexName(string $name, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $index = $this->createMock(Index::class); - $index->expects(self::atLeastOnce()) - ->method('getName') - ->willReturn($name); - - $table = $this->createMock(Table::class); - $table->expects(self::any()) - ->method('getName') - ->willReturn('valid'); - - $table->expects(self::atMost(1)) - ->method('getColumns') - ->willReturn([]); - $table->expects(self::once()) - ->method('getIndexes') - ->willReturn([$index]); - $table->expects(self::atMost(1)) - ->method('getForeignKeys') - ->willReturn([]); - - $schema = $this->createMock(Schema::class); - $schema->expects(self::once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects(self::once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects(self::any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects(self::any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, 3); - } - - #[DataProvider('dataEnsureNamingConstraints')] - public function testEnsureNamingConstraintsForeignKeyName(string $name, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $foreignKey = $this->createMock(ForeignKeyConstraint::class); - $foreignKey->expects(self::any()) - ->method('getName') - ->willReturn($name); - - $table = $this->createMock(Table::class); - $table->expects(self::any()) - ->method('getName') - ->willReturn('valid'); - - $table->expects(self::once()) - ->method('getColumns') - ->willReturn([]); - $table->expects(self::once()) - ->method('getIndexes') - ->willReturn([]); - $table->expects(self::once()) - ->method('getForeignKeys') - ->willReturn([$foreignKey]); - - $schema = $this->createMock(Schema::class); - $schema->expects(self::once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects(self::once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects(self::any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects(self::any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, 3); - } - - #[DataProvider('dataEnsureNamingConstraints')] - public function testEnsureNamingConstraintsSequenceName(string $name, bool $throws): void { - if ($throws) { - $this->expectException(\InvalidArgumentException::class); - } - - $sequence = $this->createMock(Sequence::class); - $sequence->expects($this->any()) - ->method('getName') - ->willReturn($name); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([$sequence]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureNamingConstraints($sourceSchema, $schema, 3); - } - - public static function dataEnsureNamingConstraints(): array { - return [ - 'valid length' => [\str_repeat('x', 63), false], - 'too long' => [\str_repeat('x', 64), true], - ]; - } - - public function testEnsureOracleConstraintsValid(): void { - $table = $this->createMock(Table::class); - $table->expects($this->atLeastOnce()) - ->method('getName') - ->willReturn('tablename'); - - $primaryKey = $this->createMock(Index::class); - $primaryKey->expects($this->once()) - ->method('getName') - ->willReturn('primary_key'); - - $column = $this->createMock(Column::class); - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([$column]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn($primaryKey); - - $sequence = $this->createMock(Sequence::class); - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([$sequence]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - public function testEnsureOracleConstraintsValidWithPrimaryKey(): void { - $index = $this->createMock(Index::class); - $index->expects($this->any()) - ->method('getName') - ->willReturn(\str_repeat('a', 30)); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn(\str_repeat('a', 26)); - - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn($index); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - public function testEnsureOracleConstraintsValidWithPrimaryKeyDefault(): void { - $defaultName = 'PRIMARY'; - if ($this->db->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) { - $defaultName = \str_repeat('a', 26) . '_' . \str_repeat('b', 30) . '_seq'; - } elseif ($this->db->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) { - $defaultName = \str_repeat('a', 26) . '_seq'; - } - - $index = $this->createMock(Index::class); - $index->expects($this->any()) - ->method('getName') - ->willReturn($defaultName); - $index->expects($this->any()) - ->method('getColumns') - ->willReturn([\str_repeat('b', 30)]); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn(\str_repeat('a', 25)); - - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn($index); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - public function testEnsureOracleConstraintsNoPrimaryKey(): void { - $this->markTestSkipped('Test disabled for now due to multiple reasons, see https://github.com/nextcloud/server/pull/31580#issuecomment-1069182234 for details.'); - $this->expectException(\InvalidArgumentException::class); - - $table = $this->createMock(Table::class); - $table->expects($this->atLeastOnce()) - ->method('getName') - ->willReturn('tablename'); - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn(null); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - /** - * Alternative for testEnsureOracleConstraintsNoPrimaryKey until we enforce it. - */ - public function testEnsureOracleConstraintsNoPrimaryKeyLogging(): void { - $table = $this->createMock(Table::class); - $table->expects($this->atLeastOnce()) - ->method('getName') - ->willReturn('tablename'); - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([]); - $table->expects($this->once()) - ->method('getPrimaryKey') - ->willReturn(null); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->expects($this->once()) - ->method('getSequences') - ->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $logger = $this->createMock(LoggerInterface::class); - $logger->expects(self::once()) - ->method('error'); - $this->overwriteService(LoggerInterface::class, $logger); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - #[TestWith([true])] - #[TestWith([false])] - public function testEnsureOracleConstraintsBooleanNotNull(bool $isOracle): void { - $this->db->method('getDatabaseProvider') - ->willReturn($isOracle ? IDBConnection::PLATFORM_ORACLE : IDBConnection::PLATFORM_MARIADB); - - $column = $this->createMock(Column::class); - $column->expects($this->any()) - ->method('getName') - ->willReturn('aaaa'); - $column->expects($this->any()) - ->method('getType') - ->willReturn(Type::getType('boolean')); - $column->expects($this->any()) - ->method('getNotnull') - ->willReturn(true); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn('tablename'); - $table->method('getIndexes')->willReturn([]); - $table->method('getForeignKeys')->willReturn([]); - - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([$column]); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - $schema->method('getSequences')->willReturn([]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - if ($isOracle) { - $column->expects($this->once()) - ->method('setNotnull') - ->with(false); - } else { - $column->expects($this->never()) - ->method('setNotnull'); - } - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } - - public function testEnsureOracleConstraintsStringLength4000(): void { - $this->expectException(\InvalidArgumentException::class); - - $column = $this->createMock(Column::class); - $column->expects($this->any()) - ->method('getName') - ->willReturn('aaaa'); - $column->expects($this->any()) - ->method('getType') - ->willReturn(Type::getType('string')); - $column->expects($this->any()) - ->method('getLength') - ->willReturn(4001); - - $table = $this->createMock(Table::class); - $table->expects($this->any()) - ->method('getName') - ->willReturn('tablename'); - - $table->expects($this->once()) - ->method('getColumns') - ->willReturn([$column]); - - $schema = $this->createMock(Schema::class); - $schema->expects($this->once()) - ->method('getTables') - ->willReturn([$table]); - - $sourceSchema = $this->createMock(Schema::class); - $sourceSchema->expects($this->any()) - ->method('getTable') - ->willThrowException(new SchemaException()); - $sourceSchema->expects($this->any()) - ->method('hasSequence') - ->willReturn(false); - - $this->migrationService->ensureOracleConstraints($sourceSchema, $schema); - } -} --- a/tests/lib/DB/MigratorTest.php +++ /dev/null @@ -1,314 +0,0 @@ -config = Server::get(IConfig::class); - $this->connection = Server::get(\OC\DB\Connection::class); - - $this->tableName = $this->getUniqueTableName(); - $this->tableNameTmp = $this->getUniqueTableName(); - } - - private function getMigrator(): Migrator { - $dispatcher = Server::get(IEventDispatcher::class); - if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE) { - return new SQLiteMigrator($this->connection, $this->config, $dispatcher); - } elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) { - return new OracleMigrator($this->connection, $this->config, $dispatcher); - } - return new Migrator($this->connection, $this->config, $dispatcher); - } - - private function getUniqueTableName() { - return strtolower($this->getUniqueID($this->config->getSystemValueString('dbtableprefix', 'oc_') . 'test_')); - } - - #[\Override] - protected function tearDown(): void { - // Try to delete if exists (IF EXISTS NOT SUPPORTED IN ORACLE) - try { - $this->connection->exec('DROP TABLE ' . $this->connection->quoteIdentifier($this->tableNameTmp)); - } catch (Exception $e) { - } - - try { - $this->connection->exec('DROP TABLE ' . $this->connection->quoteIdentifier($this->tableName)); - } catch (Exception $e) { - } - parent::tearDown(); - } - - /** - * @return \Doctrine\DBAL\Schema\Schema[] - */ - private function getDuplicateKeySchemas() { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', 'integer'); - $table->addColumn('name', 'string'); - $table->addIndex(['id'], $this->tableName . '_id'); - - $endSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $endSchema->createTable($this->tableName); - $table->addColumn('id', 'integer'); - $table->addColumn('name', 'string'); - $table->addUniqueIndex(['id'], $this->tableName . '_id'); - - return [$startSchema, $endSchema]; - } - - /** - * @return \Doctrine\DBAL\Schema\Schema[] - */ - private function getChangedTypeSchema($from, $to) { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', $from); - $table->addColumn('name', 'string'); - $table->addIndex(['id'], $this->tableName . '_id'); - - $endSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $endSchema->createTable($this->tableName); - $table->addColumn('id', $to); - $table->addColumn('name', 'string'); - $table->addIndex(['id'], $this->tableName . '_id'); - - return [$startSchema, $endSchema]; - } - - private function getSchemaConfig() { - $config = new SchemaConfig(); - $config->setName($this->connection->getDatabase()); - return $config; - } - - public function testUpgrade(): void { - [$startSchema, $endSchema] = $this->getDuplicateKeySchemas(); - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $this->connection->insert($this->tableName, ['id' => 1, 'name' => 'foo']); - $this->connection->insert($this->tableName, ['id' => 2, 'name' => 'bar']); - $this->connection->insert($this->tableName, ['id' => 3, 'name' => 'qwerty']); - - $migrator->migrate($endSchema); - $this->addToAssertionCount(1); - } - - public function testUpgradeDifferentPrefix(): void { - $oldTablePrefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); - - $this->config->setSystemValue('dbtableprefix', 'ownc_'); - $this->tableName = strtolower($this->getUniqueID($this->config->getSystemValueString('dbtableprefix') . 'test_')); - - [$startSchema, $endSchema] = $this->getDuplicateKeySchemas(); - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $this->connection->insert($this->tableName, ['id' => 1, 'name' => 'foo']); - $this->connection->insert($this->tableName, ['id' => 2, 'name' => 'bar']); - $this->connection->insert($this->tableName, ['id' => 3, 'name' => 'qwerty']); - - $migrator->migrate($endSchema); - $this->addToAssertionCount(1); - - $this->config->setSystemValue('dbtableprefix', $oldTablePrefix); - } - - public function testInsertAfterUpgrade(): void { - [$startSchema, $endSchema] = $this->getDuplicateKeySchemas(); - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $migrator->migrate($endSchema); - - $this->connection->insert($this->tableName, ['id' => 1, 'name' => 'foo']); - $this->connection->insert($this->tableName, ['id' => 2, 'name' => 'bar']); - try { - $this->connection->insert($this->tableName, ['id' => 2, 'name' => 'qwerty']); - $this->fail('Expected duplicate key insert to fail'); - } catch (Exception $e) { - $this->addToAssertionCount(1); - } - } - - public function testAddingPrimaryKeyWithAutoIncrement(): void { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', 'integer'); - $table->addColumn('name', 'string'); - - $endSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $endSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true]); - $table->addColumn('name', 'string'); - $table->setPrimaryKey(['id']); - - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $migrator->migrate($endSchema); - - $this->addToAssertionCount(1); - } - - public function testReservedKeywords(): void { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true]); - $table->addColumn('user', 'string', ['length' => 255]); - $table->setPrimaryKey(['id']); - - $endSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $endSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true]); - $table->addColumn('user', 'string', ['length' => 64]); - $table->setPrimaryKey(['id']); - - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $migrator->migrate($endSchema); - - $this->addToAssertionCount(1); - } - - /** - * Test for nextcloud/server#36803 - */ - public function testColumnCommentsInUpdate(): void { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true, 'comment' => 'foo']); - $table->setPrimaryKey(['id']); - - $endSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $endSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true, 'comment' => 'foo']); - // Assert adding comments on existing tables work (or at least does not throw) - $table->addColumn('time', 'integer', ['comment' => 'unix-timestamp', 'notnull' => false]); - $table->setPrimaryKey(['id']); - - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $migrator->migrate($endSchema); - - $this->addToAssertionCount(1); - } - - public function testAddingForeignKey(): void { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', 'integer', ['autoincrement' => true]); - $table->addColumn('name', 'string'); - $table->setPrimaryKey(['id']); - - $fkName = 'fkc'; - $tableFk = $startSchema->createTable($this->tableNameTmp); - $tableFk->addColumn('fk_id', 'integer'); - $tableFk->addColumn('name', 'string'); - $tableFk->addForeignKeyConstraint($this->tableName, ['fk_id'], ['id'], [], $fkName); - - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - $this->assertTrue($startSchema->getTable($this->tableNameTmp)->hasForeignKey($fkName)); - } - - public static function dataNotNullEmptyValuesFailOracle(): array { - return [ - [ParameterType::BOOLEAN, true, Types::BOOLEAN, false], - [ParameterType::BOOLEAN, false, Types::BOOLEAN, true], - - [ParameterType::STRING, 'foo', Types::STRING, false], - [ParameterType::STRING, '', Types::STRING, true], - - [ParameterType::INTEGER, 1234, Types::INTEGER, false], - [ParameterType::INTEGER, 0, Types::INTEGER, false], // Integer 0 is not stored as Null and therefor works - - [ParameterType::STRING, '{"a": 2}', Types::JSON, false], - ]; - } - - /** - * - * @param int $parameterType - * @param bool|int|string $value - * @param string $columnType - * @param bool $oracleThrows - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataNotNullEmptyValuesFailOracle')] - public function testNotNullEmptyValuesFailOracle(int $parameterType, $value, string $columnType, bool $oracleThrows): void { - $startSchema = new Schema([], [], $this->getSchemaConfig()); - $table = $startSchema->createTable($this->tableName); - $table->addColumn('id', Types::BIGINT); - $table->addColumn('will_it_blend', $columnType, [ - 'notnull' => true, - ]); - $table->addIndex(['id'], $this->tableName . '_id'); - - $migrator = $this->getMigrator(); - $migrator->migrate($startSchema); - - if ($oracleThrows && $this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) { - // Oracle can not store false|empty string in notnull columns - $this->expectException(\Doctrine\DBAL\Exception\NotNullConstraintViolationException::class); - } - - $this->connection->insert( - $this->tableName, - ['id' => 1, 'will_it_blend' => $value], - ['id' => ParameterType::INTEGER, 'will_it_blend' => $parameterType], - ); - - $this->addToAssertionCount(1); - } -} --- a/tests/lib/DB/OCPostgreSqlPlatformTest.php +++ /dev/null @@ -1,60 +0,0 @@ -createTableAndColumn($sourceSchema, Types::INTEGER); - $this->createTableAndColumn($targetSchema, Types::BIGINT); - - $comparator = new Comparator(); - $diff = $comparator->compare($sourceSchema, $targetSchema); - $sqlStatements = $diff->toSql($platform); - $this->assertContains( - 'ALTER TABLE poor_yorick ALTER id TYPE BIGINT', - $sqlStatements, - true - ); - - $this->assertNotContains( - 'ALTER TABLE poor_yorick ALTER id DROP DEFAULT', - $sqlStatements, - true - ); - } - - protected function createTableAndColumn($schema, $type) { - $table = $schema->createTable('poor_yorick'); - $table->addColumn('id', $type, [ - 'autoincrement' => true, - 'unsigned' => true, - 'notnull' => true, - 'length' => 11, - ]); - } -} --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php +++ /dev/null @@ -1,369 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->prepareTestingTable(); - } - - public static function likeProvider(): array { - $connection = Server::get(IDBConnection::class); - - return [ - ['foo', 'bar', false], - ['foo', 'foo', true], - ['foo', 'f%', true], - ['foo', '%o', true], - ['foo', '%', true], - ['foo', 'fo_', true], - ['foo', 'foo_', false], - ['foo', $connection->escapeLikeParameter('fo_'), false], - ['foo', $connection->escapeLikeParameter('f%'), false], - ]; - } - - /** - * - * @param string $param1 - * @param string $param2 - * @param boolean $match - */ - #[\PHPUnit\Framework\Attributes\DataProvider('likeProvider')] - public function testLike($param1, $param2, $match): void { - $query = $this->connection->getQueryBuilder(); - - $query->select(new Literal('1')) - ->from('users') - ->where($query->expr()->like($query->createNamedParameter($param1), $query->createNamedParameter($param2))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($match, $column); - } - - public static function ilikeProvider(): array { - $connection = Server::get(IDBConnection::class); - - return [ - ['foo', 'bar', false], - ['foo', 'foo', true], - ['foo', 'Foo', true], - ['foo', 'f%', true], - ['foo', '%o', true], - ['foo', '%', true], - ['foo', 'fo_', true], - ['foo', 'foo_', false], - ['foo', $connection->escapeLikeParameter('fo_'), false], - ['foo', $connection->escapeLikeParameter('f%'), false], - ]; - } - - /** - * - * @param string $param1 - * @param string $param2 - * @param boolean $match - */ - #[\PHPUnit\Framework\Attributes\DataProvider('ilikeProvider')] - public function testILike($param1, $param2, $match): void { - $query = $this->connection->getQueryBuilder(); - - $query->select(new Literal('1')) - ->from('users') - ->where($query->expr()->iLike($query->createNamedParameter($param1), $query->createNamedParameter($param2))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($match, $column); - } - - public static function notILikeProvider(): array { - $connection = Server::get(IDBConnection::class); - - return [ - ['foo', 'bar', true], - ['foo', 'foo', false], - ['foo', 'Foo', false], - ['foo', 'f%', false], - ['foo', '%o', false], - ['foo', '%', false], - ['foo', 'fo_', false], - ['foo', 'foo_', true], - ['foo', $connection->escapeLikeParameter('fo_'), true], - ['foo', $connection->escapeLikeParameter('f%'), true], - ]; - } - - /** - * - * @param string $param1 - * @param string $param2 - * @param boolean $match - */ - #[\PHPUnit\Framework\Attributes\DataProvider('notILikeProvider')] - public function testNotILike($param1, $param2, $match): void { - $query = $this->connection->getQueryBuilder(); - - $query->select(new Literal('1')) - ->from('users') - ->where($query->expr()->notILike($query->createNamedParameter($param1), $query->createNamedParameter($param2))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($match, $column); - } - - public function testCastColumn(): void { - $appId = $this->getUniqueID('testing'); - $this->createConfig($appId, '1', '4'); - - $query = $this->connection->getQueryBuilder(); - $query->update('appconfig') - ->set('configvalue', - $query->expr()->castColumn( - $query->createFunction( - '(' . $query->expr()->castColumn('configvalue', IQueryBuilder::PARAM_INT) - . ' + 1)' - ), IQueryBuilder::PARAM_STR - ) - ) - ->where($query->expr()->eq('appid', $query->createNamedParameter($appId))) - ->andWhere($query->expr()->eq('configkey', $query->createNamedParameter('1'))); - - $result = $query->executeStatement(); - $this->assertEquals(1, $result); - } - - public function testLongText(): void { - $appId = $this->getUniqueID('testing'); - $this->createConfig($appId, 'mykey', 'myvalue'); - - $query = $this->connection->getQueryBuilder(); - $query->select('*') - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter($appId))) - ->andWhere($query->expr()->eq('configkey', $query->createNamedParameter('mykey'))) - ->andWhere($query->expr()->eq('configvalue', $query->createNamedParameter('myvalue', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)); - - $result = $query->executeQuery(); - $entries = $result->fetchAllAssociative(); - $result->closeCursor(); - self::assertCount(1, $entries); - self::assertEquals('myvalue', $entries[0]['configvalue']); - } - - public function testJson(): void { - $appId = $this->getUniqueID('testing'); - $query = $this->connection->getQueryBuilder(); - $query->insert('share') - ->values([ - 'uid_owner' => $query->createNamedParameter('uid_owner'), - 'item_type' => $query->createNamedParameter('item_type'), - 'permissions' => $query->createNamedParameter(0), - 'stime' => $query->createNamedParameter(0), - 'accepted' => $query->createNamedParameter(0), - 'mail_send' => $query->createNamedParameter(0), - 'share_type' => $query->createNamedParameter(0), - 'share_with' => $query->createNamedParameter($appId), - 'attributes' => $query->createNamedParameter('[["permissions","before"]]'), - ]) - ->executeStatement(); - - $query = $this->connection->getQueryBuilder(); - $query->update('share') - ->set('attributes', $query->createNamedParameter('[["permissions","after"]]')); - if ($this->connection->getDatabaseProvider(true) === IDBConnection::PLATFORM_MYSQL) { - $query->where($query->expr()->eq('attributes', $query->createFunction("JSON_ARRAY(JSON_ARRAY('permissions','before'))"), IQueryBuilder::PARAM_JSON)); - } else { - $query->where($query->expr()->eq('attributes', $query->createNamedParameter('[["permissions","before"]]'), IQueryBuilder::PARAM_JSON)); - } - $query->executeStatement(); - - $query = $this->connection->getQueryBuilder(); - $query->select('attributes') - ->from('share') - ->where($query->expr()->eq('share_with', $query->createNamedParameter($appId))); - - $result = $query->executeQuery(); - $entries = $result->fetchAll(); - $result->closeCursor(); - self::assertCount(1, $entries); - self::assertEquals([['permissions','after']], json_decode($entries[0]['attributes'], true)); - } - - public function testDateTimeEquals(): void { - $dateTime = new \DateTime('2023-01-01'); - $insert = $this->connection->getQueryBuilder(); - $insert->insert('testing') - ->values(['datetime' => $insert->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)]) - ->executeStatement(); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('*') - ->from('testing') - ->where($query->expr()->eq('datetime', $query->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE))) - ->executeQuery(); - $entries = $result->fetchAllAssociative(); - $result->closeCursor(); - self::assertCount(1, $entries); - } - - public function testDateTimeLess(): void { - $dateTime = new \DateTime('2022-01-01'); - $dateTimeCompare = new \DateTime('2022-01-02'); - $insert = $this->connection->getQueryBuilder(); - $insert->insert('testing') - ->values(['datetime' => $insert->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)]) - ->executeStatement(); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('*') - ->from('testing') - ->where($query->expr()->lt('datetime', $query->createNamedParameter($dateTimeCompare, IQueryBuilder::PARAM_DATETIME_MUTABLE))) - ->executeQuery(); - $entries = $result->fetchAllAssociative(); - $result->closeCursor(); - self::assertCount(1, $entries); - } - - public function testDateTimeGreater(): void { - $dateTime = new \DateTime('2023-01-02'); - $dateTimeCompare = new \DateTime('2023-01-01'); - $insert = $this->connection->getQueryBuilder(); - $insert->insert('testing') - ->values(['datetime' => $insert->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)]) - ->executeStatement(); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('*') - ->from('testing') - ->where($query->expr()->gt('datetime', $query->createNamedParameter($dateTimeCompare, IQueryBuilder::PARAM_DATETIME_MUTABLE))) - ->executeQuery(); - $entries = $result->fetchAllAssociative(); - $result->closeCursor(); - self::assertCount(1, $entries); - } - - public static function dataExists(): array { - $connection = Server::get(IDBConnection::class); - - $query1 = $connection->getQueryBuilder() - ->select(new Literal('1')) - ->from('users'); - - $query2 = $connection->getQueryBuilder(); - $query2->select(new Literal('1')) - ->from('users') - ->where($query2->expr()->eq($query2->createNamedParameter('a'), $query2->createNamedParameter('b'))); - return [ - [$query1, true], - [$query2, false], - ]; - } - - #[DataProvider('dataExists')] - public function testExists(IQueryBuilder $subquery, bool $match): void { - $query = $this->connection->getQueryBuilder(); - - foreach ($subquery->getParameters() as $key => $value) { - $query->setParameter($key, $value); - } - - $query->select(new Literal('1')) - ->from('users') - ->where($query->expr()->exists($subquery)); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($match, $column); - } - - public static function dataNotExists(): array { - return array_map(fn (array $data) => [$data[0], !$data[1]], self::dataExists()); - } - - #[DataProvider('dataNotExists')] - public function testNotExists(IQueryBuilder $subquery, bool $match): void { - $query = $this->connection->getQueryBuilder(); - - foreach ($subquery->getParameters() as $key => $value) { - $query->setParameter($key, $value); - } - - $query->select(new Literal('1')) - ->from('users') - ->where($query->expr()->notExists($subquery)); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($match, $column); - } - - protected function createConfig($appId, $key, $value) { - $query = $this->connection->getQueryBuilder(); - $query->insert('appconfig') - ->values([ - 'appid' => $query->createNamedParameter($appId), - 'configkey' => $query->createNamedParameter((string)$key), - 'configvalue' => $query->createNamedParameter((string)$value), - ]) - ->executeStatement(); - } - - protected function prepareTestingTable(): void { - if ($this->schemaSetup) { - $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); - } - - $prefix = Server::get(IConfig::class)->getSystemValueString('dbtableprefix', 'oc_'); - $schema = $this->connection->createSchema(); - try { - $schema->getTable($prefix . 'testing'); - $this->connection->getQueryBuilder()->delete('testing')->executeStatement(); - } catch (SchemaException $e) { - $this->schemaSetup = true; - $table = $schema->createTable($prefix . 'testing'); - $table->addColumn('id', Types::BIGINT, [ - 'autoincrement' => true, - 'notnull' => true, - ]); - - $table->addColumn('datetime', Types::DATETIME, [ - 'notnull' => false, - ]); - - $table->setPrimaryKey(['id']); - $this->connection->migrateToSchema($schema); - } - } -} --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderTest.php +++ /dev/null @@ -1,366 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->internalConnection = Server::get(Connection::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $queryBuilder = $this->createMock(IQueryBuilder::class); - $queryBuilder->method('func') - ->willReturn($this->createMock(IFunctionBuilder::class)); - - $this->expressionBuilder = new ExpressionBuilder($this->connection, $queryBuilder, $this->logger); - - $this->doctrineExpressionBuilder = new DoctrineExpressionBuilder($this->internalConnection); - } - - public static function dataComparison(): array { - $valueSets = self::dataComparisons(); - $comparisonOperators = ['=', '<>', '<', '>', '<=', '>=']; - - $testSets = []; - foreach ($comparisonOperators as $operator) { - foreach ($valueSets as $values) { - $testSets[] = array_merge([$operator], $values); - } - } - return $testSets; - } - - #[DataProvider('dataComparison')] - public function testComparison(string $comparison, string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->comparison($doctrineInput1, $comparison, $doctrineInput2), - $this->expressionBuilder->comparison($ocInput1, $comparison, $ocInput2) - ); - } - - public static function dataComparisons(): array { - return [ - ['value', false, 'value', false], - ['value', false, 'value', true], - ['value', true, 'value', false], - ['value', true, 'value', true], - ]; - } - - #[DataProvider('dataComparisons')] - public function testEquals(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->eq($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->eq($ocInput1, $ocInput2) - ); - } - - #[DataProvider('dataComparisons')] - public function testNotEquals(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->neq($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->neq($ocInput1, $ocInput2) - ); - } - - #[DataProvider('dataComparisons')] - public function testLowerThan(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->lt($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->lt($ocInput1, $ocInput2) - ); - } - - #[DataProvider('dataComparisons')] - public function testLowerThanEquals(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->lte($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->lte($ocInput1, $ocInput2) - ); - } - - #[DataProvider('dataComparisons')] - public function testGreaterThan(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->gt($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->gt($ocInput1, $ocInput2) - ); - } - - #[DataProvider('dataComparisons')] - public function testGreaterThanEquals(string $input1, bool $isInput1Literal, string $input2, bool $isInput2Literal): void { - [$doctrineInput1, $ocInput1] = $this->helpWithLiteral($input1, $isInput1Literal); - [$doctrineInput2, $ocInput2] = $this->helpWithLiteral($input2, $isInput2Literal); - - $this->assertEquals( - $this->doctrineExpressionBuilder->gte($doctrineInput1, $doctrineInput2), - $this->expressionBuilder->gte($ocInput1, $ocInput2) - ); - } - - public function testIsNull(): void { - $this->assertEquals( - $this->doctrineExpressionBuilder->isNull('`test`'), - $this->expressionBuilder->isNull('test') - ); - } - - public function testIsNotNull(): void { - $this->assertEquals( - $this->doctrineExpressionBuilder->isNotNull('`test`'), - $this->expressionBuilder->isNotNull('test') - ); - } - - public static function dataLike(): array { - return [ - ['value', false], - ['value', true], - ]; - } - - #[DataProvider('dataLike')] - public function testLike(string $input, bool $isLiteral): void { - [$doctrineInput, $ocInput] = $this->helpWithLiteral($input, $isLiteral); - - $this->assertEquals( - $this->doctrineExpressionBuilder->like('`test`', $doctrineInput), - $this->expressionBuilder->like('test', $ocInput) - ); - } - - #[DataProvider('dataLike')] - public function testNotLike(string $input, bool $isLiteral): void { - [$doctrineInput, $ocInput] = $this->helpWithLiteral($input, $isLiteral); - - $this->assertEquals( - $this->doctrineExpressionBuilder->notLike('`test`', $doctrineInput), - $this->expressionBuilder->notLike('test', $ocInput) - ); - } - - public function testILike(): void { - // iLike is implemented using lower() on both sides, so we just verify it returns a string - $result = $this->expressionBuilder->iLike('test', 'value'); - $this->assertIsString($result); - } - - public function testNotILike(): void { - // notILike is implemented using notLike with lower() on both sides, so we just verify it returns a string - $result = $this->expressionBuilder->notILike('test', 'value'); - $this->assertIsString($result); - } - - public static function dataIn(): array { - return [ - ['value', false], - ['value', true], - [['value'], false], - [['value'], true], - ]; - } - - #[DataProvider('dataIn')] - public function testIn(string|array $input, bool $isLiteral): void { - [$doctrineInput, $ocInput] = $this->helpWithLiteral($input, $isLiteral); - - $this->assertEquals( - $this->doctrineExpressionBuilder->in('`test`', $doctrineInput), - $this->expressionBuilder->in('test', $ocInput) - ); - } - - #[DataProvider('dataIn')] - public function testNotIn(string|array $input, bool $isLiteral): void { - [$doctrineInput, $ocInput] = $this->helpWithLiteral($input, $isLiteral); - - $this->assertEquals( - $this->doctrineExpressionBuilder->notIn('`test`', $doctrineInput), - $this->expressionBuilder->notIn('test', $ocInput) - ); - } - - protected function helpWithLiteral(string|array $input, bool $isLiteral) { - if ($isLiteral) { - if (is_array($input)) { - $doctrineInput = array_map(function ($ident) { - return $this->doctrineExpressionBuilder->literal($ident); - }, $input); - $ocInput = array_map(function ($ident) { - return $this->expressionBuilder->literal($ident); - }, $input); - } else { - $doctrineInput = $this->doctrineExpressionBuilder->literal($input); - $ocInput = $this->expressionBuilder->literal($input); - } - } else { - if (is_array($input)) { - $doctrineInput = array_map(function ($input) { - return '`' . $input . '`'; - }, $input); - $ocInput = $input; - } else { - $doctrineInput = '`' . $input . '`'; - $ocInput = $input; - } - } - - return [$doctrineInput, $ocInput]; - } - - public static function dataLiteral(): array { - return [ - ['value', null], - ['1', null], - [1, null], - [1, 'string'], - [1, 'integer'], - [1, IQueryBuilder::PARAM_INT], - ]; - } - - #[DataProvider('dataLiteral')] - public function testLiteral(string|int $input, string|int|null $type): void { - /** @var Literal $actual */ - $actual = $this->expressionBuilder->literal($input, $type); - - $this->assertInstanceOf('\OC\DB\QueryBuilder\Literal', $actual); - $this->assertEquals( - $this->doctrineExpressionBuilder->literal($input, $type), - $actual->__toString() - ); - } - - public static function dataClobComparisons(): array { - return [ - ['eq', '5', IQueryBuilder::PARAM_STR, false, 3], - ['eq', '5', IQueryBuilder::PARAM_STR, true, 1], - ['neq', '5', IQueryBuilder::PARAM_STR, false, 8], - ['neq', '5', IQueryBuilder::PARAM_STR, true, 6], - ['lt', '5', IQueryBuilder::PARAM_STR, false, 3], - ['lt', '5', IQueryBuilder::PARAM_STR, true, 1], - ['lte', '5', IQueryBuilder::PARAM_STR, false, 6], - ['lte', '5', IQueryBuilder::PARAM_STR, true, 4], - ['gt', '5', IQueryBuilder::PARAM_STR, false, 5], - ['gt', '5', IQueryBuilder::PARAM_STR, true, 1], - ['gte', '5', IQueryBuilder::PARAM_STR, false, 8], - ['gte', '5', IQueryBuilder::PARAM_STR, true, 4], - ['like', '%5%', IQueryBuilder::PARAM_STR, false, 3], - ['like', '%5%', IQueryBuilder::PARAM_STR, true, 1], - ['like', 'under_%', IQueryBuilder::PARAM_STR, false, 2], - ['like', 'under\_%', IQueryBuilder::PARAM_STR, false, 1], - ['notLike', '%5%', IQueryBuilder::PARAM_STR, false, 8], - ['notLike', '%5%', IQueryBuilder::PARAM_STR, true, 6], - ['iLike', '%5%', IQueryBuilder::PARAM_STR, false, 3], - ['iLike', '%5%', IQueryBuilder::PARAM_STR, true, 1], - ['notILike', '%5%', IQueryBuilder::PARAM_STR, false, 8], - ['notILike', '%5%', IQueryBuilder::PARAM_STR, true, 6], - ['in', ['5'], IQueryBuilder::PARAM_STR_ARRAY, false, 3], - ['in', ['5'], IQueryBuilder::PARAM_STR_ARRAY, true, 1], - ['notIn', ['5'], IQueryBuilder::PARAM_STR_ARRAY, false, 8], - ['notIn', ['5'], IQueryBuilder::PARAM_STR_ARRAY, true, 6], - ]; - } - - #[DataProvider('dataClobComparisons')] - public function testClobComparisons(string $function, string|array $value, int $type, bool $compareKeyToValue, int $expected): void { - $appId = $this->getUniqueID('testing'); - $this->createConfig($appId, 1, 4); - $this->createConfig($appId, 2, 5); - $this->createConfig($appId, 3, 6); - $this->createConfig($appId, 4, 4); - $this->createConfig($appId, 5, 5); - $this->createConfig($appId, 6, 6); - $this->createConfig($appId, 7, 4); - $this->createConfig($appId, 8, 5); - $this->createConfig($appId, 9, 6); - $this->createConfig($appId, 10, 'under_score'); - $this->createConfig($appId, 11, 'underscore'); - - $query = $this->connection->getQueryBuilder(); - $query->select($query->func()->count('*', 'count')) - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter($appId))) - ->andWhere(call_user_func([$query->expr(), $function], 'configvalue', $query->createNamedParameter($value, $type), IQueryBuilder::PARAM_STR)); - - if ($compareKeyToValue) { - $query->andWhere(call_user_func([$query->expr(), $function], 'configkey', 'configvalue', IQueryBuilder::PARAM_STR)); - } - - $result = $query->executeQuery(); - - $this->assertEquals(['count' => $expected], $result->fetchAssociative()); - $result->closeCursor(); - - $query = $this->connection->getQueryBuilder(); - $query->delete('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter($appId))) - ->executeStatement(); - } - - protected function createConfig(string $appId, int $key, int|string $value) { - $query = $this->connection->getQueryBuilder(); - $query->insert('appconfig') - ->values([ - 'appid' => $query->createNamedParameter($appId), - 'configkey' => $query->createNamedParameter((string)$key), - 'configvalue' => $query->createNamedParameter((string)$value), - ]) - ->executeStatement(); - } -} --- a/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php +++ /dev/null @@ -1,518 +0,0 @@ -connection = Server::get(IDBConnection::class); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerTestConcatString')] - public function testConcatString($closure): void { - $query = $this->connection->getQueryBuilder(); - [$real, $arguments, $return] = $closure($query); - if ($real) { - $this->addDummyData(); - $query->where($query->expr()->eq('appid', $query->createNamedParameter('group_concat'))); - $query->orderBy('configkey', 'asc'); - } - - $query->select($query->func()->concat(...$arguments)); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($return, $column); - } - - public static function providerTestConcatString(): array { - return [ - '1 column: string param unicode' - => [function ($q) { - return [false, [$q->createNamedParameter('👍')], '👍']; - }], - '2 columns: string param and string param' - => [function ($q) { - return [false, [$q->createNamedParameter('foo'), $q->createNamedParameter('bar')], 'foobar']; - }], - '2 columns: string param and int literal' - => [function ($q) { - return [false, [$q->createNamedParameter('foo'), $q->expr()->literal(1)], 'foo1']; - }], - '2 columns: string param and string literal' - => [function ($q) { - return [false, [$q->createNamedParameter('foo'), $q->expr()->literal('bar')], 'foobar']; - }], - '2 columns: string real and int literal' - => [function ($q) { - return [true, ['configkey', $q->expr()->literal(2)], '12']; - }], - '4 columns: string literal' - => [function ($q) { - return [false, [$q->expr()->literal('foo'), $q->expr()->literal('bar'), $q->expr()->literal('foo'), $q->expr()->literal('bar')], 'foobarfoobar']; - }], - '4 columns: int literal' - => [function ($q) { - return [false, [$q->expr()->literal(1), $q->expr()->literal(2), $q->expr()->literal(3), $q->expr()->literal(4)], '1234']; - }], - '5 columns: string param with special chars used in the function' - => [function ($q) { - return [false, [$q->createNamedParameter('b'), $q->createNamedParameter("'"), $q->createNamedParameter('||'), $q->createNamedParameter(','), $q->createNamedParameter('a')], "b'||,a"]; - }], - ]; - } - - protected function clearDummyData(): void { - $delete = $this->connection->getQueryBuilder(); - - $delete->delete('appconfig') - ->where($delete->expr()->eq('appid', $delete->createNamedParameter('group_concat'))); - $delete->executeStatement(); - } - - protected function addDummyData(): void { - $this->clearDummyData(); - $insert = $this->connection->getQueryBuilder(); - - $insert->insert('appconfig') - ->setValue('appid', $insert->createNamedParameter('group_concat')) - ->setValue('configvalue', $insert->createNamedParameter('unittest')) - ->setValue('configkey', $insert->createParameter('value')); - - $insert->setParameter('value', '1'); - $insert->executeStatement(); - $insert->setParameter('value', '3'); - $insert->executeStatement(); - $insert->setParameter('value', '2'); - $insert->executeStatement(); - } - - public function testGroupConcatWithoutSeparator(): void { - $this->addDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('configkey')) - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString(',', $column); - $actual = explode(',', $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - public function testGroupConcatWithSeparator(): void { - $this->addDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('configkey', '#')) - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString('#', $column); - $actual = explode('#', $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - public function testGroupConcatWithSingleQuoteSeparator(): void { - $this->addDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('configkey', '\'')) - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString("'", $column); - $actual = explode("'", $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - public function testGroupConcatWithDoubleQuoteSeparator(): void { - $this->addDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('configkey', '"')) - ->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString('"', $column); - $actual = explode('"', $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - protected function clearIntDummyData(): void { - $delete = $this->connection->getQueryBuilder(); - - $delete->delete('systemtag') - ->where($delete->expr()->eq('name', $delete->createNamedParameter('group_concat'))); - $delete->executeStatement(); - } - - protected function addIntDummyData(): void { - $this->clearIntDummyData(); - $insert = $this->connection->getQueryBuilder(); - - $insert->insert('systemtag') - ->setValue('name', $insert->createNamedParameter('group_concat')) - ->setValue('visibility', $insert->createNamedParameter(1)) - ->setValue('editable', $insert->createParameter('value')); - - $insert->setParameter('value', 1); - $insert->executeStatement(); - $insert->setParameter('value', 2); - $insert->executeStatement(); - $insert->setParameter('value', 3); - $insert->executeStatement(); - } - - public function testIntGroupConcatWithoutSeparator(): void { - $this->addIntDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('editable')) - ->from('systemtag') - ->where($query->expr()->eq('name', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString(',', $column); - $actual = explode(',', $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - public function testIntGroupConcatWithSeparator(): void { - $this->addIntDummyData(); - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->groupConcat('editable', '#')) - ->from('systemtag') - ->where($query->expr()->eq('name', $query->createNamedParameter('group_concat'))); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertStringContainsString('#', $column); - $actual = explode('#', $column); - $this->assertEqualsCanonicalizing([1,2,3], $actual); - } - - public function testMd5(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->md5($query->createNamedParameter('foobar'))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(md5('foobar'), $column); - } - - public function testSubstring(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->substring($query->createNamedParameter('foobar'), new Literal(2), $query->createNamedParameter(2))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals('oo', $column); - } - - public function testSubstringNoLength(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->substring($query->createNamedParameter('foobar'), new Literal(2))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals('oobar', $column); - } - - public function testLower(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->lower($query->createNamedParameter('FooBar'))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals('foobar', $column); - } - - public function testAdd(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->add($query->createNamedParameter(2, IQueryBuilder::PARAM_INT), new Literal(1))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(3, $column); - } - - public function testSubtract(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->subtract($query->createNamedParameter(2, IQueryBuilder::PARAM_INT), new Literal(1))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(1, $column); - } - - public function testCount(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->count('appid')); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertGreaterThan(1, $column); - } - - public static function octetLengthProvider(): array { - return [ - ['', 0], - ['foobar', 6], - ['fé', 3], - ['šđčćž', 10], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('octetLengthProvider')] - public function testOctetLength(string $str, int $bytes): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->octetLength($query->createNamedParameter($str, IQueryBuilder::PARAM_STR))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($bytes, $column); - } - - public static function charLengthProvider(): array { - return [ - ['', 0], - ['foobar', 6], - ['fé', 2], - ['šđčćž', 5], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('charLengthProvider')] - public function testCharLength(string $str, int $bytes): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->charLength($query->createNamedParameter($str, IQueryBuilder::PARAM_STR))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $column = $result->fetchOne(); - $result->closeCursor(); - $this->assertNotNull($column); - $this->assertEquals($bytes, $column); - } - - private function setUpMinMax($value) { - $query = $this->connection->getQueryBuilder(); - - $query->insert('appconfig') - ->values([ - 'appid' => $query->createNamedParameter('minmax'), - 'configkey' => $query->createNamedParameter(uniqid()), - 'configvalue' => $query->createNamedParameter((string)$value), - ]); - $query->executeStatement(); - } - - private function clearMinMax() { - $query = $this->connection->getQueryBuilder(); - - $query->delete('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('minmax'))); - $query->executeStatement(); - } - - public function testMaxEmpty(): void { - $this->clearMinMax(); - - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->max($query->expr()->castColumn('configvalue', IQueryBuilder::PARAM_INT))); - $query->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('minmax'))) - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(null, $row); - } - - public function testMinEmpty(): void { - $this->clearMinMax(); - - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->min($query->expr()->castColumn('configvalue', IQueryBuilder::PARAM_INT))); - $query->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('minmax'))) - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(null, $row); - } - - public function testMax(): void { - $this->clearMinMax(); - $this->setUpMinMax(10); - $this->setUpMinMax(11); - $this->setUpMinMax(20); - - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->max($query->expr()->castColumn('configvalue', IQueryBuilder::PARAM_INT))); - $query->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('minmax'))) - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(20, $row); - } - - public function testMin(): void { - $this->clearMinMax(); - $this->setUpMinMax(10); - $this->setUpMinMax(11); - $this->setUpMinMax(20); - - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->min($query->expr()->castColumn('configvalue', IQueryBuilder::PARAM_INT))); - $query->from('appconfig') - ->where($query->expr()->eq('appid', $query->createNamedParameter('minmax'))) - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(10, $row); - } - - public function testGreatest(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->greatest($query->createNamedParameter(2, IQueryBuilder::PARAM_INT), new Literal(1))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(2, $row); - } - - public function testLeast(): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->least($query->createNamedParameter(2, IQueryBuilder::PARAM_INT), new Literal(1))); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals(1, $row); - } - - public static function regexSubstringData(): array { - return [ - ['foobar', 'foo', 'foo'], - ['foobar', 'b.+$', 'bar'], - ['foo#bar', 'ba.+r$', null], - ['foo#bar', 'o#.', 'o#b'], - ['a/file/path', '[^/]+$', 'path'], - ]; - } - - #[DataProvider('regexSubstringData')] - public function testRegexSubstring(string $input, string $pattern, ?string $expected): void { - $query = $this->connection->getQueryBuilder(); - - $query->select($query->func()->regexSubstring( - $query->createNamedParameter($input), - $query->createNamedParameter($pattern), - )); - $query->from('appconfig') - ->setMaxResults(1); - - $result = $query->executeQuery(); - $row = $result->fetchOne(); - $result->closeCursor(); - $this->assertEquals($expected, $row); - } -} --- a/tests/lib/DB/QueryBuilder/Partitioned/JoinConditionTest.php +++ /dev/null @@ -1,75 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace lib\DB\QueryBuilder\Partitioned; - -use OC\DB\ConnectionAdapter; -use OC\DB\QueryBuilder\Partitioned\JoinCondition; -use OC\DB\QueryBuilder\QueryBuilder; -use OC\SystemConfig; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; -use Psr\Log\LoggerInterface; -use Test\TestCase; - -class JoinConditionTest extends TestCase { - #[\Override] - protected function setUp(): void { - parent::setUp(); - } - - public static function platformProvider(): array { - return [ - [IDBConnection::PLATFORM_SQLITE], - [IDBConnection::PLATFORM_POSTGRES], - [IDBConnection::PLATFORM_MYSQL], - [IDBConnection::PLATFORM_ORACLE], - ]; - } - - private function getBuilder(string $platform): IQueryBuilder { - $connection = $this->createMock(ConnectionAdapter::class); - $connection->method('getDatabaseProvider')->willReturn($platform); - return new QueryBuilder( - $connection, - $this->createMock(SystemConfig::class), - $this->createMock(LoggerInterface::class) - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('platformProvider')] - public function testParseCondition(string $platform): void { - $query = $this->getBuilder($platform); - $param1 = $query->createNamedParameter('files'); - $param2 = $query->createNamedParameter('test'); - $condition = $query->expr()->andX( - $query->expr()->eq('tagmap.categoryid', 'tag.id'), - $query->expr()->eq('tag.type', $param1), - $query->expr()->eq('tag.uid', $param2) - ); - $parsed = JoinCondition::parse($condition, 'vcategory', 'tag', 'tagmap'); - $this->assertEquals('tagmap.categoryid', $parsed->fromColumn); - $this->assertEquals('tag.id', $parsed->toColumn); - $this->assertEquals([], $parsed->fromConditions); - $this->assertEquals([ - $query->expr()->eq('tag.type', $param1), - $query->expr()->eq('tag.uid', $param2), - ], $parsed->toConditions); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('platformProvider')] - public function testParseCastCondition(string $platform): void { - $query = $this->getBuilder($platform); - - $condition = $query->expr()->eq($query->expr()->castColumn('m.objectid', IQueryBuilder::PARAM_INT), 'f.fileid'); - $parsed = JoinCondition::parse($condition, 'filecache', 'f', 'm'); - $this->assertEquals('m.objectid', $parsed->fromColumn); - $this->assertEquals('f.fileid', $parsed->toColumn); - $this->assertEquals([], $parsed->fromConditions); - } -} --- a/tests/lib/DB/QueryBuilder/Partitioned/PartitionedQueryBuilderTest.php +++ /dev/null @@ -1,234 +0,0 @@ -markTestSkipped('Test requires 64bit'); - return; - } - $this->connection = Server::get(IDBConnection::class); - $this->shardConnectionManager = Server::get(ShardConnectionManager::class); - $this->autoIncrementHandler = Server::get(AutoIncrementHandler::class); - - $this->setupFileCache(); - } - - #[\Override] - protected function tearDown(): void { - // PHP unit also runs tearDown when the test is skipped, but we only initialized when using 64bit - // see https://github.com/sebastianbergmann/phpunit/issues/6394 - if (PHP_INT_SIZE >= 8) { - $this->cleanupDb(); - } - parent::tearDown(); - } - - private function getQueryBuilder(): PartitionedQueryBuilder { - $builder = $this->connection->getQueryBuilder(); - if ($builder instanceof PartitionedQueryBuilder) { - return $builder; - } else { - return new PartitionedQueryBuilder($builder, [], $this->shardConnectionManager, $this->autoIncrementHandler); - } - } - - private function setupFileCache(): void { - $this->cleanupDb(); - $query = $this->getQueryBuilder(); - $query->insert('storages') - ->values([ - 'numeric_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT), - 'id' => $query->createNamedParameter('test1'), - ]); - $query->executeStatement(); - - $query = $this->getQueryBuilder(); - $query->insert('filecache') - ->values([ - 'storage' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT), - 'path' => $query->createNamedParameter('file1'), - 'path_hash' => $query->createNamedParameter(md5('file1')), - ]); - $query->executeStatement(); - $fileId = $query->getLastInsertId(); - - $query = $this->getQueryBuilder(); - $query->insert('filecache_extended') - ->hintShardKey('storage', 1001001) - ->values([ - 'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT), - 'upload_time' => $query->createNamedParameter(1234, IQueryBuilder::PARAM_INT), - ]); - $query->executeStatement(); - - $query = $this->getQueryBuilder(); - $query->insert('mounts') - ->values([ - 'storage_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT), - 'user_id' => $query->createNamedParameter('partitioned_test'), - 'mount_point' => $query->createNamedParameter('/mount/point'), - 'mount_point_hash' => $query->createNamedParameter(hash('xxh128', '/mount/point')), - 'mount_provider_class' => $query->createNamedParameter('test'), - 'root_id' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT), - ]); - $query->executeStatement(); - } - - private function cleanupDb(): void { - $query = $this->getQueryBuilder(); - $query->delete('storages') - ->where($query->expr()->gt('numeric_id', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT))); - $query->executeStatement(); - - $query = $this->getQueryBuilder(); - $query->delete('filecache') - ->where($query->expr()->gt('storage', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT))) - ->runAcrossAllShards(); - $query->executeStatement(); - - $query = $this->getQueryBuilder(); - $query->delete('filecache_extended') - ->runAcrossAllShards(); - $query->executeStatement(); - - $query = $this->getQueryBuilder(); - $query->delete('mounts') - ->where($query->expr()->like('user_id', $query->createNamedParameter('partitioned_%'))); - $query->executeStatement(); - } - - public function testSimpleOnlyPartitionQuery(): void { - $builder = $this->getQueryBuilder(); - $builder->addPartition(new PartitionSplit('filecache', ['filecache'])); - - // query borrowed from UserMountCache - $query = $builder->select('path') - ->from('filecache') - ->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT))); - - $results = $query->executeQuery()->fetchAll(); - $this->assertCount(1, $results); - $this->assertEquals($results[0]['path'], 'file1'); - } - - public function testSimplePartitionedQuery(): void { - $builder = $this->getQueryBuilder(); - $builder->addPartition(new PartitionSplit('filecache', ['filecache'])); - - // query borrowed from UserMountCache - $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class') - ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT))); - - $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test'))); - - $this->assertEquals(2, $query->getPartitionCount()); - - $results = $query->executeQuery()->fetchAll(); - $this->assertCount(1, $results); - $this->assertEquals($results[0]['user_id'], 'partitioned_test'); - $this->assertEquals($results[0]['mount_point'], '/mount/point'); - $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point')); - $this->assertEquals($results[0]['mount_provider_class'], 'test'); - $this->assertEquals($results[0]['path'], 'file1'); - } - - public function testMultiTablePartitionedQuery(): void { - $builder = $this->getQueryBuilder(); - $builder->addPartition(new PartitionSplit('filecache', ['filecache', 'filecache_extended'])); - - $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class', 'fe.upload_time') - ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->innerJoin('f', 'filecache_extended', 'fe', $builder->expr()->eq('f.fileid', 'fe.fileid')) - ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT))); - - $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test'))); - - $this->assertEquals(2, $query->getPartitionCount()); - - $results = $query->executeQuery()->fetchAll(); - $this->assertCount(1, $results); - $this->assertEquals($results[0]['user_id'], 'partitioned_test'); - $this->assertEquals($results[0]['mount_point'], '/mount/point'); - $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point')); - $this->assertEquals($results[0]['mount_provider_class'], 'test'); - $this->assertEquals($results[0]['path'], 'file1'); - $this->assertEquals($results[0]['upload_time'], 1234); - } - - public function testPartitionedQueryFromSplit(): void { - $builder = $this->getQueryBuilder(); - $builder->addPartition(new PartitionSplit('filecache', ['filecache'])); - - $query = $builder->select('storage', 'm.root_id', 'm.user_id', 'm.mount_point', 'm.mount_point_hash', 'm.mount_id', 'path', 'm.mount_provider_class') - ->from('filecache', 'f') - ->innerJoin('f', 'mounts', 'm', $builder->expr()->eq('m.root_id', 'f.fileid')); - $query->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT))); - - $query->andWhere($builder->expr()->eq('m.user_id', $builder->createNamedParameter('partitioned_test'))); - - $this->assertEquals(2, $query->getPartitionCount()); - - $results = $query->executeQuery()->fetchAll(); - $this->assertCount(1, $results); - $this->assertEquals($results[0]['user_id'], 'partitioned_test'); - $this->assertEquals($results[0]['mount_point'], '/mount/point'); - $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point')); - $this->assertEquals($results[0]['mount_provider_class'], 'test'); - $this->assertEquals($results[0]['path'], 'file1'); - } - - public function testMultiJoinPartitionedQuery(): void { - $builder = $this->getQueryBuilder(); - $builder->addPartition(new PartitionSplit('filecache', ['filecache'])); - - // query borrowed from UserMountCache - $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class') - ->selectAlias('s.id', 'storage_string_id') - ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) - ->innerJoin('f', 'storages', 's', $builder->expr()->eq('f.storage', 's.numeric_id')) - ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT))); - - $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test'))); - - $this->assertEquals(3, $query->getPartitionCount()); - - $results = $query->executeQuery()->fetchAll(); - $this->assertCount(1, $results); - $this->assertEquals($results[0]['user_id'], 'partitioned_test'); - $this->assertEquals($results[0]['mount_point'], '/mount/point'); - $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point')); - $this->assertEquals($results[0]['mount_provider_class'], 'test'); - $this->assertEquals($results[0]['path'], 'file1'); - $this->assertEquals($results[0]['storage_string_id'], 'test1'); - } -} --- a/tests/lib/DB/QueryBuilder/QueryBuilderTest.php +++ /dev/null @@ -1,1452 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->config = $this->createMock(SystemConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->queryBuilder = new QueryBuilder($this->connection, $this->config, $this->logger); - } - - protected function createTestingRows($appId = 'testFirstResult') { - $qB = $this->connection->getQueryBuilder(); - for ($i = 1; $i < 10; $i++) { - $qB->insert('*PREFIX*appconfig') - ->values([ - 'appid' => $qB->expr()->literal($appId), - 'configkey' => $qB->expr()->literal('testing' . $i), - 'configvalue' => $qB->expr()->literal(100 - $i), - ]) - ->executeStatement(); - } - } - - protected function getTestingRows(QueryBuilder $queryBuilder) { - $queryBuilder->select('configvalue') - ->from('*PREFIX*appconfig') - ->where($queryBuilder->expr()->eq( - 'appid', - $queryBuilder->expr()->literal('testFirstResult') - )) - ->orderBy('configkey', 'ASC'); - - $query = $queryBuilder->executeQuery(); - $rows = []; - while ($row = $query->fetch()) { - $rows[] = $row['configvalue']; - } - $query->closeCursor(); - - return $rows; - } - - protected function deleteTestingRows($appId = 'testFirstResult') { - $qB = $this->connection->getQueryBuilder(); - - $qB->delete('*PREFIX*appconfig') - ->where($qB->expr()->eq('appid', $qB->expr()->literal($appId))) - ->executeStatement(); - } - - public static function dataFirstResult(): array { - return [ - [0, [99, 98, 97, 96, 95, 94, 93, 92, 91]], - [0, [99, 98, 97, 96, 95, 94, 93, 92, 91]], - [1, [98, 97, 96, 95, 94, 93, 92, 91]], - [5, [94, 93, 92, 91]], - ]; - } - - /** - * - * @param int|null $firstResult - * @param array $expectedSet - */ - #[DataProvider('dataFirstResult')] - public function testFirstResult($firstResult, $expectedSet): void { - $this->deleteTestingRows(); - $this->createTestingRows(); - - if ($firstResult !== null) { - $this->queryBuilder->setFirstResult($firstResult); - } - - $this->assertSame( - $firstResult ?? 0, - $this->queryBuilder->getFirstResult() - ); - - $rows = $this->getTestingRows($this->queryBuilder); - - $this->assertCount(sizeof($expectedSet), $rows); - $this->assertEquals($expectedSet, $rows); - - $this->deleteTestingRows(); - } - - public static function dataMaxResults(): array { - return [ - [null, [99, 98, 97, 96, 95, 94, 93, 92, 91]], - // Limit 0 gives mixed results: either all entries or none is returned - //[0, []], - [1, [99]], - [5, [99, 98, 97, 96, 95]], - ]; - } - - /** - * - * @param int $maxResult - * @param array $expectedSet - */ - #[DataProvider('dataMaxResults')] - public function testMaxResults($maxResult, $expectedSet): void { - $this->deleteTestingRows(); - $this->createTestingRows(); - - if ($maxResult !== null) { - $this->queryBuilder->setMaxResults($maxResult); - } - - $this->assertSame( - $maxResult, - $this->queryBuilder->getMaxResults() - ); - - $rows = $this->getTestingRows($this->queryBuilder); - - $this->assertCount(sizeof($expectedSet), $rows); - $this->assertEquals($expectedSet, $rows); - - $this->deleteTestingRows(); - } - - public static function dataSelect(): array { - return [ - // select('column1') - [['configvalue'], ['configvalue' => '99']], - - // select('column1', 'column2') - [['configvalue', 'configkey'], ['configvalue' => '99', 'configkey' => 'testing1']], - - // select(['column1', 'column2']) - [[['configvalue', 'configkey']], ['configvalue' => '99', 'configkey' => 'testing1']], - - // select(new Literal('column1')) - [['l::column1'], [], 'column1'], - - // select(new Literal('column1'), 'column2') - [['l::column1', 'configkey'], ['configkey' => 'testing1'], 'column1'], - - // select([new Literal('column1'), 'column2']) - [[['l::column1', 'configkey']], ['configkey' => 'testing1'], 'column1'], - ]; - } - - #[DataProvider('dataSelect')] - public function testSelect(array $selectArguments, array $expected, string $expectedLiteral = ''): void { - $this->deleteTestingRows(); - $this->createTestingRows(); - - array_walk_recursive( - $selectArguments, - function (string &$arg): void { - if (\str_starts_with($arg, 'l::')) { - $arg = $this->queryBuilder->expr()->literal(substr($arg, 3)); - } - }, - ); - $this->queryBuilder->select(...$selectArguments); - - $this->queryBuilder->from('*PREFIX*appconfig') - ->where($this->queryBuilder->expr()->eq( - 'appid', - $this->queryBuilder->expr()->literal('testFirstResult') - )) - ->orderBy('configkey', 'ASC') - ->setMaxResults(1); - - $query = $this->queryBuilder->executeQuery(); - $row = $query->fetch(); - $query->closeCursor(); - - foreach ($expected as $key => $value) { - $this->assertArrayHasKey($key, $row); - $this->assertEquals($value, $row[$key]); - unset($row[$key]); - } - - if ($expectedLiteral) { - $this->assertEquals([$expectedLiteral], array_values($row)); - } else { - $this->assertEmpty($row); - } - - $this->deleteTestingRows(); - } - - public static function dataSelectAlias(): array { - return [ - ['configvalue', 'cv', ['cv' => '99']], - ['l::column1', 'thing', ['thing' => 'column1']], - ]; - } - - #[DataProvider('dataSelectAlias')] - public function testSelectAlias(string $select, string $alias, array $expected): void { - if (str_starts_with($select, 'l::')) { - $select = $this->queryBuilder->expr()->literal(substr($select, 3)); - } - - $this->deleteTestingRows(); - $this->createTestingRows(); - - $this->queryBuilder->selectAlias($select, $alias); - - $this->queryBuilder->from('*PREFIX*appconfig') - ->where($this->queryBuilder->expr()->eq( - 'appid', - $this->queryBuilder->expr()->literal('testFirstResult') - )) - ->orderBy('configkey', 'ASC') - ->setMaxResults(1); - - $query = $this->queryBuilder->executeQuery(); - $row = $query->fetch(); - $query->closeCursor(); - - $this->assertEquals( - $expected, - $row - ); - - $this->deleteTestingRows(); - } - - public function testSelectDistinct(): void { - $this->deleteTestingRows('testFirstResult1'); - $this->deleteTestingRows('testFirstResult2'); - $this->createTestingRows('testFirstResult1'); - $this->createTestingRows('testFirstResult2'); - - $this->queryBuilder->selectDistinct('appid'); - - $this->queryBuilder->from('*PREFIX*appconfig') - ->where($this->queryBuilder->expr()->in( - 'appid', - [$this->queryBuilder->expr()->literal('testFirstResult1'), $this->queryBuilder->expr()->literal('testFirstResult2')] - )) - ->orderBy('appid', 'DESC'); - - $query = $this->queryBuilder->executeQuery(); - $rows = $query->fetchAll(); - $query->closeCursor(); - - $this->assertEquals( - [['appid' => 'testFirstResult2'], ['appid' => 'testFirstResult1']], - $rows - ); - - $this->deleteTestingRows('testFirstResult1'); - $this->deleteTestingRows('testFirstResult2'); - } - - public function testSelectDistinctMultiple(): void { - $this->deleteTestingRows('testFirstResult1'); - $this->deleteTestingRows('testFirstResult2'); - $this->createTestingRows('testFirstResult1'); - $this->createTestingRows('testFirstResult2'); - - $this->queryBuilder->selectDistinct(['appid', 'configkey']); - - $this->queryBuilder->from('*PREFIX*appconfig') - ->where($this->queryBuilder->expr()->eq( - 'appid', - $this->queryBuilder->expr()->literal('testFirstResult1') - )) - ->orderBy('configkey', 'ASC'); - - $query = $this->queryBuilder->executeQuery(); - $rows = $query->fetchAll(); - $query->closeCursor(); - - $this->assertEquals( - [ - ['appid' => 'testFirstResult1', 'configkey' => 'testing1'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing2'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing3'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing4'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing5'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing6'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing7'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing8'], - ['appid' => 'testFirstResult1', 'configkey' => 'testing9'], - ], - $rows - ); - - $this->deleteTestingRows('testFirstResult1'); - $this->deleteTestingRows('testFirstResult2'); - } - - public static function dataAddSelect(): array { - return [ - // addSelect('column1') - [['configvalue'], ['appid' => 'testFirstResult', 'configvalue' => '99']], - - // addSelect('column1', 'column2') - [['configvalue', 'configkey'], ['appid' => 'testFirstResult', 'configvalue' => '99', 'configkey' => 'testing1']], - - // addSelect(['column1', 'column2']) - [[['configvalue', 'configkey']], ['appid' => 'testFirstResult', 'configvalue' => '99', 'configkey' => 'testing1']], - - // select(new Literal('column1')) - [['l::column1'], ['appid' => 'testFirstResult'], 'column1'], - - // select(new Literal('column1'), 'column2') - [['l::column1', 'configkey'], ['appid' => 'testFirstResult', 'configkey' => 'testing1'], 'column1'], - - // select([new Literal('column1'), 'column2']) - [[['l::column1', 'configkey']], ['appid' => 'testFirstResult', 'configkey' => 'testing1'], 'column1'], - ]; - } - - #[DataProvider('dataAddSelect')] - public function testAddSelect(array $selectArguments, array $expected, string $expectedLiteral = ''): void { - $this->deleteTestingRows(); - $this->createTestingRows(); - - array_walk_recursive( - $selectArguments, - function (string &$arg): void { - if (\str_starts_with($arg, 'l::')) { - $arg = $this->queryBuilder->expr()->literal(substr($arg, 3)); - } - }, - ); - - $this->queryBuilder->select('appid'); - - call_user_func_array( - [$this->queryBuilder, 'addSelect'], - $selectArguments - ); - - $this->queryBuilder->from('*PREFIX*appconfig') - ->where($this->queryBuilder->expr()->eq( - 'appid', - $this->queryBuilder->expr()->literal('testFirstResult') - )) - ->orderBy('configkey', 'ASC') - ->setMaxResults(1); - - $query = $this->queryBuilder->executeQuery(); - $row = $query->fetch(); - $query->closeCursor(); - - foreach ($expected as $key => $value) { - $this->assertArrayHasKey($key, $row); - $this->assertEquals($value, $row[$key]); - unset($row[$key]); - } - - if ($expectedLiteral) { - $this->assertEquals([$expectedLiteral], array_values($row)); - } else { - $this->assertEmpty($row); - } - - $this->deleteTestingRows(); - } - - public static function dataDelete(): array { - return [ - ['data', null, ['table' => '`*PREFIX*data`', 'alias' => null], '`*PREFIX*data`'], - ['data', 't', ['table' => '`*PREFIX*data`', 'alias' => 't'], '`*PREFIX*data` t'], - ]; - } - - /** - * - * @param string $tableName - * @param string $tableAlias - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataDelete')] - public function testDelete($tableName, $tableAlias, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->delete($tableName, $tableAlias); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('from') - ); - - $this->assertSame( - 'DELETE FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataUpdate(): array { - return [ - ['data', null, ['table' => '`*PREFIX*data`', 'alias' => null], '`*PREFIX*data`'], - ['data', 't', ['table' => '`*PREFIX*data`', 'alias' => 't'], '`*PREFIX*data` t'], - ]; - } - - /** - * - * @param string $tableName - * @param string $tableAlias - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataUpdate')] - public function testUpdate($tableName, $tableAlias, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->update($tableName, $tableAlias); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('from') - ); - - $this->assertSame( - 'UPDATE ' . $expectedQuery . ' SET ', - $this->queryBuilder->getSQL() - ); - } - - public static function dataInsert(): array { - return [ - ['data', ['table' => '`*PREFIX*data`'], '`*PREFIX*data`'], - ]; - } - - /** - * - * @param string $tableName - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataInsert')] - public function testInsert($tableName, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->insert($tableName); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('from') - ); - - $this->assertSame( - 'INSERT INTO ' . $expectedQuery . ' () VALUES()', - $this->queryBuilder->getSQL() - ); - } - - public static function dataFrom(): array { - return [ - ['function', 'q', null, null, [ - ['table' => '(SELECT * FROM `*PREFIX*test`)', 'alias' => '`q`'] - ], '(SELECT * FROM `*PREFIX*test`) `q`'], - ['data', null, null, null, [['table' => '`*PREFIX*data`', 'alias' => null]], '`*PREFIX*data`'], - ['data', 't', null, null, [['table' => '`*PREFIX*data`', 'alias' => '`t`']], '`*PREFIX*data` `t`'], - ['data1', null, 'data2', null, [ - ['table' => '`*PREFIX*data1`', 'alias' => null], - ['table' => '`*PREFIX*data2`', 'alias' => null] - ], '`*PREFIX*data1`, `*PREFIX*data2`'], - ['data', 't1', 'data', 't2', [ - ['table' => '`*PREFIX*data`', 'alias' => '`t1`'], - ['table' => '`*PREFIX*data`', 'alias' => '`t2`'] - ], '`*PREFIX*data` `t1`, `*PREFIX*data` `t2`'], - ]; - } - - #[DataProvider('dataFrom')] - public function testFrom(string $table1Name, ?string $table1Alias, ?string $table2Name, ?string $table2Alias, array $expectedQueryPart, string $expectedQuery): void { - $config = $this->createMock(SystemConfig::class); - $logger = $this->createMock(LoggerInterface::class); - $queryBuilder = new QueryBuilder(Server::get(IDBConnection::class), $config, $logger); - - if ($table1Name === 'function') { - $table1Name = $queryBuilder->createFunction('(' . $queryBuilder->select('*')->from('test')->getSQL() . ')'); - } - $this->queryBuilder->from($table1Name, $table1Alias); - if ($table2Name !== null) { - $this->queryBuilder->from($table2Name, $table2Alias); - } - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('from') - ); - - $this->assertSame( - 'SELECT FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataJoin(): array { - return [ - [ - 'd1', 'data2', '', null, - ['`d1`' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` INNER JOIN `*PREFIX*data2` ' - ], - [ - 'd1', 'data2', 'd2', null, - ['`d1`' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` INNER JOIN `*PREFIX*data2` `d2`' - ], - [ - 'd1', 'data2', 'd2', '`d1`.`field1` = `d2`.`field2`', - ['`d1`' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => '`d1`.`field1` = `d2`.`field2`']]], - '`*PREFIX*data1` `d1` INNER JOIN `*PREFIX*data2` `d2` ON `d1`.`field1` = `d2`.`field2`' - ], - ]; - } - - /** - * - * @param string $fromAlias - * @param string $tableName - * @param string $tableAlias - * @param string $condition - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataJoin')] - public function testJoin($fromAlias, $tableName, $tableAlias, $condition, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->from('data1', 'd1'); - $this->queryBuilder->join( - $fromAlias, - $tableName, - $tableAlias, - $condition - ); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('join') - ); - - $this->assertSame( - 'SELECT FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - /** - * - * @param string $fromAlias - * @param string $tableName - * @param string $tableAlias - * @param string $condition - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataJoin')] - public function testInnerJoin($fromAlias, $tableName, $tableAlias, $condition, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->from('data1', 'd1'); - $this->queryBuilder->innerJoin( - $fromAlias, - $tableName, - $tableAlias, - $condition - ); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('join') - ); - - $this->assertSame( - 'SELECT FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataLeftJoin(): array { - return [ - [ - 'd1', 'data2', '', null, - ['`d1`' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` LEFT JOIN `*PREFIX*data2` ' - ], - [ - 'd1', 'data2', 'd2', null, - ['`d1`' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` LEFT JOIN `*PREFIX*data2` `d2`' - ], - [ - 'd1', 'data2', 'd2', '`d1`.`field1` = `d2`.`field2`', - ['`d1`' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => '`d1`.`field1` = `d2`.`field2`']]], - '`*PREFIX*data1` `d1` LEFT JOIN `*PREFIX*data2` `d2` ON `d1`.`field1` = `d2`.`field2`' - ], - ]; - } - - /** - * - * @param string $fromAlias - * @param string $tableName - * @param string $tableAlias - * @param string $condition - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataLeftJoin')] - public function testLeftJoin($fromAlias, $tableName, $tableAlias, $condition, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->from('data1', 'd1'); - $this->queryBuilder->leftJoin( - $fromAlias, - $tableName, - $tableAlias, - $condition - ); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('join') - ); - - $this->assertSame( - 'SELECT FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataRightJoin(): array { - return [ - [ - 'd1', 'data2', '', null, - ['`d1`' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` RIGHT JOIN `*PREFIX*data2` ' - ], - [ - 'd1', 'data2', 'd2', null, - ['`d1`' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => null]]], - '`*PREFIX*data1` `d1` RIGHT JOIN `*PREFIX*data2` `d2`' - ], - [ - 'd1', 'data2', 'd2', '`d1`.`field1` = `d2`.`field2`', - ['`d1`' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => '`d2`', 'joinCondition' => '`d1`.`field1` = `d2`.`field2`']]], - '`*PREFIX*data1` `d1` RIGHT JOIN `*PREFIX*data2` `d2` ON `d1`.`field1` = `d2`.`field2`' - ], - ]; - } - - #[DataProvider('dataRightJoin')] - public function testRightJoin( - string $fromAlias, - string $tableName, - string $tableAlias, - ?string $condition, - array $expectedQueryPart, - string $expectedQuery, - ): void { - $this->queryBuilder->from('data1', 'd1'); - $this->queryBuilder->rightJoin( - $fromAlias, - $tableName, - $tableAlias, - $condition - ); - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('join') - ); - - $this->assertSame( - 'SELECT FROM ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataSet(): array { - return [ - ['column1', new Literal('value'), null, null, ['`column1` = value'], '`column1` = value'], - ['column1', new Parameter(':param'), null, null, ['`column1` = :param'], '`column1` = :param'], - ['column1', 'column2', null, null, ['`column1` = `column2`'], '`column1` = `column2`'], - ['column1', 'column2', 'column3', new Literal('value'), ['`column1` = `column2`', '`column3` = value'], '`column1` = `column2`, `column3` = value'], - ]; - } - - /** - * - * @param string $partOne1 - * @param string $partOne2 - * @param string $partTwo1 - * @param string $partTwo2 - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataSet')] - public function testSet($partOne1, $partOne2, $partTwo1, $partTwo2, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->update('data'); - $this->queryBuilder->set($partOne1, $partOne2); - if ($partTwo1 !== null) { - $this->queryBuilder->set($partTwo1, $partTwo2); - } - - $this->assertSame( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('set') - ); - - $this->assertSame( - 'UPDATE `*PREFIX*data` SET ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataWhere(): array { - return [ - [['where1'], new CompositeExpression('AND', ['where1']), 'where1'], - [['where1', 'where2'], new CompositeExpression('AND', ['where1', 'where2']), '(where1) AND (where2)'], - ]; - } - - /** - * - * @param array $whereArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataWhere')] - public function testWhere($whereArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->select('column'); - call_user_func_array( - [$this->queryBuilder, 'where'], - $whereArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('where') - ); - - $this->assertSame( - 'SELECT `column` WHERE ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - /** - * - * @param array $whereArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataWhere')] - public function testAndWhere($whereArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->select('column'); - call_user_func_array( - [$this->queryBuilder, 'andWhere'], - $whereArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('where') - ); - - $this->assertSame( - 'SELECT `column` WHERE ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataOrWhere(): array { - return [ - [['where1'], new CompositeExpression('OR', ['where1']), 'where1'], - [['where1', 'where2'], new CompositeExpression('OR', ['where1', 'where2']), '(where1) OR (where2)'], - ]; - } - - /** - * - * @param array $whereArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataOrWhere')] - public function testOrWhere($whereArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->select('column'); - call_user_func_array( - [$this->queryBuilder, 'orWhere'], - $whereArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('where') - ); - - $this->assertSame( - 'SELECT `column` WHERE ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataGroupBy(): array { - return [ - [['column1'], ['`column1`'], '`column1`'], - [['column1', 'column2'], ['`column1`', '`column2`'], '`column1`, `column2`'], - ]; - } - - /** - * - * @param array $groupByArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataGroupBy')] - public function testGroupBy($groupByArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->select('column'); - call_user_func_array( - [$this->queryBuilder, 'groupBy'], - $groupByArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('groupBy') - ); - - $this->assertSame( - 'SELECT `column` GROUP BY ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataAddGroupBy(): array { - return [ - [['column2'], ['`column1`', '`column2`'], '`column1`, `column2`'], - [['column2', 'column3'], ['`column1`', '`column2`', '`column3`'], '`column1`, `column2`, `column3`'], - ]; - } - - /** - * - * @param array $groupByArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataAddGroupBy')] - public function testAddGroupBy($groupByArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->select('column'); - $this->queryBuilder->groupBy('column1'); - call_user_func_array( - [$this->queryBuilder, 'addGroupBy'], - $groupByArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('groupBy') - ); - - $this->assertSame( - 'SELECT `column` GROUP BY ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataSetValue(): array { - return [ - ['column', 'value', ['`column`' => 'value'], '(`column`) VALUES(value)'], - ]; - } - - /** - * - * @param string $column - * @param string $value - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataSetValue')] - public function testSetValue($column, $value, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->insert('data'); - $this->queryBuilder->setValue($column, $value); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('values') - ); - - $this->assertSame( - 'INSERT INTO `*PREFIX*data` ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - /** - * - * @param string $column - * @param string $value - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataSetValue')] - public function testValues($column, $value, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->insert('data'); - $this->queryBuilder->values([ - $column => $value, - ]); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('values') - ); - - $this->assertSame( - 'INSERT INTO `*PREFIX*data` ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataHaving(): array { - return [ - [['condition1'], new CompositeExpression('AND', ['condition1']), 'HAVING condition1'], - [['condition1', 'condition2'], new CompositeExpression('AND', ['condition1', 'condition2']), 'HAVING (condition1) AND (condition2)'], - [ - [new CompositeExpression('OR', ['condition1', 'condition2'])], - new CompositeExpression('OR', ['condition1', 'condition2']), - 'HAVING (condition1) OR (condition2)' - ], - [ - [new CompositeExpression('AND', ['condition1', 'condition2'])], - new CompositeExpression('AND', ['condition1', 'condition2']), - 'HAVING (condition1) AND (condition2)' - ], - ]; - } - - /** - * - * @param array $havingArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataHaving')] - public function testHaving($havingArguments, $expectedQueryPart, $expectedQuery): void { - call_user_func_array( - [$this->queryBuilder, 'having'], - $havingArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('having') - ); - - $this->assertSame( - 'SELECT ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataAndHaving(): array { - return [ - [['condition2'], new CompositeExpression('AND', ['condition1', 'condition2']), 'HAVING (condition1) AND (condition2)'], - [['condition2', 'condition3'], new CompositeExpression('AND', ['condition1', 'condition2', 'condition3']), 'HAVING (condition1) AND (condition2) AND (condition3)'], - [ - [new CompositeExpression('OR', ['condition2', 'condition3'])], - new CompositeExpression('AND', ['condition1', new CompositeExpression('OR', ['condition2', 'condition3'])]), - 'HAVING (condition1) AND ((condition2) OR (condition3))' - ], - [ - [new CompositeExpression('AND', ['condition2', 'condition3'])], - new CompositeExpression('AND', ['condition1', new CompositeExpression('AND', ['condition2', 'condition3'])]), - 'HAVING (condition1) AND ((condition2) AND (condition3))' - ], - ]; - } - - /** - * - * @param array $havingArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataAndHaving')] - public function testAndHaving($havingArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->having('condition1'); - call_user_func_array( - [$this->queryBuilder, 'andHaving'], - $havingArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('having') - ); - - $this->assertSame( - 'SELECT ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataOrHaving(): array { - return [ - [['condition2'], new CompositeExpression('OR', ['condition1', 'condition2']), 'HAVING (condition1) OR (condition2)'], - [['condition2', 'condition3'], new CompositeExpression('OR', ['condition1', 'condition2', 'condition3']), 'HAVING (condition1) OR (condition2) OR (condition3)'], - [ - [new CompositeExpression('OR', ['condition2', 'condition3'])], - new CompositeExpression('OR', ['condition1', new CompositeExpression('OR', ['condition2', 'condition3'])]), - 'HAVING (condition1) OR ((condition2) OR (condition3))' - ], - [ - [new CompositeExpression('AND', ['condition2', 'condition3'])], - new CompositeExpression('OR', ['condition1', new CompositeExpression('AND', ['condition2', 'condition3'])]), - 'HAVING (condition1) OR ((condition2) AND (condition3))' - ], - ]; - } - - /** - * - * @param array $havingArguments - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataOrHaving')] - public function testOrHaving($havingArguments, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->having('condition1'); - call_user_func_array( - [$this->queryBuilder, 'orHaving'], - $havingArguments - ); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('having') - ); - - $this->assertSame( - 'SELECT ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataOrderBy(): array { - return [ - ['column', null, ['`column` ASC'], 'ORDER BY `column` ASC'], - ['column', 'ASC', ['`column` ASC'], 'ORDER BY `column` ASC'], - ['column', 'DESC', ['`column` DESC'], 'ORDER BY `column` DESC'], - ]; - } - - /** - * - * @param string $sort - * @param string $order - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataOrderBy')] - public function testOrderBy($sort, $order, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->orderBy($sort, $order); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('orderBy') - ); - - $this->assertSame( - 'SELECT ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public static function dataAddOrderBy(): array { - return [ - ['column2', null, null, ['`column1` ASC', '`column2` ASC'], 'ORDER BY `column1` ASC, `column2` ASC'], - ['column2', null, 'ASC', ['`column1` ASC', '`column2` ASC'], 'ORDER BY `column1` ASC, `column2` ASC'], - ['column2', null, 'DESC', ['`column1` DESC', '`column2` ASC'], 'ORDER BY `column1` DESC, `column2` ASC'], - ['column2', 'ASC', null, ['`column1` ASC', '`column2` ASC'], 'ORDER BY `column1` ASC, `column2` ASC'], - ['column2', 'ASC', 'ASC', ['`column1` ASC', '`column2` ASC'], 'ORDER BY `column1` ASC, `column2` ASC'], - ['column2', 'ASC', 'DESC', ['`column1` DESC', '`column2` ASC'], 'ORDER BY `column1` DESC, `column2` ASC'], - ['column2', 'DESC', null, ['`column1` ASC', '`column2` DESC'], 'ORDER BY `column1` ASC, `column2` DESC'], - ['column2', 'DESC', 'ASC', ['`column1` ASC', '`column2` DESC'], 'ORDER BY `column1` ASC, `column2` DESC'], - ['column2', 'DESC', 'DESC', ['`column1` DESC', '`column2` DESC'], 'ORDER BY `column1` DESC, `column2` DESC'], - ]; - } - - /** - * - * @param string $sort2 - * @param string $order2 - * @param string $order1 - * @param array $expectedQueryPart - * @param string $expectedQuery - */ - #[DataProvider('dataAddOrderBy')] - public function testAddOrderBy($sort2, $order2, $order1, $expectedQueryPart, $expectedQuery): void { - $this->queryBuilder->orderBy('column1', $order1); - $this->queryBuilder->addOrderBy($sort2, $order2); - - $this->assertEquals( - $expectedQueryPart, - $this->queryBuilder->getQueryPart('orderBy') - ); - - $this->assertSame( - 'SELECT ' . $expectedQuery, - $this->queryBuilder->getSQL() - ); - } - - public function testGetLastInsertId(): void { - $qB = $this->connection->getQueryBuilder(); - - try { - $qB->getLastInsertId(); - $this->fail('getLastInsertId() should throw an exception, when being called before insert()'); - } catch (\BadMethodCallException $e) { - $this->addToAssertionCount(1); - } - - $qB->insert('properties') - ->values([ - 'userid' => $qB->expr()->literal('testFirstResult'), - 'propertypath' => $qB->expr()->literal('testing'), - 'propertyname' => $qB->expr()->literal('testing'), - 'propertyvalue' => $qB->expr()->literal('testing'), - ]) - ->executeStatement(); - - $actual = $qB->getLastInsertId(); - - $this->assertNotNull($actual); - $this->assertIsInt($actual); - $this->assertEquals($this->connection->lastInsertId('*PREFIX*properties'), $actual); - - $qB->delete('properties') - ->where($qB->expr()->eq('userid', $qB->expr()->literal('testFirstResult'))) - ->executeStatement(); - - try { - $qB->getLastInsertId(); - $this->fail('getLastInsertId() should throw an exception, when being called after delete()'); - } catch (\BadMethodCallException $e) { - $this->addToAssertionCount(1); - } - } - - public static function dataGetTableName(): array { - return [ - ['*PREFIX*table', null, '`*PREFIX*table`'], - ['*PREFIX*table', true, '`*PREFIX*table`'], - ['*PREFIX*table', false, '`*PREFIX*table`'], - - ['table', null, '`*PREFIX*table`'], - ['table', true, '`*PREFIX*table`'], - ['table', false, '`table`'], - - ['function', null, '(SELECT * FROM `*PREFIX*table`)'], - ['function', true, '(SELECT * FROM `*PREFIX*table`)'], - ['function', false, '(SELECT * FROM `*PREFIX*table`)'], - ]; - } - - #[DataProvider('dataGetTableName')] - public function testGetTableName(string $tableName, ?bool $automatic, string $expected): void { - if ($tableName === 'function') { - $tableName = $this->queryBuilder->createFunction('(' . $this->queryBuilder->select('*')->from('table')->getSQL() . ')'); - } - - if ($automatic !== null) { - $this->queryBuilder->automaticTablePrefix($automatic); - } - - $this->assertSame( - $expected, - $this->queryBuilder->getTableName($tableName) - ); - } - - public static function dataGetColumnName(): array { - return [ - ['column', '', '`column`'], - ['column', 'a', '`a`.`column`'], - ]; - } - - #[DataProvider('dataGetColumnName')] - public function testGetColumnName(string $column, string $prefix, string $expected): void { - $this->assertSame( - $expected, - $this->queryBuilder->getColumnName($column, $prefix) - ); - } - - private function getConnection(): MockObject&ConnectionAdapter { - $connection = $this->createMock(ConnectionAdapter::class); - $connection->method('executeStatement') - ->willReturn(3); - $connection->method('executeQuery') - ->willReturn($this->createMock(IResult::class)); - return $connection; - } - - public function testExecuteWithoutLogger(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $queryBuilder - ->method('getSQL') - ->willReturn(''); - $queryBuilder - ->method('getParameters') - ->willReturn([]); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([]); - $this->logger - ->expects($this->never()) - ->method('debug'); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(false); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->assertEquals(3, $this->queryBuilder->executeStatement()); - } - - public function testExecuteWithLoggerAndNamedArray(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $queryBuilder - ->expects($this->any()) - ->method('getParameters') - ->willReturn([ - 'foo' => 'bar', - 'key' => 'value', - ]); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([ - 'foo' => IQueryBuilder::PARAM_STR, - 'key' => IQueryBuilder::PARAM_STR, - ]); - $queryBuilder - ->expects($this->any()) - ->method('getSQL') - ->willReturn('UPDATE FOO SET bar = 1 WHERE BAR = ?'); - $this->logger - ->expects($this->once()) - ->method('debug') - ->with( - 'DB QueryBuilder: \'{query}\' with parameters: {params}', - [ - 'query' => 'UPDATE FOO SET bar = 1 WHERE BAR = ?', - 'params' => 'foo => \'bar\', key => \'value\'', - 'app' => 'core', - ] - ); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(true); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->assertEquals(3, $this->queryBuilder->executeStatement()); - } - - public function testExecuteWithLoggerAndUnnamedArray(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $queryBuilder - ->expects($this->any()) - ->method('getParameters') - ->willReturn(['Bar']); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([IQueryBuilder::PARAM_STR]); - $queryBuilder - ->expects($this->any()) - ->method('getSQL') - ->willReturn('UPDATE FOO SET bar = false WHERE BAR = ?'); - $this->logger - ->expects($this->once()) - ->method('debug') - ->with( - 'DB QueryBuilder: \'{query}\' with parameters: {params}', - [ - 'query' => 'UPDATE FOO SET bar = false WHERE BAR = ?', - 'params' => '0 => \'Bar\'', - 'app' => 'core', - ] - ); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(true); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->assertEquals(3, $this->queryBuilder->executeStatement()); - } - - public function testExecuteWithLoggerAndNoParams(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $queryBuilder - ->expects($this->any()) - ->method('getParameters') - ->willReturn([]); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([]); - $queryBuilder - ->expects($this->any()) - ->method('getSQL') - ->willReturn('UPDATE FOO SET bar = false WHERE BAR = ?'); - $this->logger - ->expects($this->once()) - ->method('debug') - ->with( - 'DB QueryBuilder: \'{query}\'', - [ - 'query' => 'UPDATE FOO SET bar = false WHERE BAR = ?', - 'app' => 'core', - ] - ); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(true); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->assertEquals(3, $this->queryBuilder->executeStatement()); - } - - public function testExecuteWithParameterTooLarge(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $p = array_fill(0, 1001, 'foo'); - $queryBuilder - ->expects($this->any()) - ->method('getParameters') - ->willReturn([$p]); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([IQueryBuilder::PARAM_STR_ARRAY]); - $queryBuilder - ->expects($this->any()) - ->method('getType') - ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::SELECT); - $queryBuilder - ->expects($this->any()) - ->method('getSQL') - ->willReturn('SELECT * FROM FOO WHERE BAR IN (?)'); - $this->logger - ->expects($this->once()) - ->method('error') - ->willReturnCallback(function ($message, $parameters): void { - $this->assertInstanceOf(QueryException::class, $parameters['exception']); - $this->assertSame( - 'More than 1000 expressions in a list are not allowed on Oracle.', - $message - ); - }); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(false); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->queryBuilder->executeQuery(); - } - - public function testExecuteWithParametersTooMany(): void { - $queryBuilder = $this->createMock(\Doctrine\DBAL\Query\QueryBuilder::class); - $p = array_fill(0, 999, 'foo'); - $queryBuilder - ->expects($this->any()) - ->method('getParameters') - ->willReturn(array_fill(0, 66, $p)); - $queryBuilder - ->method('getParameterTypes') - ->willReturn([IQueryBuilder::PARAM_STR_ARRAY]); - $queryBuilder - ->expects($this->any()) - ->method('getType') - ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::SELECT); - $queryBuilder - ->expects($this->any()) - ->method('getSQL') - ->willReturn('SELECT * FROM FOO WHERE BAR IN (?) OR BAR IN (?)'); - $this->logger - ->expects($this->once()) - ->method('error') - ->willReturnCallback(function ($message, $parameters): void { - $this->assertInstanceOf(QueryException::class, $parameters['exception']); - $this->assertSame( - 'The number of parameters must not exceed 65535. Restriction by PostgreSQL.', - $message - ); - }); - $this->config - ->expects($this->once()) - ->method('getValue') - ->with('log_query', false) - ->willReturn(false); - - $this->invokePrivate($this->queryBuilder, 'queryBuilder', [$queryBuilder]); - $this->invokePrivate($this->queryBuilder, 'connection', [$this->getConnection()]); - $this->queryBuilder->executeQuery(); - } -} --- a/tests/lib/DB/QueryBuilder/QuoteHelperTest.php +++ /dev/null @@ -1,133 +0,0 @@ -helper = new QuoteHelper(); - } - - public static function dataQuoteColumnName(): array { - return [ - ['column', '`column`'], - [new Literal('literal'), 'literal'], - [new Literal(1), '1'], - [new Parameter(':param'), ':param'], - - // (string) 'null' is Doctrines way to set columns to null - // See https://github.com/owncloud/core/issues/19314 - ['null', 'null'], - ]; - } - - /** - * @param mixed $input - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataQuoteColumnName')] - public function testQuoteColumnName($input, $expected): void { - $this->assertSame( - $expected, - $this->helper->quoteColumnName($input) - ); - } - - public static function dataQuoteColumnNames(): array { - return [ - // Single case - ['d.column', '`d`.`column`'], - ['column', '`column`'], - [new Literal('literal'), 'literal'], - [new Literal(1), '1'], - [new Parameter(':param'), ':param'], - - // Array case - [['column'], ['`column`']], - [[new Literal('literal')], ['literal']], - [[new Literal(1)], ['1']], - [[new Parameter(':param')], [':param']], - - // Array mixed cases - [['column1', 'column2'], ['`column1`', '`column2`']], - [['column', new Literal('literal')], ['`column`', 'literal']], - [['column', new Literal(1)], ['`column`', '1']], - [['column', new Parameter(':param')], ['`column`', ':param']], - ]; - } - - /** - * @param mixed $input - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataQuoteColumnNames')] - public function testQuoteColumnNames($input, $expected): void { - $this->assertSame( - $expected, - $this->helper->quoteColumnNames($input) - ); - } - - /** - * @param array|string|ILiteral|IParameter $strings string, Literal or Parameter - * @return array|string - */ - public function quoteColumnNames($strings) { - if (!is_array($strings)) { - return $this->quoteColumnName($strings); - } - - $return = []; - foreach ($strings as $string) { - $return[] = $this->quoteColumnName($string); - } - - return $return; - } - - /** - * @param string|ILiteral|IParameter $string string, Literal or Parameter - * @return string - */ - public function quoteColumnName($string) { - if ($string instanceof IParameter) { - return $string->getName(); - } - - if ($string instanceof ILiteral) { - return $string->getLiteral(); - } - - if ($string === null) { - return $string; - } - - if (!is_string($string)) { - throw new \InvalidArgumentException('Only strings, Literals and Parameters are allowed'); - } - - if (substr_count($string, '.')) { - [$alias, $columnName] = explode('.', $string); - return '`' . $alias . '`.`' . $columnName . '`'; - } - - return '`' . $string . '`'; - } -} --- a/tests/lib/DB/QueryBuilder/Sharded/SharedQueryBuilderTest.php +++ /dev/null @@ -1,128 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace Test\DB\QueryBuilder\Sharded; - -use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler; -use OC\DB\QueryBuilder\Sharded\InvalidShardedQueryException; -use OC\DB\QueryBuilder\Sharded\RoundRobinShardMapper; -use OC\DB\QueryBuilder\Sharded\ShardConnectionManager; -use OC\DB\QueryBuilder\Sharded\ShardDefinition; -use OC\DB\QueryBuilder\Sharded\ShardedQueryBuilder; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; -use OCP\Server; -use Test\TestCase; - -#[\PHPUnit\Framework\Attributes\Group('DB')] -class SharedQueryBuilderTest extends TestCase { - private IDBConnection $connection; - private AutoIncrementHandler $autoIncrementHandler; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - if (PHP_INT_SIZE < 8) { - $this->markTestSkipped('Test requires 64bit'); - } - $this->connection = Server::get(IDBConnection::class); - $this->autoIncrementHandler = Server::get(AutoIncrementHandler::class); - } - - private function getQueryBuilder(string $table, string $shardColumn, string $primaryColumn, array $companionTables = []): ShardedQueryBuilder { - return new ShardedQueryBuilder( - $this->connection->getQueryBuilder(), - [ - new ShardDefinition($table, $primaryColumn, [], $shardColumn, new RoundRobinShardMapper(), $companionTables, [], 0, 0), - ], - $this->createMock(ShardConnectionManager::class), - $this->autoIncrementHandler, - ); - } - - public function testGetShardKeySingleParam(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->eq('storage', $query->createNamedParameter(10, IQueryBuilder::PARAM_INT))); - - $this->assertEquals([], $query->getPrimaryKeys()); - $this->assertEquals([10], $query->getShardKeys()); - } - - public function testGetPrimaryKeyParam(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->in('fileid', $query->createNamedParameter([10, 11], IQueryBuilder::PARAM_INT))); - - $this->assertEquals([10, 11], $query->getPrimaryKeys()); - $this->assertEquals([], $query->getShardKeys()); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testValidateWithShardKey(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->eq('storage', $query->createNamedParameter(10))); - - $query->validate(); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testValidateWithPrimaryKey(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->in('fileid', $query->createNamedParameter([10, 11], IQueryBuilder::PARAM_INT))); - - $query->validate(); - } - - public function testValidateWithNoKey(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->lt('size', $query->createNamedParameter(0))); - - $this->expectException(InvalidShardedQueryException::class); - $query->validate(); - $this->fail('exception expected'); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testValidateNonSharedTable(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('configvalue') - ->from('appconfig') - ->where($query->expr()->eq('configkey', $query->createNamedParameter('test'))); - - $query->validate(); - } - - public function testGetShardKeyMultipleSingleParam(): void { - $query = $this->getQueryBuilder('filecache', 'storage', 'fileid'); - $query->select('fileid', 'path') - ->from('filecache') - ->where($query->expr()->andX( - $query->expr()->gt('mtime', $query->createNamedParameter(0), IQueryBuilder::PARAM_INT), - $query->expr()->orX( - $query->expr()->eq('storage', $query->createNamedParameter(10, IQueryBuilder::PARAM_INT)), - $query->expr()->andX( - $query->expr()->eq('storage', $query->createNamedParameter(11, IQueryBuilder::PARAM_INT)), - $query->expr()->like('path', $query->createNamedParameter('foo/%')) - ) - ) - )); - - $this->assertEquals([], $query->getPrimaryKeys()); - $this->assertEquals([10, 11], $query->getShardKeys()); - } -} --- a/tests/lib/DB/SchemaCheckerTest.php +++ /dev/null @@ -1,164 +0,0 @@ -connection = $this->createMock(Connection::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->schemaChecker = new SchemaChecker( - $this->connection, - $this->appConfig, - $this->appManager, - $this->eventDispatcher, - $this->logger, - ); - } - - public static function dataFormatFinding(): array { - return [ - 'missing_table' => [['table' => 'oc_foo', 'type' => 'missing_table'], "missing table 'oc_foo'"], - 'unexpected_table' => [['table' => 'oc_foo', 'type' => 'unexpected_table'], "unexpected table 'oc_foo'"], - 'missing_column' => [['table' => 'oc_foo', 'type' => 'missing_column', 'name' => 'bar'], "oc_foo: missing column 'bar'"], - 'unexpected_column' => [['table' => 'oc_foo', 'type' => 'unexpected_column', 'name' => 'bar'], "oc_foo: unexpected column 'bar'"], - 'modified_column' => [['table' => 'oc_foo', 'type' => 'modified_column', 'name' => 'bar', 'changes' => ['type', 'default']], "oc_foo: column 'bar' differs in: type, default"], - 'missing_index' => [['table' => 'oc_foo', 'type' => 'missing_index', 'name' => 'bar_idx'], "oc_foo: missing index 'bar_idx'"], - 'unexpected_index' => [['table' => 'oc_foo', 'type' => 'unexpected_index', 'name' => 'bar_idx'], "oc_foo: unexpected index 'bar_idx'"], - 'unknown' => [['table' => 'oc_foo', 'type' => 'something_else'], "oc_foo: unknown finding 'something_else'"], - ]; - } - - #[DataProvider('dataFormatFinding')] - public function testFormatFinding(array $finding, string $expected): void { - $this->assertSame($expected, $this->schemaChecker->formatFinding($finding)); - } - - public function testPartitionFindingsSplitsBlockingAndDisabled(): void { - $blockingFinding = ['table' => 'oc_foo', 'type' => 'missing_column', 'name' => 'a', 'app' => 'core', 'enabled' => true]; - $disabledAppFinding = ['table' => 'oc_bar', 'type' => 'missing_column', 'name' => 'b', 'app' => 'files', 'enabled' => false]; - $unattributedFinding = ['table' => 'oc_baz', 'type' => 'unexpected_table', 'app' => null, 'enabled' => false]; - - $result = $this->schemaChecker->partitionFindings([ - $blockingFinding, - $disabledAppFinding, - $unattributedFinding, - ]); - - $this->assertSame([$blockingFinding], $result['blocking']); - $this->assertSame(['files' => [$disabledAppFinding]], array_intersect_key($result['byDisabledApp'], ['files' => true])); - $this->assertSame(['(unknown app)' => [$unattributedFinding]], array_intersect_key($result['byDisabledApp'], ['(unknown app)' => true])); - } - - public function testNormalizeLongStringColumnsRewritesOnlyColumnsOverTheLimit(): void { - $schema = new Schema(); - $table = $schema->createTable('oc_test'); - $table->addColumn('short_col', Types::STRING, ['length' => 255]); - $table->addColumn('long_col', Types::STRING, ['length' => 4001]); - - self::invokePrivate($this->schemaChecker, 'normalizeLongStringColumns', [$schema]); - - $this->assertSame(Types::STRING, Type::getTypeRegistry()->lookupName($table->getColumn('short_col')->getType())); - $this->assertSame(255, $table->getColumn('short_col')->getLength()); - - $this->assertInstanceOf(TextType::class, $table->getColumn('long_col')->getType()); - $this->assertNull($table->getColumn('long_col')->getLength()); - } - - public static function dataIsIgnorableTextDefaultDiff(): array { - return [ - 'mysql text' => [IDBConnection::PLATFORM_MYSQL, Types::TEXT, true], - 'mariadb blob' => [IDBConnection::PLATFORM_MARIADB, Types::BLOB, true], - 'mysql string is not ignorable' => [IDBConnection::PLATFORM_MYSQL, Types::STRING, false], - 'sqlite text is not ignorable' => [IDBConnection::PLATFORM_SQLITE, Types::TEXT, false], - ]; - } - - #[DataProvider('dataIsIgnorableTextDefaultDiff')] - public function testIsIgnorableTextDefaultDiff(string $provider, string $typeName, bool $expected): void { - $this->connection->method('getDatabaseProvider')->willReturn($provider); - - $column = new Column('some_col', Type::getType($typeName)); - $columnDiff = new ColumnDiff('some_col', $column, ['default'], $column); - - $result = self::invokePrivate($this->schemaChecker, 'isIgnorableTextDefaultDiff', [$columnDiff]); - - $this->assertSame($expected, $result); - } - - public function testGetOptionalIndexNamesCollectsMissingAndReplacedIndices(): void { - $this->connection->method('getPrefix')->willReturn('oc_'); - $this->eventDispatcher->method('dispatchTyped') - ->willReturnCallback(function (AddMissingIndicesEvent $event): void { - $event->addMissingIndex('foo', 'foo_idx', ['col']); - $event->replaceIndex('bar', ['old_idx'], 'new_idx', ['col'], false); - }); - - $names = self::invokePrivate($this->schemaChecker, 'getOptionalIndexNames'); - - $this->assertSame([ - 'oc_foo' => ['foo_idx' => true], - 'oc_bar' => ['new_idx' => true, 'old_idx' => true], - ], $names); - } - - public static function dataIsOptionalIndexFinding(): array { - $optionalIndexNames = ['oc_foo' => ['foo_idx' => true]]; - - return [ - 'matching missing_index' => [['table' => 'oc_foo', 'type' => 'missing_index', 'name' => 'foo_idx'], $optionalIndexNames, true], - 'matching unexpected_index' => [['table' => 'oc_foo', 'type' => 'unexpected_index', 'name' => 'foo_idx'], $optionalIndexNames, true], - 'different index name' => [['table' => 'oc_foo', 'type' => 'missing_index', 'name' => 'other_idx'], $optionalIndexNames, false], - 'different table' => [['table' => 'oc_bar', 'type' => 'missing_index', 'name' => 'foo_idx'], $optionalIndexNames, false], - 'non-index finding type' => [['table' => 'oc_foo', 'type' => 'missing_column', 'name' => 'foo_idx'], $optionalIndexNames, false], - ]; - } - - /** - * Optional-index findings must be filtered out entirely (never reach - * partitionFindings() or --output=json), not just hidden from plain-text - * output - Settings already has a dedicated admin-overview surface for them. - */ - #[DataProvider('dataIsOptionalIndexFinding')] - public function testIsOptionalIndexFinding(array $finding, array $optionalIndexNames, bool $expected): void { - $result = self::invokePrivate($this->schemaChecker, 'isOptionalIndexFinding', [$finding, $optionalIndexNames]); - - $this->assertSame($expected, $result); - } -} --- a/tests/lib/DateTimeFormatterTest.php +++ /dev/null @@ -1,179 +0,0 @@ -formatter = new DateTimeFormatter(new \DateTimeZone('UTC'), Util::getL10N('lib', 'en')); - } - - protected static function getTimestampAgo($time, $seconds = 0, $minutes = 0, $hours = 0, $days = 0, $years = 0) { - return $time - $seconds - $minutes * 60 - $hours * 3600 - $days * 24 * 3600 - $years * 365 * 24 * 3600; - } - - public static function formatTimeSpanData(): array { - $time = 1416916800; // Use a fixed timestamp so we don't switch days/years with the getTimestampAgo - $deL10N = Util::getL10N('lib', 'de'); - return [ - ['seconds ago', $time, $time], - ['in a few seconds', $time + 5 , $time], - ['1 minute ago', self::getTimestampAgo($time, 30, 1), $time], - ['15 minutes ago', self::getTimestampAgo($time, 30, 15), $time], - ['in 15 minutes', $time, self::getTimestampAgo($time, 30, 15)], - ['1 hour ago', self::getTimestampAgo($time, 30, 15, 1), $time], - ['3 hours ago', self::getTimestampAgo($time, 30, 15, 3), $time], - ['in 3 hours', $time, self::getTimestampAgo($time, 30, 15, 3)], - ['4 days ago', self::getTimestampAgo($time, 30, 15, 3, 4), $time], - - ['seconds ago', new \DateTime('Wed, 02 Oct 2013 23:59:58 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['seconds ago', new \DateTime('Wed, 02 Oct 2013 23:59:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['1 minute ago', new \DateTime('Wed, 02 Oct 2013 23:58:30 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['3 minutes ago', new \DateTime('Wed, 02 Oct 2013 23:56:30 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['59 minutes ago', new \DateTime('Wed, 02 Oct 2013 23:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['1 hour ago', new \DateTime('Wed, 02 Oct 2013 22:59:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['3 hours ago', new \DateTime('Wed, 02 Oct 2013 20:39:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['yesterday', new \DateTime('Tue, 01 Oct 2013 20:39:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['2 days ago', new \DateTime('Mon, 30 Sep 2013 20:39:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - - [$deL10N->t('seconds ago'), new \DateTime('Wed, 02 Oct 2013 23:59:58 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->n('%n minute ago', '%n minutes ago', 1), new \DateTime('Wed, 02 Oct 2013 23:58:30 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->n('%n minute ago', '%n minutes ago', 3), new \DateTime('Wed, 02 Oct 2013 23:56:30 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->n('%n hour ago', '%n hours ago', 1), new \DateTime('Wed, 02 Oct 2013 22:59:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->n('%n hour ago', '%n hours ago', 3), new \DateTime('Wed, 02 Oct 2013 20:39:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->n('%n day ago', '%n days ago', 2), new \DateTime('Mon, 30 Sep 2013 20:39:59 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('formatTimeSpanData')] - public function testFormatTimeSpan($expected, $timestamp, $compare, $locale = null): void { - $this->assertEquals((string)$expected, (string)$this->formatter->formatTimeSpan($timestamp, $compare, $locale)); - } - - public static function formatDateSpanData(): array { - $time = 1416916800; // Use a fixed timestamp so we don't switch days/years with the getTimestampAgo - $deL10N = Util::getL10N('lib', 'de'); - return [ - // Normal testing - ['today', self::getTimestampAgo($time, 30, 15), $time], - ['yesterday', self::getTimestampAgo($time, 0, 0, 0, 1), $time], - ['tomorrow', $time, self::getTimestampAgo($time, 0, 0, 0, 1)], - ['4 days ago', self::getTimestampAgo($time, 0, 0, 0, 4), $time], - ['in 4 days', $time, self::getTimestampAgo($time, 0, 0, 0, 4)], - ['5 months ago', self::getTimestampAgo($time, 0, 0, 0, 155), $time], - ['next month', $time, self::getTimestampAgo($time, 0, 0, 0, 32)], - ['in 5 months', $time, self::getTimestampAgo($time, 0, 0, 0, 155)], - ['2 years ago', self::getTimestampAgo($time, 0, 0, 0, 0, 2), $time], - ['next year', $time, self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - ['in 2 years', $time, self::getTimestampAgo($time, 0, 0, 0, 0, 2)], - - // Test with compare timestamp - ['today', self::getTimestampAgo($time, 0, 0, 0, 0, 1), self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - ['yesterday', self::getTimestampAgo($time, 30, 15, 3, 1, 1), self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - ['4 days ago', self::getTimestampAgo($time, 30, 15, 3, 4, 1), self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - ['5 months ago', self::getTimestampAgo($time, 30, 15, 3, 155, 1), self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - ['2 years ago', self::getTimestampAgo($time, 30, 15, 3, 35, 3), self::getTimestampAgo($time, 0, 0, 0, 0, 1)], - - // Test translations - [$deL10N->t('today'), new \DateTime('Wed, 02 Oct 2013 12:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000'), $deL10N], - [$deL10N->t('yesterday'), new \DateTime('Tue, 01 Oct 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000'), $deL10N], - [$deL10N->n('%n day ago', '%n days ago', 2), new \DateTime('Mon, 30 Sep 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000'), $deL10N], - [$deL10N->n('%n month ago', '%n months ago', 9), new \DateTime('Tue, 31 Dec 2013 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000'), $deL10N], - [$deL10N->n('%n year ago', '%n years ago', 2), new \DateTime('Sun, 01 Jan 2012 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000'), $deL10N], - - // Test time - ['today', new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['today', new \DateTime('Wed, 02 Oct 2013 12:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['today', new \DateTime('Wed, 02 Oct 2013 23:59:58 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - - // Test some special yesterdays - ['yesterday', new \DateTime('Tue, 01 Oct 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000')], - ['yesterday', new \DateTime('Tue, 01 Oct 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['yesterday', new \DateTime('Tue, 01 Oct 2013 23:59:58 +0000'), new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000')], - ['yesterday', new \DateTime('Tue, 01 Oct 2013 23:59:58 +0000'), new \DateTime('Wed, 02 Oct 2013 23:59:59 +0000')], - ['yesterday', new \DateTime('Mon, 30 Sep 2013 00:00:00 +0000'), new \DateTime('Tue, 01 Oct 2013 00:00:00 +0000')], - ['yesterday', new \DateTime('Mon, 31 Dec 2012 00:00:00 +0000'), new \DateTime('Tue, 01 Jan 2013 00:00:00 +0000')], - - // Test last month - ['2 days ago', new \DateTime('Mon, 30 Sep 2013 00:00:00 +0000'), new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000')], - ['last month', new \DateTime('Mon, 30 Sep 2013 00:00:00 +0000'), new \DateTime('Tue, 31 Oct 2013 00:00:00 +0000')], - ['last month', new \DateTime('Sun, 01 Sep 2013 00:00:00 +0000'), new \DateTime('Tue, 01 Oct 2013 00:00:00 +0000')], - ['last month', new \DateTime('Sun, 01 Sep 2013 00:00:00 +0000'), new \DateTime('Thu, 31 Oct 2013 00:00:00 +0000')], - - // Test last year - ['9 months ago', new \DateTime('Tue, 31 Dec 2013 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000')], - ['11 months ago', new \DateTime('Thu, 03 Oct 2013 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000')], - ['last year', new \DateTime('Wed, 02 Oct 2013 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000')], - ['last year', new \DateTime('Tue, 01 Jan 2013 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000')], - ['2 years ago', new \DateTime('Sun, 01 Jan 2012 00:00:00 +0000'), new \DateTime('Thu, 02 Oct 2014 00:00:00 +0000')], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('formatDateSpanData')] - public function testFormatDateSpan($expected, $timestamp, $compare = null, $locale = null): void { - $this->assertEquals((string)$expected, (string)$this->formatter->formatDateSpan($timestamp, $compare, $locale)); - } - - public static function formatDateData(): array { - return [ - [1102831200, 'December 12, 2004'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('formatDateData')] - public function testFormatDate($timestamp, $expected): void { - $this->assertEquals($expected, (string)$this->formatter->formatDate($timestamp)); - } - - public static function formatDateTimeData(): array { - return [ - [1350129205, null, "October 13, 2012, 11:53:25\xE2\x80\xAFAM UTC"], - [1350129205, new \DateTimeZone('Europe/Berlin'), "October 13, 2012, 1:53:25\xE2\x80\xAFPM GMT+2"], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('formatDateTimeData')] - public function testFormatDateTime($timestamp, $timeZone, $expected): void { - $this->assertEquals($expected, (string)$this->formatter->formatDateTime($timestamp, 'long', 'long', $timeZone)); - } - - public function testFormatDateWithInvalidTZ(): void { - $this->expectException(\Exception::class); - - $this->formatter->formatDate(1350129205, 'long', new \DateTimeZone('Mordor/Barad-dûr')); - } -} --- a/tests/lib/Diagnostics/EventLoggerTest.php +++ /dev/null @@ -1,58 +0,0 @@ -logger = new EventLogger( - $this->createMock(SystemConfig::class), - $this->createMock(LoggerInterface::class), - $this->createMock(Log::class) - ); - } - - public function testQueryLogger(): void { - // Module is not activated and this should not be logged - $this->logger->start('test1', 'testevent1'); - $this->logger->end('test1'); - $this->logger->log('test2', 'testevent2', microtime(true), microtime(true)); - $events = $this->logger->getEvents(); - $this->assertSame(0, sizeof($events)); - - // Activate module and log some query - $this->logger->activate(); - - // start one event - $this->logger->start('test3', 'testevent3'); - - // force log of another event - $this->logger->log('test4', 'testevent4', microtime(true), microtime(true)); - - // log started event - $this->logger->end('test3'); - - $events = $this->logger->getEvents(); - $this->assertSame('test4', $events['test4']->getId()); - $this->assertSame('testevent4', $events['test4']->getDescription()); - $this->assertSame('test3', $events['test3']->getId()); - $this->assertSame('testevent3', $events['test3']->getDescription()); - $this->assertSame(2, sizeof($events)); - } -} --- a/tests/lib/Diagnostics/QueryLoggerTest.php +++ /dev/null @@ -1,43 +0,0 @@ -logger = new QueryLogger( - Server::get(SystemConfig::class) - ); - } - - public function testQueryLogger(): void { - // Module is not activated and this should not be logged - $this->logger->startQuery('SELECT', ['testuser', 'count'], ['string', 'int']); - $this->logger->stopQuery(); - $queries = $this->logger->getQueries(); - $this->assertSame(0, sizeof($queries)); - - // Activate module and log some query - $this->logger->activate(); - $this->logger->startQuery('SELECT', ['testuser', 'count'], ['string', 'int']); - $this->logger->stopQuery(); - - $queries = $this->logger->getQueries(); - $this->assertSame(1, sizeof($queries)); - } -} --- a/tests/lib/DirectEditing/ManagerTest.php +++ /dev/null @@ -1,324 +0,0 @@ -editor = new Editor(); - - $this->random = $this->createMock(ISecureRandom::class); - $this->connection = Server::get(IDBConnection::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->userFolder = $this->createMock(Folder::class); - $this->l10n = $this->createMock(IL10N::class); - $this->encryptionManager = $this->createMock(IManager::class); - - $l10nFactory = $this->createMock(IFactory::class); - $l10nFactory->expects($this->once()) - ->method('get') - ->willReturn($this->l10n); - - $this->rootFolder->expects($this->any()) - ->method('getUserFolder') - ->willReturn($this->userFolder); - - $user = $this->createMock(IUser::class); - $user->expects(self::any()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession->expects(self::any()) - ->method('getUser') - ->willReturn($user); - - $this->manager = new Manager( - $this->random, $this->connection, $this->userSession, $this->rootFolder, $l10nFactory, $this->encryptionManager - ); - - $this->manager->registerDirectEditor($this->editor); - } - - public function testEditorRegistration(): void { - $this->assertEquals($this->manager->getEditors(), ['testeditor' => $this->editor]); - } - - public function testCreateToken(): void { - $expectedToken = 'TOKEN' . time(); - $file = $this->createMock(File::class); - $file->expects($this->any()) - ->method('getId') - ->willReturn(123); - $this->random->expects($this->once()) - ->method('generate') - ->willReturn($expectedToken); - $folder = $this->createMock(Folder::class); - $this->userFolder - ->method('nodeExists') - ->willReturnMap([ - ['/File.txt', false], - ['/', true], - ]); - $this->userFolder - ->method('get') - ->with('/') - ->willReturn($folder); - $folder->expects($this->once()) - ->method('newFile') - ->willReturn($file); - $token = $this->manager->create('/File.txt', 'testeditor', 'createEmpty'); - $this->assertEquals($token, $expectedToken); - } - - public function testCreateTokenAccess(): void { - $expectedToken = 'TOKEN' . time(); - $file = $this->createMock(File::class); - $file->expects($this->any()) - ->method('getId') - ->willReturn(123); - $this->random->expects($this->once()) - ->method('generate') - ->willReturn($expectedToken); - $folder = $this->createMock(Folder::class); - $this->userFolder - ->method('nodeExists') - ->willReturnMap([ - ['/File.txt', false], - ['/', true], - ]); - $this->userFolder - ->method('get') - ->with('/') - ->willReturn($folder); - $folder->expects($this->once()) - ->method('newFile') - ->willReturn($file); - $this->manager->create('/File.txt', 'testeditor', 'createEmpty'); - $firstResult = $this->manager->edit($expectedToken); - $secondResult = $this->manager->edit($expectedToken); - $this->assertInstanceOf(DataResponse::class, $firstResult); - $this->assertInstanceOf(NotFoundResponse::class, $secondResult); - } - - public function testOpenByPath(): void { - $expectedToken = 'TOKEN' . time(); - $file = $this->createMock(File::class); - $file->expects($this->any()) - ->method('getId') - ->willReturn(123); - $file->expects($this->any()) - ->method('getPath') - ->willReturn('/admin/files/File.txt'); - $this->random->expects($this->once()) - ->method('generate') - ->willReturn($expectedToken); - $this->userFolder - ->method('nodeExists') - ->willReturnMap([ - ['/File.txt', false], - ['/', true], - ]); - $this->userFolder - ->method('get') - ->with('/File.txt') - ->willReturn($file); - $this->userFolder - ->method('getRelativePath') - ->willReturn('/File.txt'); - $this->manager->open('/File.txt', 'testeditor'); - $firstResult = $this->manager->edit($expectedToken); - $secondResult = $this->manager->edit($expectedToken); - $this->assertInstanceOf(DataResponse::class, $firstResult); - $this->assertInstanceOf(NotFoundResponse::class, $secondResult); - } - - public function testOpenById(): void { - $expectedToken = 'TOKEN' . time(); - $fileRead = $this->createMock(File::class); - $fileRead->method('getPermissions') - ->willReturn(1); - $fileRead->expects($this->any()) - ->method('getId') - ->willReturn(123); - $fileRead->expects($this->any()) - ->method('getPath') - ->willReturn('/admin/files/shared_file.txt'); - $file = $this->createMock(File::class); - $file->method('getPermissions') - ->willReturn(1); - $file->expects($this->any()) - ->method('getId') - ->willReturn(123); - $file->expects($this->any()) - ->method('getPath') - ->willReturn('/admin/files/File.txt'); - $this->random->expects($this->once()) - ->method('generate') - ->willReturn($expectedToken); - $folder = $this->createMock(Folder::class); - $folder->expects($this->any()) - ->method('getById') - ->willReturn([ - $fileRead, - $file - ]); - $this->userFolder - ->method('nodeExists') - ->willReturnMap([ - ['/File.txt', false], - ['/', true], - ]); - $this->userFolder - ->method('get') - ->with('/') - ->willReturn($folder); - $this->userFolder - ->method('getRelativePath') - ->willReturn('/File.txt'); - - $this->manager->open('/', 'testeditor', 123); - $firstResult = $this->manager->edit($expectedToken); - $secondResult = $this->manager->edit($expectedToken); - $this->assertInstanceOf(DataResponse::class, $firstResult); - $this->assertInstanceOf(NotFoundResponse::class, $secondResult); - } - - public function testCreateFileAlreadyExists(): void { - $this->expectException(\RuntimeException::class); - $this->userFolder - ->method('nodeExists') - ->with('/File.txt') - ->willReturn(true); - - $this->manager->create('/File.txt', 'testeditor', 'createEmpty'); - } -} --- a/tests/lib/EmojiHelperTest.php +++ /dev/null @@ -1,3155 +0,0 @@ -db = $this->createMock(IDBConnection::class); - $this->helper = new EmojiHelper($this->db); - } - - /** - * @param bool $supports4ByteText - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('doesPlatformSupportEmojiDataProvider')] - public function testDoesPlatformSupportEmoji(bool $supports4ByteText, bool $expected): void { - $this->db->expects($this->once()) - ->method('supports4ByteText') - ->willReturn($supports4ByteText); - - $this->assertEquals($expected, $this->helper->doesPlatformSupportEmoji()); - } - - public static function doesPlatformSupportEmojiDataProvider(): array { - return [ - [true, true], - [false, false], - ]; - } - - /** - * @param string $emoji - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('isValidSingleEmojiDataProvider')] - public function testIsValidSingleEmoji(string $emoji, bool $expected): void { - $actual = $this->helper->isValidSingleEmoji($emoji); - - $this->assertEquals($expected, $actual); - } - - public static function isValidSingleEmojiDataProvider(): array { - return [ - ['📱📠', false], - ['a', false], - ['0', false], - ['$', false], - ['$$', false], - // Cyrillic - ['а', false], - ['Б', false], - ['ж', false], - ['Я', false], - // Greek - ['α', false], - ['Ω', false], - ['λ', false], - ['Σ', false], - // Extracted from spreed/node_modules/emojis-list/index.js - // https://github.com/Kikobeats/emojis-list/blob/master/index.js - ['🀄️', true], - ['🃏', true], - ['🅰️', true], - ['🅱️', true], - ['🅾️', true], - ['🅿️', true], - ['🆎', true], - ['🆑', true], - ['🆒', true], - ['🆓', true], - ['🆔', true], - ['🆕', true], - ['🆖', true], - ['🆗', true], - ['🆘', true], - ['🆙', true], - ['🆚', true], - ['🇦🇨', true], - ['🇦🇩', true], - ['🇦🇪', true], - ['🇦🇫', true], - ['🇦🇬', true], - ['🇦🇮', true], - ['🇦🇱', true], - ['🇦🇲', true], - ['🇦🇴', true], - ['🇦🇶', true], - ['🇦🇷', true], - ['🇦🇸', true], - ['🇦🇹', true], - ['🇦🇺', true], - ['🇦🇼', true], - ['🇦🇽', true], - ['🇦🇿', true], - ['🇦', true], - ['🇧🇦', true], - ['🇧🇧', true], - ['🇧🇩', true], - ['🇧🇪', true], - ['🇧🇫', true], - ['🇧🇬', true], - ['🇧🇭', true], - ['🇧🇮', true], - ['🇧🇯', true], - ['🇧🇱', true], - ['🇧🇲', true], - ['🇧🇳', true], - ['🇧🇴', true], - ['🇧🇶', true], - ['🇧🇷', true], - ['🇧🇸', true], - ['🇧🇹', true], - ['🇧🇻', true], - ['🇧🇼', true], - ['🇧🇾', true], - ['🇧🇿', true], - ['🇧', true], - ['🇨🇦', true], - ['🇨🇨', true], - ['🇨🇩', true], - ['🇨🇫', true], - ['🇨🇬', true], - ['🇨🇭', true], - ['🇨🇮', true], - ['🇨🇰', true], - ['🇨🇱', true], - ['🇨🇲', true], - ['🇨🇳', true], - ['🇨🇴', true], - ['🇨🇵', true], - ['🇨🇷', true], - ['🇨🇺', true], - ['🇨🇻', true], - ['🇨🇼', true], - ['🇨🇽', true], - ['🇨🇾', true], - ['🇨🇿', true], - ['🇨', true], - ['🇩🇪', true], - ['🇩🇬', true], - ['🇩🇯', true], - ['🇩🇰', true], - ['🇩🇲', true], - ['🇩🇴', true], - ['🇩🇿', true], - ['🇩', true], - ['🇪🇦', true], - ['🇪🇨', true], - ['🇪🇪', true], - ['🇪🇬', true], - ['🇪🇭', true], - ['🇪🇷', true], - ['🇪🇸', true], - ['🇪🇹', true], - ['🇪🇺', true], - ['🇪', true], - ['🇫🇮', true], - ['🇫🇯', true], - ['🇫🇰', true], - ['🇫🇲', true], - ['🇫🇴', true], - ['🇫🇷', true], - ['🇫', true], - ['🇬🇦', true], - ['🇬🇧', true], - ['🇬🇩', true], - ['🇬🇪', true], - ['🇬🇫', true], - ['🇬🇬', true], - ['🇬🇭', true], - ['🇬🇮', true], - ['🇬🇱', true], - ['🇬🇲', true], - ['🇬🇳', true], - ['🇬🇵', true], - ['🇬🇶', true], - ['🇬🇷', true], - ['🇬🇸', true], - ['🇬🇹', true], - ['🇬🇺', true], - ['🇬🇼', true], - ['🇬🇾', true], - ['🇬', true], - ['🇭🇰', true], - ['🇭🇲', true], - ['🇭🇳', true], - ['🇭🇷', true], - ['🇭🇹', true], - ['🇭🇺', true], - ['🇭', true], - ['🇮🇨', true], - ['🇮🇩', true], - ['🇮🇪', true], - ['🇮🇱', true], - ['🇮🇲', true], - ['🇮🇳', true], - ['🇮🇴', true], - ['🇮🇶', true], - ['🇮🇷', true], - ['🇮🇸', true], - ['🇮🇹', true], - ['🇮', true], - ['🇯🇪', true], - ['🇯🇲', true], - ['🇯🇴', true], - ['🇯🇵', true], - ['🇯', true], - ['🇰🇪', true], - ['🇰🇬', true], - ['🇰🇭', true], - ['🇰🇮', true], - ['🇰🇲', true], - ['🇰🇳', true], - ['🇰🇵', true], - ['🇰🇷', true], - ['🇰🇼', true], - ['🇰🇾', true], - ['🇰🇿', true], - ['🇰', true], - ['🇱🇦', true], - ['🇱🇧', true], - ['🇱🇨', true], - ['🇱🇮', true], - ['🇱🇰', true], - ['🇱🇷', true], - ['🇱🇸', true], - ['🇱🇹', true], - ['🇱🇺', true], - ['🇱🇻', true], - ['🇱🇾', true], - ['🇱', true], - ['🇲🇦', true], - ['🇲🇨', true], - ['🇲🇩', true], - ['🇲🇪', true], - ['🇲🇫', true], - ['🇲🇬', true], - ['🇲🇭', true], - ['🇲🇰', true], - ['🇲🇱', true], - ['🇲🇲', true], - ['🇲🇳', true], - ['🇲🇴', true], - ['🇲🇵', true], - ['🇲🇶', true], - ['🇲🇷', true], - ['🇲🇸', true], - ['🇲🇹', true], - ['🇲🇺', true], - ['🇲🇻', true], - ['🇲🇼', true], - ['🇲🇽', true], - ['🇲🇾', true], - ['🇲🇿', true], - ['🇲', true], - ['🇳🇦', true], - ['🇳🇨', true], - ['🇳🇪', true], - ['🇳🇫', true], - ['🇳🇬', true], - ['🇳🇮', true], - ['🇳🇱', true], - ['🇳🇴', true], - ['🇳🇵', true], - ['🇳🇷', true], - ['🇳🇺', true], - ['🇳🇿', true], - ['🇳', true], - ['🇴🇲', true], - ['🇴', true], - ['🇵🇦', true], - ['🇵🇪', true], - ['🇵🇫', true], - ['🇵🇬', true], - ['🇵🇭', true], - ['🇵🇰', true], - ['🇵🇱', true], - ['🇵🇲', true], - ['🇵🇳', true], - ['🇵🇷', true], - ['🇵🇸', true], - ['🇵🇹', true], - ['🇵🇼', true], - ['🇵🇾', true], - ['🇵', true], - ['🇶🇦', true], - ['🇶', true], - ['🇷🇪', true], - ['🇷🇴', true], - ['🇷🇸', true], - ['🇷🇺', true], - ['🇷🇼', true], - ['🇷', true], - ['🇸🇦', true], - ['🇸🇧', true], - ['🇸🇨', true], - ['🇸🇩', true], - ['🇸🇪', true], - ['🇸🇬', true], - ['🇸🇭', true], - ['🇸🇮', true], - ['🇸🇯', true], - ['🇸🇰', true], - ['🇸🇱', true], - ['🇸🇲', true], - ['🇸🇳', true], - ['🇸🇴', true], - ['🇸🇷', true], - ['🇸🇸', true], - ['🇸🇹', true], - ['🇸🇻', true], - ['🇸🇽', true], - ['🇸🇾', true], - ['🇸🇿', true], - ['🇸', true], - ['🇹🇦', true], - ['🇹🇨', true], - ['🇹🇩', true], - ['🇹🇫', true], - ['🇹🇬', true], - ['🇹🇭', true], - ['🇹🇯', true], - ['🇹🇰', true], - ['🇹🇱', true], - ['🇹🇲', true], - ['🇹🇳', true], - ['🇹🇴', true], - ['🇹🇷', true], - ['🇹🇹', true], - ['🇹🇻', true], - ['🇹🇼', true], - ['🇹🇿', true], - ['🇹', true], - ['🇺🇦', true], - ['🇺🇬', true], - ['🇺🇲', true], - ['🇺🇳', true], - ['🇺🇸', true], - ['🇺🇾', true], - ['🇺🇿', true], - ['🇺', true], - ['🇻🇦', true], - ['🇻🇨', true], - ['🇻🇪', true], - ['🇻🇬', true], - ['🇻🇮', true], - ['🇻🇳', true], - ['🇻🇺', true], - ['🇻', true], - ['🇼🇫', true], - ['🇼🇸', true], - ['🇼', true], - ['🇽🇰', true], - ['🇽', true], - ['🇾🇪', true], - ['🇾🇹', true], - ['🇾', true], - ['🇿🇦', true], - ['🇿🇲', true], - ['🇿🇼', true], - ['🇿', true], - ['🈁', true], - ['🈂️', true], - ['🈚️', true], - ['🈯️', true], - ['🈲', true], - ['🈳', true], - ['🈴', true], - ['🈵', true], - ['🈶', true], - ['🈷️', true], - ['🈸', true], - ['🈹', true], - ['🈺', true], - ['🉐', true], - ['🉑', true], - ['🌀', true], - ['🌁', true], - ['🌂', true], - ['🌃', true], - ['🌄', true], - ['🌅', true], - ['🌆', true], - ['🌇', true], - ['🌈', true], - ['🌉', true], - ['🌊', true], - ['🌋', true], - ['🌌', true], - ['🌍', true], - ['🌎', true], - ['🌏', true], - ['🌐', true], - ['🌑', true], - ['🌒', true], - ['🌓', true], - ['🌔', true], - ['🌕', true], - ['🌖', true], - ['🌗', true], - ['🌘', true], - ['🌙', true], - ['🌚', true], - ['🌛', true], - ['🌜', true], - ['🌝', true], - ['🌞', true], - ['🌟', true], - ['🌠', true], - ['🌡️', true], - ['🌤️', true], - ['🌥️', true], - ['🌦️', true], - ['🌧️', true], - ['🌨️', true], - ['🌩️', true], - ['🌪️', true], - ['🌫️', true], - ['🌬️', true], - ['🌭', true], - ['🌮', true], - ['🌯', true], - ['🌰', true], - ['🌱', true], - ['🌲', true], - ['🌳', true], - ['🌴', true], - ['🌵', true], - ['🌶️', true], - ['🌷', true], - ['🌸', true], - ['🌹', true], - ['🌺', true], - ['🌻', true], - ['🌼', true], - ['🌽', true], - ['🌾', true], - ['🌿', true], - ['🍀', true], - ['🍁', true], - ['🍂', true], - ['🍃', true], - ['🍄', true], - ['🍅', true], - ['🍆', true], - ['🍇', true], - ['🍈', true], - ['🍉', true], - ['🍊', true], - ['🍋', true], - ['🍌', true], - ['🍍', true], - ['🍎', true], - ['🍏', true], - ['🍐', true], - ['🍑', true], - ['🍒', true], - ['🍓', true], - ['🍔', true], - ['🍕', true], - ['🍖', true], - ['🍗', true], - ['🍘', true], - ['🍙', true], - ['🍚', true], - ['🍛', true], - ['🍜', true], - ['🍝', true], - ['🍞', true], - ['🍟', true], - ['🍠', true], - ['🍡', true], - ['🍢', true], - ['🍣', true], - ['🍤', true], - ['🍥', true], - ['🍦', true], - ['🍧', true], - ['🍨', true], - ['🍩', true], - ['🍪', true], - ['🍫', true], - ['🍬', true], - ['🍭', true], - ['🍮', true], - ['🍯', true], - ['🍰', true], - ['🍱', true], - ['🍲', true], - ['🍳', true], - ['🍴', true], - ['🍵', true], - ['🍶', true], - ['🍷', true], - ['🍸', true], - ['🍹', true], - ['🍺', true], - ['🍻', true], - ['🍼', true], - ['🍽️', true], - ['🍾', true], - ['🍿', true], - ['🎀', true], - ['🎁', true], - ['🎂', true], - ['🎃', true], - ['🎄', true], - ['🎅🏻', true], - ['🎅🏼', true], - ['🎅🏽', true], - ['🎅🏾', true], - ['🎅🏿', true], - ['🎅', true], - ['🎆', true], - ['🎇', true], - ['🎈', true], - ['🎉', true], - ['🎊', true], - ['🎋', true], - ['🎌', true], - ['🎍', true], - ['🎎', true], - ['🎏', true], - ['🎐', true], - ['🎑', true], - ['🎒', true], - ['🎓', true], - ['🎖️', true], - ['🎗️', true], - ['🎙️', true], - ['🎚️', true], - ['🎛️', true], - ['🎞️', true], - ['🎟️', true], - ['🎠', true], - ['🎡', true], - ['🎢', true], - ['🎣', true], - ['🎤', true], - ['🎥', true], - ['🎦', true], - ['🎧', true], - ['🎨', true], - ['🎩', true], - ['🎪', true], - ['🎫', true], - ['🎬', true], - ['🎭', true], - ['🎮', true], - ['🎯', true], - ['🎰', true], - ['🎱', true], - ['🎲', true], - ['🎳', true], - ['🎴', true], - ['🎵', true], - ['🎶', true], - ['🎷', true], - ['🎸', true], - ['🎹', true], - ['🎺', true], - ['🎻', true], - ['🎼', true], - ['🎽', true], - ['🎾', true], - ['🎿', true], - ['🏀', true], - ['🏁', true], - ['🏂🏻', true], - ['🏂🏼', true], - ['🏂🏽', true], - ['🏂🏾', true], - ['🏂🏿', true], - ['🏂', true], - ['🏃🏻‍♀️', true], - ['🏃🏻‍♂️', true], - ['🏃🏻', true], - ['🏃🏼‍♀️', true], - ['🏃🏼‍♂️', true], - ['🏃🏼', true], - ['🏃🏽‍♀️', true], - ['🏃🏽‍♂️', true], - ['🏃🏽', true], - ['🏃🏾‍♀️', true], - ['🏃🏾‍♂️', true], - ['🏃🏾', true], - ['🏃🏿‍♀️', true], - ['🏃🏿‍♂️', true], - ['🏃🏿', true], - ['🏃‍♀️', true], - ['🏃‍♂️', true], - ['🏃', true], - ['🏄🏻‍♀️', true], - ['🏄🏻‍♂️', true], - ['🏄🏻', true], - ['🏄🏼‍♀️', true], - ['🏄🏼‍♂️', true], - ['🏄🏼', true], - ['🏄🏽‍♀️', true], - ['🏄🏽‍♂️', true], - ['🏄🏽', true], - ['🏄🏾‍♀️', true], - ['🏄🏾‍♂️', true], - ['🏄🏾', true], - ['🏄🏿‍♀️', true], - ['🏄🏿‍♂️', true], - ['🏄🏿', true], - ['🏄‍♀️', true], - ['🏄‍♂️', true], - ['🏄', true], - ['🏅', true], - ['🏆', true], - ['🏇🏻', true], - ['🏇🏼', true], - ['🏇🏽', true], - ['🏇🏾', true], - ['🏇🏿', true], - ['🏇', true], - ['🏈', true], - ['🏉', true], - ['🏊🏻‍♀️', true], - ['🏊🏻‍♂️', true], - ['🏊🏻', true], - ['🏊🏼‍♀️', true], - ['🏊🏼‍♂️', true], - ['🏊🏼', true], - ['🏊🏽‍♀️', true], - ['🏊🏽‍♂️', true], - ['🏊🏽', true], - ['🏊🏾‍♀️', true], - ['🏊🏾‍♂️', true], - ['🏊🏾', true], - ['🏊🏿‍♀️', true], - ['🏊🏿‍♂️', true], - ['🏊🏿', true], - ['🏊‍♀️', true], - ['🏊‍♂️', true], - ['🏊', true], - ['🏋🏻‍♀️', true], - ['🏋🏻‍♂️', true], - ['🏋🏻', true], - ['🏋🏼‍♀️', true], - ['🏋🏼‍♂️', true], - ['🏋🏼', true], - ['🏋🏽‍♀️', true], - ['🏋🏽‍♂️', true], - ['🏋🏽', true], - ['🏋🏾‍♀️', true], - ['🏋🏾‍♂️', true], - ['🏋🏾', true], - ['🏋🏿‍♀️', true], - ['🏋🏿‍♂️', true], - ['🏋🏿', true], - ['🏋️‍♀️', true], - ['🏋️‍♂️', true], - ['🏋️', true], - ['🏌🏻‍♀️', true], - ['🏌🏻‍♂️', true], - ['🏌🏻', true], - ['🏌🏼‍♀️', true], - ['🏌🏼‍♂️', true], - ['🏌🏼', true], - ['🏌🏽‍♀️', true], - ['🏌🏽‍♂️', true], - ['🏌🏽', true], - ['🏌🏾‍♀️', true], - ['🏌🏾‍♂️', true], - ['🏌🏾', true], - ['🏌🏿‍♀️', true], - ['🏌🏿‍♂️', true], - ['🏌🏿', true], - ['🏌️‍♀️', true], - ['🏌️‍♂️', true], - ['🏌️', true], - ['🏍️', true], - ['🏎️', true], - ['🏏', true], - ['🏐', true], - ['🏑', true], - ['🏒', true], - ['🏓', true], - ['🏔️', true], - ['🏕️', true], - ['🏖️', true], - ['🏗️', true], - ['🏘️', true], - ['🏙️', true], - ['🏚️', true], - ['🏛️', true], - ['🏜️', true], - ['🏝️', true], - ['🏞️', true], - ['🏟️', true], - ['🏠', true], - ['🏡', true], - ['🏢', true], - ['🏣', true], - ['🏤', true], - ['🏥', true], - ['🏦', true], - ['🏧', true], - ['🏨', true], - ['🏩', true], - ['🏪', true], - ['🏫', true], - ['🏬', true], - ['🏭', true], - ['🏮', true], - ['🏯', true], - ['🏰', true], - ['🏳️‍🌈', true], - ['🏳️', true], - ['🏴‍☠️', true], - ['🏴󠁧󠁢󠁥󠁮󠁧󠁿', true], - ['🏴󠁧󠁢󠁳󠁣󠁴󠁿', true], - ['🏴󠁧󠁢󠁷󠁬󠁳󠁿', true], - ['🏴', true], - ['🏵️', true], - ['🏷️', true], - ['🏸', true], - ['🏹', true], - ['🏺', true], - ['🏻', true], - ['🏼', true], - ['🏽', true], - ['🏾', true], - ['🏿', true], - ['🐀', true], - ['🐁', true], - ['🐂', true], - ['🐃', true], - ['🐄', true], - ['🐅', true], - ['🐆', true], - ['🐇', true], - ['🐈', true], - ['🐉', true], - ['🐊', true], - ['🐋', true], - ['🐌', true], - ['🐍', true], - ['🐎', true], - ['🐏', true], - ['🐐', true], - ['🐑', true], - ['🐒', true], - ['🐓', true], - ['🐔', true], - ['🐕‍🦺', true], - ['🐕', true], - ['🐖', true], - ['🐗', true], - ['🐘', true], - ['🐙', true], - ['🐚', true], - ['🐛', true], - ['🐜', true], - ['🐝', true], - ['🐞', true], - ['🐟', true], - ['🐠', true], - ['🐡', true], - ['🐢', true], - ['🐣', true], - ['🐤', true], - ['🐥', true], - ['🐦', true], - ['🐧', true], - ['🐨', true], - ['🐩', true], - ['🐪', true], - ['🐫', true], - ['🐬', true], - ['🐭', true], - ['🐮', true], - ['🐯', true], - ['🐰', true], - ['🐱', true], - ['🐲', true], - ['🐳', true], - ['🐴', true], - ['🐵', true], - ['🐶', true], - ['🐷', true], - ['🐸', true], - ['🐹', true], - ['🐺', true], - ['🐻', true], - ['🐼', true], - ['🐽', true], - ['🐾', true], - ['🐿️', true], - ['👀', true], - ['👁‍🗨', true], - ['👁️', true], - ['👂🏻', true], - ['👂🏼', true], - ['👂🏽', true], - ['👂🏾', true], - ['👂🏿', true], - ['👂', true], - ['👃🏻', true], - ['👃🏼', true], - ['👃🏽', true], - ['👃🏾', true], - ['👃🏿', true], - ['👃', true], - ['👄', true], - ['👅', true], - ['👆🏻', true], - ['👆🏼', true], - ['👆🏽', true], - ['👆🏾', true], - ['👆🏿', true], - ['👆', true], - ['👇🏻', true], - ['👇🏼', true], - ['👇🏽', true], - ['👇🏾', true], - ['👇🏿', true], - ['👇', true], - ['👈🏻', true], - ['👈🏼', true], - ['👈🏽', true], - ['👈🏾', true], - ['👈🏿', true], - ['👈', true], - ['👉🏻', true], - ['👉🏼', true], - ['👉🏽', true], - ['👉🏾', true], - ['👉🏿', true], - ['👉', true], - ['👊🏻', true], - ['👊🏼', true], - ['👊🏽', true], - ['👊🏾', true], - ['👊🏿', true], - ['👊', true], - ['👋🏻', true], - ['👋🏼', true], - ['👋🏽', true], - ['👋🏾', true], - ['👋🏿', true], - ['👋', true], - ['👌🏻', true], - ['👌🏼', true], - ['👌🏽', true], - ['👌🏾', true], - ['👌🏿', true], - ['👌', true], - ['👍🏻', true], - ['👍🏼', true], - ['👍🏽', true], - ['👍🏾', true], - ['👍🏿', true], - ['👍', true], - ['👎🏻', true], - ['👎🏼', true], - ['👎🏽', true], - ['👎🏾', true], - ['👎🏿', true], - ['👎', true], - ['👏🏻', true], - ['👏🏼', true], - ['👏🏽', true], - ['👏🏾', true], - ['👏🏿', true], - ['👏', true], - ['👐🏻', true], - ['👐🏼', true], - ['👐🏽', true], - ['👐🏾', true], - ['👐🏿', true], - ['👐', true], - ['👑', true], - ['👒', true], - ['👓', true], - ['👔', true], - ['👕', true], - ['👖', true], - ['👗', true], - ['👘', true], - ['👙', true], - ['👚', true], - ['👛', true], - ['👜', true], - ['👝', true], - ['👞', true], - ['👟', true], - ['👠', true], - ['👡', true], - ['👢', true], - ['👣', true], - ['👤', true], - ['👥', true], - ['👦🏻', true], - ['👦🏼', true], - ['👦🏽', true], - ['👦🏾', true], - ['👦🏿', true], - ['👦', true], - ['👧🏻', true], - ['👧🏼', true], - ['👧🏽', true], - ['👧🏾', true], - ['👧🏿', true], - ['👧', true], - ['👨🏻‍🌾', true], - ['👨🏻‍🍳', true], - ['👨🏻‍🎓', true], - ['👨🏻‍🎤', true], - ['👨🏻‍🎨', true], - ['👨🏻‍🏫', true], - ['👨🏻‍🏭', true], - ['👨🏻‍💻', true], - ['👨🏻‍💼', true], - ['👨🏻‍🔧', true], - ['👨🏻‍🔬', true], - ['👨🏻‍🚀', true], - ['👨🏻‍🚒', true], - ['👨🏻‍🦯', true], - ['👨🏻‍🦰', true], - ['👨🏻‍🦱', true], - ['👨🏻‍🦲', true], - ['👨🏻‍🦳', true], - ['👨🏻‍🦼', true], - ['👨🏻‍🦽', true], - ['👨🏻‍⚕️', true], - ['👨🏻‍⚖️', true], - ['👨🏻‍✈️', true], - ['👨🏻', true], - ['👨🏼‍🌾', true], - ['👨🏼‍🍳', true], - ['👨🏼‍🎓', true], - ['👨🏼‍🎤', true], - ['👨🏼‍🎨', true], - ['👨🏼‍🏫', true], - ['👨🏼‍🏭', true], - ['👨🏼‍💻', true], - ['👨🏼‍💼', true], - ['👨🏼‍🔧', true], - ['👨🏼‍🔬', true], - ['👨🏼‍🚀', true], - ['👨🏼‍🚒', true], - ['👨🏼‍🤝‍👨🏻', true], - ['👨🏼‍🦯', true], - ['👨🏼‍🦰', true], - ['👨🏼‍🦱', true], - ['👨🏼‍🦲', true], - ['👨🏼‍🦳', true], - ['👨🏼‍🦼', true], - ['👨🏼‍🦽', true], - ['👨🏼‍⚕️', true], - ['👨🏼‍⚖️', true], - ['👨🏼‍✈️', true], - ['👨🏼', true], - ['👨🏽‍🌾', true], - ['👨🏽‍🍳', true], - ['👨🏽‍🎓', true], - ['👨🏽‍🎤', true], - ['👨🏽‍🎨', true], - ['👨🏽‍🏫', true], - ['👨🏽‍🏭', true], - ['👨🏽‍💻', true], - ['👨🏽‍💼', true], - ['👨🏽‍🔧', true], - ['👨🏽‍🔬', true], - ['👨🏽‍🚀', true], - ['👨🏽‍🚒', true], - ['👨🏽‍🤝‍👨🏻', true], - ['👨🏽‍🤝‍👨🏼', true], - ['👨🏽‍🦯', true], - ['👨🏽‍🦰', true], - ['👨🏽‍🦱', true], - ['👨🏽‍🦲', true], - ['👨🏽‍🦳', true], - ['👨🏽‍🦼', true], - ['👨🏽‍🦽', true], - ['👨🏽‍⚕️', true], - ['👨🏽‍⚖️', true], - ['👨🏽‍✈️', true], - ['👨🏽', true], - ['👨🏾‍🌾', true], - ['👨🏾‍🍳', true], - ['👨🏾‍🎓', true], - ['👨🏾‍🎤', true], - ['👨🏾‍🎨', true], - ['👨🏾‍🏫', true], - ['👨🏾‍🏭', true], - ['👨🏾‍💻', true], - ['👨🏾‍💼', true], - ['👨🏾‍🔧', true], - ['👨🏾‍🔬', true], - ['👨🏾‍🚀', true], - ['👨🏾‍🚒', true], - ['👨🏾‍🤝‍👨🏻', true], - ['👨🏾‍🤝‍👨🏼', true], - ['👨🏾‍🤝‍👨🏽', true], - ['👨🏾‍🦯', true], - ['👨🏾‍🦰', true], - ['👨🏾‍🦱', true], - ['👨🏾‍🦲', true], - ['👨🏾‍🦳', true], - ['👨🏾‍🦼', true], - ['👨🏾‍🦽', true], - ['👨🏾‍⚕️', true], - ['👨🏾‍⚖️', true], - ['👨🏾‍✈️', true], - ['👨🏾', true], - ['👨🏿‍🌾', true], - ['👨🏿‍🍳', true], - ['👨🏿‍🎓', true], - ['👨🏿‍🎤', true], - ['👨🏿‍🎨', true], - ['👨🏿‍🏫', true], - ['👨🏿‍🏭', true], - ['👨🏿‍💻', true], - ['👨🏿‍💼', true], - ['👨🏿‍🔧', true], - ['👨🏿‍🔬', true], - ['👨🏿‍🚀', true], - ['👨🏿‍🚒', true], - ['👨🏿‍🤝‍👨🏻', true], - ['👨🏿‍🤝‍👨🏼', true], - ['👨🏿‍🤝‍👨🏽', true], - ['👨🏿‍🤝‍👨🏾', true], - ['👨🏿‍🦯', true], - ['👨🏿‍🦰', true], - ['👨🏿‍🦱', true], - ['👨🏿‍🦲', true], - ['👨🏿‍🦳', true], - ['👨🏿‍🦼', true], - ['👨🏿‍🦽', true], - ['👨🏿‍⚕️', true], - ['👨🏿‍⚖️', true], - ['👨🏿‍✈️', true], - ['👨🏿', true], - ['👨‍🌾', true], - ['👨‍🍳', true], - ['👨‍🎓', true], - ['👨‍🎤', true], - ['👨‍🎨', true], - ['👨‍🏫', true], - ['👨‍🏭', true], - ['👨‍👦‍👦', true], - ['👨‍👦', true], - ['👨‍👧‍👦', true], - ['👨‍👧‍👧', true], - ['👨‍👧', true], - ['👨‍👨‍👦‍👦', true], - ['👨‍👨‍👦', true], - ['👨‍👨‍👧‍👦', true], - ['👨‍👨‍👧‍👧', true], - ['👨‍👨‍👧', true], - ['👨‍👩‍👦‍👦', true], - ['👨‍👩‍👦', true], - ['👨‍👩‍👧‍👦', true], - ['👨‍👩‍👧‍👧', true], - ['👨‍👩‍👧', true], - ['👨‍💻', true], - ['👨‍💼', true], - ['👨‍🔧', true], - ['👨‍🔬', true], - ['👨‍🚀', true], - ['👨‍🚒', true], - ['👨‍🦯', true], - ['👨‍🦰', true], - ['👨‍🦱', true], - ['👨‍🦲', true], - ['👨‍🦳', true], - ['👨‍🦼', true], - ['👨‍🦽', true], - ['👨‍⚕️', true], - ['👨‍⚖️', true], - ['👨‍✈️', true], - ['👨‍❤️‍👨', true], - ['👨‍❤️‍💋‍👨', true], - ['👨', true], - ['👩🏻‍🌾', true], - ['👩🏻‍🍳', true], - ['👩🏻‍🎓', true], - ['👩🏻‍🎤', true], - ['👩🏻‍🎨', true], - ['👩🏻‍🏫', true], - ['👩🏻‍🏭', true], - ['👩🏻‍💻', true], - ['👩🏻‍💼', true], - ['👩🏻‍🔧', true], - ['👩🏻‍🔬', true], - ['👩🏻‍🚀', true], - ['👩🏻‍🚒', true], - ['👩🏻‍🤝‍👨🏼', true], - ['👩🏻‍🤝‍👨🏽', true], - ['👩🏻‍🤝‍👨🏾', true], - ['👩🏻‍🤝‍👨🏿', true], - ['👩🏻‍🦯', true], - ['👩🏻‍🦰', true], - ['👩🏻‍🦱', true], - ['👩🏻‍🦲', true], - ['👩🏻‍🦳', true], - ['👩🏻‍🦼', true], - ['👩🏻‍🦽', true], - ['👩🏻‍⚕️', true], - ['👩🏻‍⚖️', true], - ['👩🏻‍✈️', true], - ['👩🏻', true], - ['👩🏼‍🌾', true], - ['👩🏼‍🍳', true], - ['👩🏼‍🎓', true], - ['👩🏼‍🎤', true], - ['👩🏼‍🎨', true], - ['👩🏼‍🏫', true], - ['👩🏼‍🏭', true], - ['👩🏼‍💻', true], - ['👩🏼‍💼', true], - ['👩🏼‍🔧', true], - ['👩🏼‍🔬', true], - ['👩🏼‍🚀', true], - ['👩🏼‍🚒', true], - ['👩🏼‍🤝‍👨🏻', true], - ['👩🏼‍🤝‍👨🏽', true], - ['👩🏼‍🤝‍👨🏾', true], - ['👩🏼‍🤝‍👨🏿', true], - ['👩🏼‍🤝‍👩🏻', true], - ['👩🏼‍🦯', true], - ['👩🏼‍🦰', true], - ['👩🏼‍🦱', true], - ['👩🏼‍🦲', true], - ['👩🏼‍🦳', true], - ['👩🏼‍🦼', true], - ['👩🏼‍🦽', true], - ['👩🏼‍⚕️', true], - ['👩🏼‍⚖️', true], - ['👩🏼‍✈️', true], - ['👩🏼', true], - ['👩🏽‍🌾', true], - ['👩🏽‍🍳', true], - ['👩🏽‍🎓', true], - ['👩🏽‍🎤', true], - ['👩🏽‍🎨', true], - ['👩🏽‍🏫', true], - ['👩🏽‍🏭', true], - ['👩🏽‍💻', true], - ['👩🏽‍💼', true], - ['👩🏽‍🔧', true], - ['👩🏽‍🔬', true], - ['👩🏽‍🚀', true], - ['👩🏽‍🚒', true], - ['👩🏽‍🤝‍👨🏻', true], - ['👩🏽‍🤝‍👨🏼', true], - ['👩🏽‍🤝‍👨🏾', true], - ['👩🏽‍🤝‍👨🏿', true], - ['👩🏽‍🤝‍👩🏻', true], - ['👩🏽‍🤝‍👩🏼', true], - ['👩🏽‍🦯', true], - ['👩🏽‍🦰', true], - ['👩🏽‍🦱', true], - ['👩🏽‍🦲', true], - ['👩🏽‍🦳', true], - ['👩🏽‍🦼', true], - ['👩🏽‍🦽', true], - ['👩🏽‍⚕️', true], - ['👩🏽‍⚖️', true], - ['👩🏽‍✈️', true], - ['👩🏽', true], - ['👩🏾‍🌾', true], - ['👩🏾‍🍳', true], - ['👩🏾‍🎓', true], - ['👩🏾‍🎤', true], - ['👩🏾‍🎨', true], - ['👩🏾‍🏫', true], - ['👩🏾‍🏭', true], - ['👩🏾‍💻', true], - ['👩🏾‍💼', true], - ['👩🏾‍🔧', true], - ['👩🏾‍🔬', true], - ['👩🏾‍🚀', true], - ['👩🏾‍🚒', true], - ['👩🏾‍🤝‍👨🏻', true], - ['👩🏾‍🤝‍👨🏼', true], - ['👩🏾‍🤝‍👨🏽', true], - ['👩🏾‍🤝‍👨🏿', true], - ['👩🏾‍🤝‍👩🏻', true], - ['👩🏾‍🤝‍👩🏼', true], - ['👩🏾‍🤝‍👩🏽', true], - ['👩🏾‍🦯', true], - ['👩🏾‍🦰', true], - ['👩🏾‍🦱', true], - ['👩🏾‍🦲', true], - ['👩🏾‍🦳', true], - ['👩🏾‍🦼', true], - ['👩🏾‍🦽', true], - ['👩🏾‍⚕️', true], - ['👩🏾‍⚖️', true], - ['👩🏾‍✈️', true], - ['👩🏾', true], - ['👩🏿‍🌾', true], - ['👩🏿‍🍳', true], - ['👩🏿‍🎓', true], - ['👩🏿‍🎤', true], - ['👩🏿‍🎨', true], - ['👩🏿‍🏫', true], - ['👩🏿‍🏭', true], - ['👩🏿‍💻', true], - ['👩🏿‍💼', true], - ['👩🏿‍🔧', true], - ['👩🏿‍🔬', true], - ['👩🏿‍🚀', true], - ['👩🏿‍🚒', true], - ['👩🏿‍🤝‍👨🏻', true], - ['👩🏿‍🤝‍👨🏼', true], - ['👩🏿‍🤝‍👨🏽', true], - ['👩🏿‍🤝‍👨🏾', true], - ['👩🏿‍🤝‍👩🏻', true], - ['👩🏿‍🤝‍👩🏼', true], - ['👩🏿‍🤝‍👩🏽', true], - ['👩🏿‍🤝‍👩🏾', true], - ['👩🏿‍🦯', true], - ['👩🏿‍🦰', true], - ['👩🏿‍🦱', true], - ['👩🏿‍🦲', true], - ['👩🏿‍🦳', true], - ['👩🏿‍🦼', true], - ['👩🏿‍🦽', true], - ['👩🏿‍⚕️', true], - ['👩🏿‍⚖️', true], - ['👩🏿‍✈️', true], - ['👩🏿', true], - ['👩‍🌾', true], - ['👩‍🍳', true], - ['👩‍🎓', true], - ['👩‍🎤', true], - ['👩‍🎨', true], - ['👩‍🏫', true], - ['👩‍🏭', true], - ['👩‍👦‍👦', true], - ['👩‍👦', true], - ['👩‍👧‍👦', true], - ['👩‍👧‍👧', true], - ['👩‍👧', true], - ['👩‍👩‍👦‍👦', true], - ['👩‍👩‍👦', true], - ['👩‍👩‍👧‍👦', true], - ['👩‍👩‍👧‍👧', true], - ['👩‍👩‍👧', true], - ['👩‍💻', true], - ['👩‍💼', true], - ['👩‍🔧', true], - ['👩‍🔬', true], - ['👩‍🚀', true], - ['👩‍🚒', true], - ['👩‍🦯', true], - ['👩‍🦰', true], - ['👩‍🦱', true], - ['👩‍🦲', true], - ['👩‍🦳', true], - ['👩‍🦼', true], - ['👩‍🦽', true], - ['👩‍⚕️', true], - ['👩‍⚖️', true], - ['👩‍✈️', true], - ['👩‍❤️‍👨', true], - ['👩‍❤️‍👩', true], - ['👩‍❤️‍💋‍👨', true], - ['👩‍❤️‍💋‍👩', true], - ['👩', true], - ['👪', true], - ['👫🏻', true], - ['👫🏼', true], - ['👫🏽', true], - ['👫🏾', true], - ['👫🏿', true], - ['👫', true], - ['👬🏻', true], - ['👬🏼', true], - ['👬🏽', true], - ['👬🏾', true], - ['👬🏿', true], - ['👬', true], - ['👭🏻', true], - ['👭🏼', true], - ['👭🏽', true], - ['👭🏾', true], - ['👭🏿', true], - ['👭', true], - ['👮🏻‍♀️', true], - ['👮🏻‍♂️', true], - ['👮🏻', true], - ['👮🏼‍♀️', true], - ['👮🏼‍♂️', true], - ['👮🏼', true], - ['👮🏽‍♀️', true], - ['👮🏽‍♂️', true], - ['👮🏽', true], - ['👮🏾‍♀️', true], - ['👮🏾‍♂️', true], - ['👮🏾', true], - ['👮🏿‍♀️', true], - ['👮🏿‍♂️', true], - ['👮🏿', true], - ['👮‍♀️', true], - ['👮‍♂️', true], - ['👮', true], - ['👯‍♀️', true], - ['👯‍♂️', true], - ['👯', true], - ['👰🏻', true], - ['👰🏼', true], - ['👰🏽', true], - ['👰🏾', true], - ['👰🏿', true], - ['👰', true], - ['👱🏻‍♀️', true], - ['👱🏻‍♂️', true], - ['👱🏻', true], - ['👱🏼‍♀️', true], - ['👱🏼‍♂️', true], - ['👱🏼', true], - ['👱🏽‍♀️', true], - ['👱🏽‍♂️', true], - ['👱🏽', true], - ['👱🏾‍♀️', true], - ['👱🏾‍♂️', true], - ['👱🏾', true], - ['👱🏿‍♀️', true], - ['👱🏿‍♂️', true], - ['👱🏿', true], - ['👱‍♀️', true], - ['👱‍♂️', true], - ['👱', true], - ['👲🏻', true], - ['👲🏼', true], - ['👲🏽', true], - ['👲🏾', true], - ['👲🏿', true], - ['👲', true], - ['👳🏻‍♀️', true], - ['👳🏻‍♂️', true], - ['👳🏻', true], - ['👳🏼‍♀️', true], - ['👳🏼‍♂️', true], - ['👳🏼', true], - ['👳🏽‍♀️', true], - ['👳🏽‍♂️', true], - ['👳🏽', true], - ['👳🏾‍♀️', true], - ['👳🏾‍♂️', true], - ['👳🏾', true], - ['👳🏿‍♀️', true], - ['👳🏿‍♂️', true], - ['👳🏿', true], - ['👳‍♀️', true], - ['👳‍♂️', true], - ['👳', true], - ['👴🏻', true], - ['👴🏼', true], - ['👴🏽', true], - ['👴🏾', true], - ['👴🏿', true], - ['👴', true], - ['👵🏻', true], - ['👵🏼', true], - ['👵🏽', true], - ['👵🏾', true], - ['👵🏿', true], - ['👵', true], - ['👶🏻', true], - ['👶🏼', true], - ['👶🏽', true], - ['👶🏾', true], - ['👶🏿', true], - ['👶', true], - ['👷🏻‍♀️', true], - ['👷🏻‍♂️', true], - ['👷🏻', true], - ['👷🏼‍♀️', true], - ['👷🏼‍♂️', true], - ['👷🏼', true], - ['👷🏽‍♀️', true], - ['👷🏽‍♂️', true], - ['👷🏽', true], - ['👷🏾‍♀️', true], - ['👷🏾‍♂️', true], - ['👷🏾', true], - ['👷🏿‍♀️', true], - ['👷🏿‍♂️', true], - ['👷🏿', true], - ['👷‍♀️', true], - ['👷‍♂️', true], - ['👷', true], - ['👸🏻', true], - ['👸🏼', true], - ['👸🏽', true], - ['👸🏾', true], - ['👸🏿', true], - ['👸', true], - ['👹', true], - ['👺', true], - ['👻', true], - ['👼🏻', true], - ['👼🏼', true], - ['👼🏽', true], - ['👼🏾', true], - ['👼🏿', true], - ['👼', true], - ['👽', true], - ['👾', true], - ['👿', true], - ['💀', true], - ['💁🏻‍♀️', true], - ['💁🏻‍♂️', true], - ['💁🏻', true], - ['💁🏼‍♀️', true], - ['💁🏼‍♂️', true], - ['💁🏼', true], - ['💁🏽‍♀️', true], - ['💁🏽‍♂️', true], - ['💁🏽', true], - ['💁🏾‍♀️', true], - ['💁🏾‍♂️', true], - ['💁🏾', true], - ['💁🏿‍♀️', true], - ['💁🏿‍♂️', true], - ['💁🏿', true], - ['💁‍♀️', true], - ['💁‍♂️', true], - ['💁', true], - ['💂🏻‍♀️', true], - ['💂🏻‍♂️', true], - ['💂🏻', true], - ['💂🏼‍♀️', true], - ['💂🏼‍♂️', true], - ['💂🏼', true], - ['💂🏽‍♀️', true], - ['💂🏽‍♂️', true], - ['💂🏽', true], - ['💂🏾‍♀️', true], - ['💂🏾‍♂️', true], - ['💂🏾', true], - ['💂🏿‍♀️', true], - ['💂🏿‍♂️', true], - ['💂🏿', true], - ['💂‍♀️', true], - ['💂‍♂️', true], - ['💂', true], - ['💃🏻', true], - ['💃🏼', true], - ['💃🏽', true], - ['💃🏾', true], - ['💃🏿', true], - ['💃', true], - ['💄', true], - ['💅🏻', true], - ['💅🏼', true], - ['💅🏽', true], - ['💅🏾', true], - ['💅🏿', true], - ['💅', true], - ['💆🏻‍♀️', true], - ['💆🏻‍♂️', true], - ['💆🏻', true], - ['💆🏼‍♀️', true], - ['💆🏼‍♂️', true], - ['💆🏼', true], - ['💆🏽‍♀️', true], - ['💆🏽‍♂️', true], - ['💆🏽', true], - ['💆🏾‍♀️', true], - ['💆🏾‍♂️', true], - ['💆🏾', true], - ['💆🏿‍♀️', true], - ['💆🏿‍♂️', true], - ['💆🏿', true], - ['💆‍♀️', true], - ['💆‍♂️', true], - ['💆', true], - ['💇🏻‍♀️', true], - ['💇🏻‍♂️', true], - ['💇🏻', true], - ['💇🏼‍♀️', true], - ['💇🏼‍♂️', true], - ['💇🏼', true], - ['💇🏽‍♀️', true], - ['💇🏽‍♂️', true], - ['💇🏽', true], - ['💇🏾‍♀️', true], - ['💇🏾‍♂️', true], - ['💇🏾', true], - ['💇🏿‍♀️', true], - ['💇🏿‍♂️', true], - ['💇🏿', true], - ['💇‍♀️', true], - ['💇‍♂️', true], - ['💇', true], - ['💈', true], - ['💉', true], - ['💊', true], - ['💋', true], - ['💌', true], - ['💍', true], - ['💎', true], - ['💏', true], - ['💐', true], - ['💑', true], - ['💒', true], - ['💓', true], - ['💔', true], - ['💕', true], - ['💖', true], - ['💗', true], - ['💘', true], - ['💙', true], - ['💚', true], - ['💛', true], - ['💜', true], - ['💝', true], - ['💞', true], - ['💟', true], - ['💠', true], - ['💡', true], - ['💢', true], - ['💣', true], - ['💤', true], - ['💥', true], - ['💦', true], - ['💧', true], - ['💨', true], - ['💩', true], - ['💪🏻', true], - ['💪🏼', true], - ['💪🏽', true], - ['💪🏾', true], - ['💪🏿', true], - ['💪', true], - ['💫', true], - ['💬', true], - ['💭', true], - ['💮', true], - ['💯', true], - ['💰', true], - ['💱', true], - ['💲', true], - ['💳', true], - ['💴', true], - ['💵', true], - ['💶', true], - ['💷', true], - ['💸', true], - ['💹', true], - ['💺', true], - ['💻', true], - ['💼', true], - ['💽', true], - ['💾', true], - ['💿', true], - ['📀', true], - ['📁', true], - ['📂', true], - ['📃', true], - ['📄', true], - ['📅', true], - ['📆', true], - ['📇', true], - ['📈', true], - ['📉', true], - ['📊', true], - ['📋', true], - ['📌', true], - ['📍', true], - ['📎', true], - ['📏', true], - ['📐', true], - ['📑', true], - ['📒', true], - ['📓', true], - ['📔', true], - ['📕', true], - ['📖', true], - ['📗', true], - ['📘', true], - ['📙', true], - ['📚', true], - ['📛', true], - ['📜', true], - ['📝', true], - ['📞', true], - ['📟', true], - ['📠', true], - ['📡', true], - ['📢', true], - ['📣', true], - ['📤', true], - ['📥', true], - ['📦', true], - ['📧', true], - ['📨', true], - ['📩', true], - ['📪', true], - ['📫', true], - ['📬', true], - ['📭', true], - ['📮', true], - ['📯', true], - ['📰', true], - ['📱', true], - ['📲', true], - ['📳', true], - ['📴', true], - ['📵', true], - ['📶', true], - ['📷', true], - ['📸', true], - ['📹', true], - ['📺', true], - ['📻', true], - ['📼', true], - ['📽️', true], - ['📿', true], - ['🔀', true], - ['🔁', true], - ['🔂', true], - ['🔃', true], - ['🔄', true], - ['🔅', true], - ['🔆', true], - ['🔇', true], - ['🔈', true], - ['🔉', true], - ['🔊', true], - ['🔋', true], - ['🔌', true], - ['🔍', true], - ['🔎', true], - ['🔏', true], - ['🔐', true], - ['🔑', true], - ['🔒', true], - ['🔓', true], - ['🔔', true], - ['🔕', true], - ['🔖', true], - ['🔗', true], - ['🔘', true], - ['🔙', true], - ['🔚', true], - ['🔛', true], - ['🔜', true], - ['🔝', true], - ['🔞', true], - ['🔟', true], - ['🔠', true], - ['🔡', true], - ['🔢', true], - ['🔣', true], - ['🔤', true], - ['🔥', true], - ['🔦', true], - ['🔧', true], - ['🔨', true], - ['🔩', true], - ['🔪', true], - ['🔫', true], - ['🔬', true], - ['🔭', true], - ['🔮', true], - ['🔯', true], - ['🔰', true], - ['🔱', true], - ['🔲', true], - ['🔳', true], - ['🔴', true], - ['🔵', true], - ['🔶', true], - ['🔷', true], - ['🔸', true], - ['🔹', true], - ['🔺', true], - ['🔻', true], - ['🔼', true], - ['🔽', true], - ['🕉️', true], - ['🕊️', true], - ['🕋', true], - ['🕌', true], - ['🕍', true], - ['🕎', true], - ['🕐', true], - ['🕑', true], - ['🕒', true], - ['🕓', true], - ['🕔', true], - ['🕕', true], - ['🕖', true], - ['🕗', true], - ['🕘', true], - ['🕙', true], - ['🕚', true], - ['🕛', true], - ['🕜', true], - ['🕝', true], - ['🕞', true], - ['🕟', true], - ['🕠', true], - ['🕡', true], - ['🕢', true], - ['🕣', true], - ['🕤', true], - ['🕥', true], - ['🕦', true], - ['🕧', true], - ['🕯️', true], - ['🕰️', true], - ['🕳️', true], - ['🕴🏻‍♀️', true], - ['🕴🏻‍♂️', true], - ['🕴🏻', true], - ['🕴🏼‍♀️', true], - ['🕴🏼‍♂️', true], - ['🕴🏼', true], - ['🕴🏽‍♀️', true], - ['🕴🏽‍♂️', true], - ['🕴🏽', true], - ['🕴🏾‍♀️', true], - ['🕴🏾‍♂️', true], - ['🕴🏾', true], - ['🕴🏿‍♀️', true], - ['🕴🏿‍♂️', true], - ['🕴🏿', true], - ['🕴️‍♀️', true], - ['🕴️‍♂️', true], - ['🕴️', true], - ['🕵🏻‍♀️', true], - ['🕵🏻‍♂️', true], - ['🕵🏻', true], - ['🕵🏼‍♀️', true], - ['🕵🏼‍♂️', true], - ['🕵🏼', true], - ['🕵🏽‍♀️', true], - ['🕵🏽‍♂️', true], - ['🕵🏽', true], - ['🕵🏾‍♀️', true], - ['🕵🏾‍♂️', true], - ['🕵🏾', true], - ['🕵🏿‍♀️', true], - ['🕵🏿‍♂️', true], - ['🕵🏿', true], - ['🕵️‍♀️', true], - ['🕵️‍♂️', true], - ['🕵️', true], - ['🕶️', true], - ['🕷️', true], - ['🕸️', true], - ['🕹️', true], - ['🕺🏻', true], - ['🕺🏼', true], - ['🕺🏽', true], - ['🕺🏾', true], - ['🕺🏿', true], - ['🕺', true], - ['🖇️', true], - ['🖊️', true], - ['🖋️', true], - ['🖌️', true], - ['🖍️', true], - ['🖐🏻', true], - ['🖐🏼', true], - ['🖐🏽', true], - ['🖐🏾', true], - ['🖐🏿', true], - ['🖐️', true], - ['🖕🏻', true], - ['🖕🏼', true], - ['🖕🏽', true], - ['🖕🏾', true], - ['🖕🏿', true], - ['🖕', true], - ['🖖🏻', true], - ['🖖🏼', true], - ['🖖🏽', true], - ['🖖🏾', true], - ['🖖🏿', true], - ['🖖', true], - ['🖤', true], - ['🖥️', true], - ['🖨️', true], - ['🖱️', true], - ['🖲️', true], - ['🖼️', true], - ['🗂️', true], - ['🗃️', true], - ['🗄️', true], - ['🗑️', true], - ['🗒️', true], - ['🗓️', true], - ['🗜️', true], - ['🗝️', true], - ['🗞️', true], - ['🗡️', true], - ['🗣️', true], - ['🗨️', true], - ['🗯️', true], - ['🗳️', true], - ['🗺️', true], - ['🗻', true], - ['🗼', true], - ['🗽', true], - ['🗾', true], - ['🗿', true], - ['😀', true], - ['😁', true], - ['😂', true], - ['😃', true], - ['😄', true], - ['😅', true], - ['😆', true], - ['😇', true], - ['😈', true], - ['😉', true], - ['😊', true], - ['😋', true], - ['😌', true], - ['😍', true], - ['😎', true], - ['😏', true], - ['😐', true], - ['😑', true], - ['😒', true], - ['😓', true], - ['😔', true], - ['😕', true], - ['😖', true], - ['😗', true], - ['😘', true], - ['😙', true], - ['😚', true], - ['😛', true], - ['😜', true], - ['😝', true], - ['😞', true], - ['😟', true], - ['😠', true], - ['😡', true], - ['😢', true], - ['😣', true], - ['😤', true], - ['😥', true], - ['😦', true], - ['😧', true], - ['😨', true], - ['😩', true], - ['😪', true], - ['😫', true], - ['😬', true], - ['😭', true], - ['😮', true], - ['😯', true], - ['😰', true], - ['😱', true], - ['😲', true], - ['😳', true], - ['😴', true], - ['😵', true], - ['😶', true], - ['😷', true], - ['😸', true], - ['😹', true], - ['😺', true], - ['😻', true], - ['😼', true], - ['😽', true], - ['😾', true], - ['😿', true], - ['🙀', true], - ['🙁', true], - ['🙂', true], - ['🙃', true], - ['🙄', true], - ['🙅🏻‍♀️', true], - ['🙅🏻‍♂️', true], - ['🙅🏻', true], - ['🙅🏼‍♀️', true], - ['🙅🏼‍♂️', true], - ['🙅🏼', true], - ['🙅🏽‍♀️', true], - ['🙅🏽‍♂️', true], - ['🙅🏽', true], - ['🙅🏾‍♀️', true], - ['🙅🏾‍♂️', true], - ['🙅🏾', true], - ['🙅🏿‍♀️', true], - ['🙅🏿‍♂️', true], - ['🙅🏿', true], - ['🙅‍♀️', true], - ['🙅‍♂️', true], - ['🙅', true], - ['🙆🏻‍♀️', true], - ['🙆🏻‍♂️', true], - ['🙆🏻', true], - ['🙆🏼‍♀️', true], - ['🙆🏼‍♂️', true], - ['🙆🏼', true], - ['🙆🏽‍♀️', true], - ['🙆🏽‍♂️', true], - ['🙆🏽', true], - ['🙆🏾‍♀️', true], - ['🙆🏾‍♂️', true], - ['🙆🏾', true], - ['🙆🏿‍♀️', true], - ['🙆🏿‍♂️', true], - ['🙆🏿', true], - ['🙆‍♀️', true], - ['🙆‍♂️', true], - ['🙆', true], - ['🙇🏻‍♀️', true], - ['🙇🏻‍♂️', true], - ['🙇🏻', true], - ['🙇🏼‍♀️', true], - ['🙇🏼‍♂️', true], - ['🙇🏼', true], - ['🙇🏽‍♀️', true], - ['🙇🏽‍♂️', true], - ['🙇🏽', true], - ['🙇🏾‍♀️', true], - ['🙇🏾‍♂️', true], - ['🙇🏾', true], - ['🙇🏿‍♀️', true], - ['🙇🏿‍♂️', true], - ['🙇🏿', true], - ['🙇‍♀️', true], - ['🙇‍♂️', true], - ['🙇', true], - ['🙈', true], - ['🙉', true], - ['🙊', true], - ['🙋🏻‍♀️', true], - ['🙋🏻‍♂️', true], - ['🙋🏻', true], - ['🙋🏼‍♀️', true], - ['🙋🏼‍♂️', true], - ['🙋🏼', true], - ['🙋🏽‍♀️', true], - ['🙋🏽‍♂️', true], - ['🙋🏽', true], - ['🙋🏾‍♀️', true], - ['🙋🏾‍♂️', true], - ['🙋🏾', true], - ['🙋🏿‍♀️', true], - ['🙋🏿‍♂️', true], - ['🙋🏿', true], - ['🙋‍♀️', true], - ['🙋‍♂️', true], - ['🙋', true], - ['🙌🏻', true], - ['🙌🏼', true], - ['🙌🏽', true], - ['🙌🏾', true], - ['🙌🏿', true], - ['🙌', true], - ['🙍🏻‍♀️', true], - ['🙍🏻‍♂️', true], - ['🙍🏻', true], - ['🙍🏼‍♀️', true], - ['🙍🏼‍♂️', true], - ['🙍🏼', true], - ['🙍🏽‍♀️', true], - ['🙍🏽‍♂️', true], - ['🙍🏽', true], - ['🙍🏾‍♀️', true], - ['🙍🏾‍♂️', true], - ['🙍🏾', true], - ['🙍🏿‍♀️', true], - ['🙍🏿‍♂️', true], - ['🙍🏿', true], - ['🙍‍♀️', true], - ['🙍‍♂️', true], - ['🙍', true], - ['🙎🏻‍♀️', true], - ['🙎🏻‍♂️', true], - ['🙎🏻', true], - ['🙎🏼‍♀️', true], - ['🙎🏼‍♂️', true], - ['🙎🏼', true], - ['🙎🏽‍♀️', true], - ['🙎🏽‍♂️', true], - ['🙎🏽', true], - ['🙎🏾‍♀️', true], - ['🙎🏾‍♂️', true], - ['🙎🏾', true], - ['🙎🏿‍♀️', true], - ['🙎🏿‍♂️', true], - ['🙎🏿', true], - ['🙎‍♀️', true], - ['🙎‍♂️', true], - ['🙎', true], - ['🙏🏻', true], - ['🙏🏼', true], - ['🙏🏽', true], - ['🙏🏾', true], - ['🙏🏿', true], - ['🙏', true], - ['🚀', true], - ['🚁', true], - ['🚂', true], - ['🚃', true], - ['🚄', true], - ['🚅', true], - ['🚆', true], - ['🚇', true], - ['🚈', true], - ['🚉', true], - ['🚊', true], - ['🚋', true], - ['🚌', true], - ['🚍', true], - ['🚎', true], - ['🚏', true], - ['🚐', true], - ['🚑', true], - ['🚒', true], - ['🚓', true], - ['🚔', true], - ['🚕', true], - ['🚖', true], - ['🚗', true], - ['🚘', true], - ['🚙', true], - ['🚚', true], - ['🚛', true], - ['🚜', true], - ['🚝', true], - ['🚞', true], - ['🚟', true], - ['🚠', true], - ['🚡', true], - ['🚢', true], - ['🚣🏻‍♀️', true], - ['🚣🏻‍♂️', true], - ['🚣🏻', true], - ['🚣🏼‍♀️', true], - ['🚣🏼‍♂️', true], - ['🚣🏼', true], - ['🚣🏽‍♀️', true], - ['🚣🏽‍♂️', true], - ['🚣🏽', true], - ['🚣🏾‍♀️', true], - ['🚣🏾‍♂️', true], - ['🚣🏾', true], - ['🚣🏿‍♀️', true], - ['🚣🏿‍♂️', true], - ['🚣🏿', true], - ['🚣‍♀️', true], - ['🚣‍♂️', true], - ['🚣', true], - ['🚤', true], - ['🚥', true], - ['🚦', true], - ['🚧', true], - ['🚨', true], - ['🚩', true], - ['🚪', true], - ['🚫', true], - ['🚬', true], - ['🚭', true], - ['🚮', true], - ['🚯', true], - ['🚰', true], - ['🚱', true], - ['🚲', true], - ['🚳', true], - ['🚴🏻‍♀️', true], - ['🚴🏻‍♂️', true], - ['🚴🏻', true], - ['🚴🏼‍♀️', true], - ['🚴🏼‍♂️', true], - ['🚴🏼', true], - ['🚴🏽‍♀️', true], - ['🚴🏽‍♂️', true], - ['🚴🏽', true], - ['🚴🏾‍♀️', true], - ['🚴🏾‍♂️', true], - ['🚴🏾', true], - ['🚴🏿‍♀️', true], - ['🚴🏿‍♂️', true], - ['🚴🏿', true], - ['🚴‍♀️', true], - ['🚴‍♂️', true], - ['🚴', true], - ['🚵🏻‍♀️', true], - ['🚵🏻‍♂️', true], - ['🚵🏻', true], - ['🚵🏼‍♀️', true], - ['🚵🏼‍♂️', true], - ['🚵🏼', true], - ['🚵🏽‍♀️', true], - ['🚵🏽‍♂️', true], - ['🚵🏽', true], - ['🚵🏾‍♀️', true], - ['🚵🏾‍♂️', true], - ['🚵🏾', true], - ['🚵🏿‍♀️', true], - ['🚵🏿‍♂️', true], - ['🚵🏿', true], - ['🚵‍♀️', true], - ['🚵‍♂️', true], - ['🚵', true], - ['🚶🏻‍♀️', true], - ['🚶🏻‍♂️', true], - ['🚶🏻', true], - ['🚶🏼‍♀️', true], - ['🚶🏼‍♂️', true], - ['🚶🏼', true], - ['🚶🏽‍♀️', true], - ['🚶🏽‍♂️', true], - ['🚶🏽', true], - ['🚶🏾‍♀️', true], - ['🚶🏾‍♂️', true], - ['🚶🏾', true], - ['🚶🏿‍♀️', true], - ['🚶🏿‍♂️', true], - ['🚶🏿', true], - ['🚶‍♀️', true], - ['🚶‍♂️', true], - ['🚶', true], - ['🚷', true], - ['🚸', true], - ['🚹', true], - ['🚺', true], - ['🚻', true], - ['🚼', true], - ['🚽', true], - ['🚾', true], - ['🚿', true], - ['🛀🏻', true], - ['🛀🏼', true], - ['🛀🏽', true], - ['🛀🏾', true], - ['🛀🏿', true], - ['🛀', true], - ['🛁', true], - ['🛂', true], - ['🛃', true], - ['🛄', true], - ['🛅', true], - ['🛋️', true], - ['🛌🏻', true], - ['🛌🏼', true], - ['🛌🏽', true], - ['🛌🏾', true], - ['🛌🏿', true], - ['🛌', true], - ['🛍️', true], - ['🛎️', true], - ['🛏️', true], - ['🛐', true], - ['🛑', true], - ['🛒', true], - ['🛕', true], - ['🛠️', true], - ['🛡️', true], - ['🛢️', true], - ['🛣️', true], - ['🛤️', true], - ['🛥️', true], - ['🛩️', true], - ['🛫', true], - ['🛬', true], - ['🛰️', true], - ['🛳️', true], - ['🛴', true], - ['🛵', true], - ['🛶', true], - ['🛷', true], - ['🛸', true], - ['🛹', true], - ['🛺', true], - ['🟠', true], - ['🟡', true], - ['🟢', true], - ['🟣', true], - ['🟤', true], - ['🟥', true], - ['🟦', true], - ['🟧', true], - ['🟨', true], - ['🟩', true], - ['🟪', true], - ['🟫', true], - ['🤍', true], - ['🤎', true], - ['🤏🏻', true], - ['🤏🏼', true], - ['🤏🏽', true], - ['🤏🏾', true], - ['🤏🏿', true], - ['🤏', true], - ['🤐', true], - ['🤑', true], - ['🤒', true], - ['🤓', true], - ['🤔', true], - ['🤕', true], - ['🤖', true], - ['🤗', true], - ['🤘🏻', true], - ['🤘🏼', true], - ['🤘🏽', true], - ['🤘🏾', true], - ['🤘🏿', true], - ['🤘', true], - ['🤙🏻', true], - ['🤙🏼', true], - ['🤙🏽', true], - ['🤙🏾', true], - ['🤙🏿', true], - ['🤙', true], - ['🤚🏻', true], - ['🤚🏼', true], - ['🤚🏽', true], - ['🤚🏾', true], - ['🤚🏿', true], - ['🤚', true], - ['🤛🏻', true], - ['🤛🏼', true], - ['🤛🏽', true], - ['🤛🏾', true], - ['🤛🏿', true], - ['🤛', true], - ['🤜🏻', true], - ['🤜🏼', true], - ['🤜🏽', true], - ['🤜🏾', true], - ['🤜🏿', true], - ['🤜', true], - ['🤝', true], - ['🤞🏻', true], - ['🤞🏼', true], - ['🤞🏽', true], - ['🤞🏾', true], - ['🤞🏿', true], - ['🤞', true], - ['🤟🏻', true], - ['🤟🏼', true], - ['🤟🏽', true], - ['🤟🏾', true], - ['🤟🏿', true], - ['🤟', true], - ['🤠', true], - ['🤡', true], - ['🤢', true], - ['🤣', true], - ['🤤', true], - ['🤥', true], - ['🤦🏻‍♀️', true], - ['🤦🏻‍♂️', true], - ['🤦🏻', true], - ['🤦🏼‍♀️', true], - ['🤦🏼‍♂️', true], - ['🤦🏼', true], - ['🤦🏽‍♀️', true], - ['🤦🏽‍♂️', true], - ['🤦🏽', true], - ['🤦🏾‍♀️', true], - ['🤦🏾‍♂️', true], - ['🤦🏾', true], - ['🤦🏿‍♀️', true], - ['🤦🏿‍♂️', true], - ['🤦🏿', true], - ['🤦‍♀️', true], - ['🤦‍♂️', true], - ['🤦', true], - ['🤧', true], - ['🤨', true], - ['🤩', true], - ['🤪', true], - ['🤫', true], - ['🤬', true], - ['🤭', true], - ['🤮', true], - ['🤯', true], - ['🤰🏻', true], - ['🤰🏼', true], - ['🤰🏽', true], - ['🤰🏾', true], - ['🤰🏿', true], - ['🤰', true], - ['🤱🏻', true], - ['🤱🏼', true], - ['🤱🏽', true], - ['🤱🏾', true], - ['🤱🏿', true], - ['🤱', true], - ['🤲🏻', true], - ['🤲🏼', true], - ['🤲🏽', true], - ['🤲🏾', true], - ['🤲🏿', true], - ['🤲', true], - ['🤳🏻', true], - ['🤳🏼', true], - ['🤳🏽', true], - ['🤳🏾', true], - ['🤳🏿', true], - ['🤳', true], - ['🤴🏻', true], - ['🤴🏼', true], - ['🤴🏽', true], - ['🤴🏾', true], - ['🤴🏿', true], - ['🤴', true], - ['🤵🏻‍♀️', true], - ['🤵🏻‍♂️', true], - ['🤵🏻', true], - ['🤵🏼‍♀️', true], - ['🤵🏼‍♂️', true], - ['🤵🏼', true], - ['🤵🏽‍♀️', true], - ['🤵🏽‍♂️', true], - ['🤵🏽', true], - ['🤵🏾‍♀️', true], - ['🤵🏾‍♂️', true], - ['🤵🏾', true], - ['🤵🏿‍♀️', true], - ['🤵🏿‍♂️', true], - ['🤵🏿', true], - ['🤵‍♀️', true], - ['🤵‍♂️', true], - ['🤵', true], - ['🤶🏻', true], - ['🤶🏼', true], - ['🤶🏽', true], - ['🤶🏾', true], - ['🤶🏿', true], - ['🤶', true], - ['🤷🏻‍♀️', true], - ['🤷🏻‍♂️', true], - ['🤷🏻', true], - ['🤷🏼‍♀️', true], - ['🤷🏼‍♂️', true], - ['🤷🏼', true], - ['🤷🏽‍♀️', true], - ['🤷🏽‍♂️', true], - ['🤷🏽', true], - ['🤷🏾‍♀️', true], - ['🤷🏾‍♂️', true], - ['🤷🏾', true], - ['🤷🏿‍♀️', true], - ['🤷🏿‍♂️', true], - ['🤷🏿', true], - ['🤷‍♀️', true], - ['🤷‍♂️', true], - ['🤷', true], - ['🤸🏻‍♀️', true], - ['🤸🏻‍♂️', true], - ['🤸🏻', true], - ['🤸🏼‍♀️', true], - ['🤸🏼‍♂️', true], - ['🤸🏼', true], - ['🤸🏽‍♀️', true], - ['🤸🏽‍♂️', true], - ['🤸🏽', true], - ['🤸🏾‍♀️', true], - ['🤸🏾‍♂️', true], - ['🤸🏾', true], - ['🤸🏿‍♀️', true], - ['🤸🏿‍♂️', true], - ['🤸🏿', true], - ['🤸‍♀️', true], - ['🤸‍♂️', true], - ['🤸', true], - ['🤹🏻‍♀️', true], - ['🤹🏻‍♂️', true], - ['🤹🏻', true], - ['🤹🏼‍♀️', true], - ['🤹🏼‍♂️', true], - ['🤹🏼', true], - ['🤹🏽‍♀️', true], - ['🤹🏽‍♂️', true], - ['🤹🏽', true], - ['🤹🏾‍♀️', true], - ['🤹🏾‍♂️', true], - ['🤹🏾', true], - ['🤹🏿‍♀️', true], - ['🤹🏿‍♂️', true], - ['🤹🏿', true], - ['🤹‍♀️', true], - ['🤹‍♂️', true], - ['🤹', true], - ['🤺', true], - ['🤼‍♀️', true], - ['🤼‍♂️', true], - ['🤼', true], - ['🤽🏻‍♀️', true], - ['🤽🏻‍♂️', true], - ['🤽🏻', true], - ['🤽🏼‍♀️', true], - ['🤽🏼‍♂️', true], - ['🤽🏼', true], - ['🤽🏽‍♀️', true], - ['🤽🏽‍♂️', true], - ['🤽🏽', true], - ['🤽🏾‍♀️', true], - ['🤽🏾‍♂️', true], - ['🤽🏾', true], - ['🤽🏿‍♀️', true], - ['🤽🏿‍♂️', true], - ['🤽🏿', true], - ['🤽‍♀️', true], - ['🤽‍♂️', true], - ['🤽', true], - ['🤾🏻‍♀️', true], - ['🤾🏻‍♂️', true], - ['🤾🏻', true], - ['🤾🏼‍♀️', true], - ['🤾🏼‍♂️', true], - ['🤾🏼', true], - ['🤾🏽‍♀️', true], - ['🤾🏽‍♂️', true], - ['🤾🏽', true], - ['🤾🏾‍♀️', true], - ['🤾🏾‍♂️', true], - ['🤾🏾', true], - ['🤾🏿‍♀️', true], - ['🤾🏿‍♂️', true], - ['🤾🏿', true], - ['🤾‍♀️', true], - ['🤾‍♂️', true], - ['🤾', true], - ['🤿', true], - ['🥀', true], - ['🥁', true], - ['🥂', true], - ['🥃', true], - ['🥄', true], - ['🥅', true], - ['🥇', true], - ['🥈', true], - ['🥉', true], - ['🥊', true], - ['🥋', true], - ['🥌', true], - ['🥍', true], - ['🥎', true], - ['🥏', true], - ['🥐', true], - ['🥑', true], - ['🥒', true], - ['🥓', true], - ['🥔', true], - ['🥕', true], - ['🥖', true], - ['🥗', true], - ['🥘', true], - ['🥙', true], - ['🥚', true], - ['🥛', true], - ['🥜', true], - ['🥝', true], - ['🥞', true], - ['🥟', true], - ['🥠', true], - ['🥡', true], - ['🥢', true], - ['🥣', true], - ['🥤', true], - ['🥥', true], - ['🥦', true], - ['🥧', true], - ['🥨', true], - ['🥩', true], - ['🥪', true], - ['🥫', true], - ['🥬', true], - ['🥭', true], - ['🥮', true], - ['🥯', true], - ['🥰', true], - ['🥱', true], - ['🥳', true], - ['🥴', true], - ['🥵', true], - ['🥶', true], - ['🥺', true], - ['🥻', true], - ['🥼', true], - ['🥽', true], - ['🥾', true], - ['🥿', true], - ['🦀', true], - ['🦁', true], - ['🦂', true], - ['🦃', true], - ['🦄', true], - ['🦅', true], - ['🦆', true], - ['🦇', true], - ['🦈', true], - ['🦉', true], - ['🦊', true], - ['🦋', true], - ['🦌', true], - ['🦍', true], - ['🦎', true], - ['🦏', true], - ['🦐', true], - ['🦑', true], - ['🦒', true], - ['🦓', true], - ['🦔', true], - ['🦕', true], - ['🦖', true], - ['🦗', true], - ['🦘', true], - ['🦙', true], - ['🦚', true], - ['🦛', true], - ['🦜', true], - ['🦝', true], - ['🦞', true], - ['🦟', true], - ['🦠', true], - ['🦡', true], - ['🦢', true], - ['🦥', true], - ['🦦', true], - ['🦧', true], - ['🦨', true], - ['🦩', true], - ['🦪', true], - ['🦮', true], - ['🦯', true], - ['🦰', true], - ['🦱', true], - ['🦲', true], - ['🦳', true], - ['🦴', true], - ['🦵🏻', true], - ['🦵🏼', true], - ['🦵🏽', true], - ['🦵🏾', true], - ['🦵🏿', true], - ['🦵', true], - ['🦶🏻', true], - ['🦶🏼', true], - ['🦶🏽', true], - ['🦶🏾', true], - ['🦶🏿', true], - ['🦶', true], - ['🦷', true], - ['🦸🏻‍♀️', true], - ['🦸🏻‍♂️', true], - ['🦸🏻', true], - ['🦸🏼‍♀️', true], - ['🦸🏼‍♂️', true], - ['🦸🏼', true], - ['🦸🏽‍♀️', true], - ['🦸🏽‍♂️', true], - ['🦸🏽', true], - ['🦸🏾‍♀️', true], - ['🦸🏾‍♂️', true], - ['🦸🏾', true], - ['🦸🏿‍♀️', true], - ['🦸🏿‍♂️', true], - ['🦸🏿', true], - ['🦸‍♀️', true], - ['🦸‍♂️', true], - ['🦸', true], - ['🦹🏻‍♀️', true], - ['🦹🏻‍♂️', true], - ['🦹🏻', true], - ['🦹🏼‍♀️', true], - ['🦹🏼‍♂️', true], - ['🦹🏼', true], - ['🦹🏽‍♀️', true], - ['🦹🏽‍♂️', true], - ['🦹🏽', true], - ['🦹🏾‍♀️', true], - ['🦹🏾‍♂️', true], - ['🦹🏾', true], - ['🦹🏿‍♀️', true], - ['🦹🏿‍♂️', true], - ['🦹🏿', true], - ['🦹‍♀️', true], - ['🦹‍♂️', true], - ['🦹', true], - ['🦺', true], - ['🦻🏻', true], - ['🦻🏼', true], - ['🦻🏽', true], - ['🦻🏾', true], - ['🦻🏿', true], - ['🦻', true], - ['🦼', true], - ['🦽', true], - ['🦾', true], - ['🦿', true], - ['🧀', true], - ['🧁', true], - ['🧂', true], - ['🧃', true], - ['🧄', true], - ['🧅', true], - ['🧆', true], - ['🧇', true], - ['🧈', true], - ['🧉', true], - ['🧊', true], - ['🧍🏻‍♀️', true], - ['🧍🏻‍♂️', true], - ['🧍🏻', true], - ['🧍🏼‍♀️', true], - ['🧍🏼‍♂️', true], - ['🧍🏼', true], - ['🧍🏽‍♀️', true], - ['🧍🏽‍♂️', true], - ['🧍🏽', true], - ['🧍🏾‍♀️', true], - ['🧍🏾‍♂️', true], - ['🧍🏾', true], - ['🧍🏿‍♀️', true], - ['🧍🏿‍♂️', true], - ['🧍🏿', true], - ['🧍‍♀️', true], - ['🧍‍♂️', true], - ['🧍', true], - ['🧎🏻‍♀️', true], - ['🧎🏻‍♂️', true], - ['🧎🏻', true], - ['🧎🏼‍♀️', true], - ['🧎🏼‍♂️', true], - ['🧎🏼', true], - ['🧎🏽‍♀️', true], - ['🧎🏽‍♂️', true], - ['🧎🏽', true], - ['🧎🏾‍♀️', true], - ['🧎🏾‍♂️', true], - ['🧎🏾', true], - ['🧎🏿‍♀️', true], - ['🧎🏿‍♂️', true], - ['🧎🏿', true], - ['🧎‍♀️', true], - ['🧎‍♂️', true], - ['🧎', true], - ['🧏🏻‍♀️', true], - ['🧏🏻‍♂️', true], - ['🧏🏻', true], - ['🧏🏼‍♀️', true], - ['🧏🏼‍♂️', true], - ['🧏🏼', true], - ['🧏🏽‍♀️', true], - ['🧏🏽‍♂️', true], - ['🧏🏽', true], - ['🧏🏾‍♀️', true], - ['🧏🏾‍♂️', true], - ['🧏🏾', true], - ['🧏🏿‍♀️', true], - ['🧏🏿‍♂️', true], - ['🧏🏿', true], - ['🧏‍♀️', true], - ['🧏‍♂️', true], - ['🧏', true], - ['🧐', true], - ['🧑🏻‍🤝‍🧑🏻', true], - ['🧑🏻', true], - ['🧑🏼‍🤝‍🧑🏻', true], - ['🧑🏼‍🤝‍🧑🏼', true], - ['🧑🏼', true], - ['🧑🏽‍🤝‍🧑🏻', true], - ['🧑🏽‍🤝‍🧑🏼', true], - ['🧑🏽‍🤝‍🧑🏽', true], - ['🧑🏽', true], - ['🧑🏾‍🤝‍🧑🏻', true], - ['🧑🏾‍🤝‍🧑🏼', true], - ['🧑🏾‍🤝‍🧑🏽', true], - ['🧑🏾‍🤝‍🧑🏾', true], - ['🧑🏾', true], - ['🧑🏿‍🤝‍🧑🏻', true], - ['🧑🏿‍🤝‍🧑🏼', true], - ['🧑🏿‍🤝‍🧑🏽', true], - ['🧑🏿‍🤝‍🧑🏾', true], - ['🧑🏿‍🤝‍🧑🏿', true], - ['🧑🏿', true], - ['🧑‍🤝‍🧑', true], - ['🧑', true], - ['🧒🏻', true], - ['🧒🏼', true], - ['🧒🏽', true], - ['🧒🏾', true], - ['🧒🏿', true], - ['🧒', true], - ['🧓🏻', true], - ['🧓🏼', true], - ['🧓🏽', true], - ['🧓🏾', true], - ['🧓🏿', true], - ['🧓', true], - ['🧔🏻', true], - ['🧔🏼', true], - ['🧔🏽', true], - ['🧔🏾', true], - ['🧔🏿', true], - ['🧔', true], - ['🧕🏻', true], - ['🧕🏼', true], - ['🧕🏽', true], - ['🧕🏾', true], - ['🧕🏿', true], - ['🧕', true], - ['🧖🏻‍♀️', true], - ['🧖🏻‍♂️', true], - ['🧖🏻', true], - ['🧖🏼‍♀️', true], - ['🧖🏼‍♂️', true], - ['🧖🏼', true], - ['🧖🏽‍♀️', true], - ['🧖🏽‍♂️', true], - ['🧖🏽', true], - ['🧖🏾‍♀️', true], - ['🧖🏾‍♂️', true], - ['🧖🏾', true], - ['🧖🏿‍♀️', true], - ['🧖🏿‍♂️', true], - ['🧖🏿', true], - ['🧖‍♀️', true], - ['🧖‍♂️', true], - ['🧖', true], - ['🧗🏻‍♀️', true], - ['🧗🏻‍♂️', true], - ['🧗🏻', true], - ['🧗🏼‍♀️', true], - ['🧗🏼‍♂️', true], - ['🧗🏼', true], - ['🧗🏽‍♀️', true], - ['🧗🏽‍♂️', true], - ['🧗🏽', true], - ['🧗🏾‍♀️', true], - ['🧗🏾‍♂️', true], - ['🧗🏾', true], - ['🧗🏿‍♀️', true], - ['🧗🏿‍♂️', true], - ['🧗🏿', true], - ['🧗‍♀️', true], - ['🧗‍♂️', true], - ['🧗', true], - ['🧘🏻‍♀️', true], - ['🧘🏻‍♂️', true], - ['🧘🏻', true], - ['🧘🏼‍♀️', true], - ['🧘🏼‍♂️', true], - ['🧘🏼', true], - ['🧘🏽‍♀️', true], - ['🧘🏽‍♂️', true], - ['🧘🏽', true], - ['🧘🏾‍♀️', true], - ['🧘🏾‍♂️', true], - ['🧘🏾', true], - ['🧘🏿‍♀️', true], - ['🧘🏿‍♂️', true], - ['🧘🏿', true], - ['🧘‍♀️', true], - ['🧘‍♂️', true], - ['🧘', true], - ['🧙🏻‍♀️', true], - ['🧙🏻‍♂️', true], - ['🧙🏻', true], - ['🧙🏼‍♀️', true], - ['🧙🏼‍♂️', true], - ['🧙🏼', true], - ['🧙🏽‍♀️', true], - ['🧙🏽‍♂️', true], - ['🧙🏽', true], - ['🧙🏾‍♀️', true], - ['🧙🏾‍♂️', true], - ['🧙🏾', true], - ['🧙🏿‍♀️', true], - ['🧙🏿‍♂️', true], - ['🧙🏿', true], - ['🧙‍♀️', true], - ['🧙‍♂️', true], - ['🧙', true], - ['🧚🏻‍♀️', true], - ['🧚🏻‍♂️', true], - ['🧚🏻', true], - ['🧚🏼‍♀️', true], - ['🧚🏼‍♂️', true], - ['🧚🏼', true], - ['🧚🏽‍♀️', true], - ['🧚🏽‍♂️', true], - ['🧚🏽', true], - ['🧚🏾‍♀️', true], - ['🧚🏾‍♂️', true], - ['🧚🏾', true], - ['🧚🏿‍♀️', true], - ['🧚🏿‍♂️', true], - ['🧚🏿', true], - ['🧚‍♀️', true], - ['🧚‍♂️', true], - ['🧚', true], - ['🧛🏻‍♀️', true], - ['🧛🏻‍♂️', true], - ['🧛🏻', true], - ['🧛🏼‍♀️', true], - ['🧛🏼‍♂️', true], - ['🧛🏼', true], - ['🧛🏽‍♀️', true], - ['🧛🏽‍♂️', true], - ['🧛🏽', true], - ['🧛🏾‍♀️', true], - ['🧛🏾‍♂️', true], - ['🧛🏾', true], - ['🧛🏿‍♀️', true], - ['🧛🏿‍♂️', true], - ['🧛🏿', true], - ['🧛‍♀️', true], - ['🧛‍♂️', true], - ['🧛', true], - ['🧜🏻‍♀️', true], - ['🧜🏻‍♂️', true], - ['🧜🏻', true], - ['🧜🏼‍♀️', true], - ['🧜🏼‍♂️', true], - ['🧜🏼', true], - ['🧜🏽‍♀️', true], - ['🧜🏽‍♂️', true], - ['🧜🏽', true], - ['🧜🏾‍♀️', true], - ['🧜🏾‍♂️', true], - ['🧜🏾', true], - ['🧜🏿‍♀️', true], - ['🧜🏿‍♂️', true], - ['🧜🏿', true], - ['🧜‍♀️', true], - ['🧜‍♂️', true], - ['🧜', true], - ['🧝🏻‍♀️', true], - ['🧝🏻‍♂️', true], - ['🧝🏻', true], - ['🧝🏼‍♀️', true], - ['🧝🏼‍♂️', true], - ['🧝🏼', true], - ['🧝🏽‍♀️', true], - ['🧝🏽‍♂️', true], - ['🧝🏽', true], - ['🧝🏾‍♀️', true], - ['🧝🏾‍♂️', true], - ['🧝🏾', true], - ['🧝🏿‍♀️', true], - ['🧝🏿‍♂️', true], - ['🧝🏿', true], - ['🧝‍♀️', true], - ['🧝‍♂️', true], - ['🧝', true], - ['🧞‍♀️', true], - ['🧞‍♂️', true], - ['🧞', true], - ['🧟‍♀️', true], - ['🧟‍♂️', true], - ['🧟', true], - ['🧠', true], - ['🧡', true], - ['🧢', true], - ['🧣', true], - ['🧤', true], - ['🧥', true], - ['🧦', true], - ['🧧', true], - ['🧨', true], - ['🧩', true], - ['🧪', true], - ['🧫', true], - ['🧬', true], - ['🧭', true], - ['🧮', true], - ['🧯', true], - ['🧰', true], - ['🧱', true], - ['🧲', true], - ['🧳', true], - ['🧴', true], - ['🧵', true], - ['🧶', true], - ['🧷', true], - ['🧸', true], - ['🧹', true], - ['🧺', true], - ['🧻', true], - ['🧼', true], - ['🧽', true], - ['🧾', true], - ['🧿', true], - ['🩰', true], - ['🩱', true], - ['🩲', true], - ['🩳', true], - ['🩸', true], - ['🩹', true], - ['🩺', true], - ['🪀', true], - ['🪁', true], - ['🪂', true], - ['🪐', true], - ['🪑', true], - ['🪒', true], - ['🪓', true], - ['🪔', true], - ['🪕', true], - ['‼️', true], - ['⁉️', true], - ['™️', true], - ['ℹ️', true], - ['↔️', true], - ['↕️', true], - ['↖️', true], - ['↗️', true], - ['↘️', true], - ['↙️', true], - ['↩️', true], - ['↪️', true], - ['#⃣', true], - ['⌚️', true], - ['⌛️', true], - ['⌨️', true], - ['⏏️', true], - ['⏩', true], - ['⏪', true], - ['⏫', true], - ['⏬', true], - ['⏭️', true], - ['⏮️', true], - ['⏯️', true], - ['⏰', true], - ['⏱️', true], - ['⏲️', true], - ['⏳', true], - ['⏸️', true], - ['⏹️', true], - ['⏺️', true], - ['Ⓜ️', true], - ['▪️', true], - ['▫️', true], - ['▶️', true], - ['◀️', true], - ['◻️', true], - ['◼️', true], - ['◽️', true], - ['◾️', true], - ['☀️', true], - ['☁️', true], - ['☂️', true], - ['☃️', true], - ['☄️', true], - ['☎️', true], - ['☑️', true], - ['☔️', true], - ['☕️', true], - ['☘️', true], - ['☝🏻', true], - ['☝🏼', true], - ['☝🏽', true], - ['☝🏾', true], - ['☝🏿', true], - ['☝️', true], - ['☠️', true], - ['☢️', true], - ['☣️', true], - ['☦️', true], - ['☪️', true], - ['☮️', true], - ['☯️', true], - ['☸️', true], - ['☹️', true], - ['☺️', true], - ['♀️', true], - ['♂️', true], - ['♈️', true], - ['♉️', true], - ['♊️', true], - ['♋️', true], - ['♌️', true], - ['♍️', true], - ['♎️', true], - ['♏️', true], - ['♐️', true], - ['♑️', true], - ['♒️', true], - ['♓️', true], - ['♟️', true], - ['♠️', true], - ['♣️', true], - ['♥️', true], - ['♦️', true], - ['♨️', true], - ['♻️', true], - ['♾', true], - ['♿️', true], - ['⚒️', true], - ['⚓️', true], - ['⚔️', true], - ['⚕️', true], - ['⚖️', true], - ['⚗️', true], - ['⚙️', true], - ['⚛️', true], - ['⚜️', true], - ['⚠️', true], - ['⚡️', true], - ['⚪️', true], - ['⚫️', true], - ['⚰️', true], - ['⚱️', true], - ['⚽️', true], - ['⚾️', true], - ['⛄️', true], - ['⛅️', true], - ['⛈️', true], - ['⛎', true], - ['⛏️', true], - ['⛑️', true], - ['⛓️', true], - ['⛔️', true], - ['⛩️', true], - ['⛪️', true], - ['⛰️', true], - ['⛱️', true], - ['⛲️', true], - ['⛳️', true], - ['⛴️', true], - ['⛵️', true], - ['⛷🏻', true], - ['⛷🏼', true], - ['⛷🏽', true], - ['⛷🏾', true], - ['⛷🏿', true], - ['⛷️', true], - ['⛸️', true], - ['⛹🏻‍♀️', true], - ['⛹🏻‍♂️', true], - ['⛹🏻', true], - ['⛹🏼‍♀️', true], - ['⛹🏼‍♂️', true], - ['⛹🏼', true], - ['⛹🏽‍♀️', true], - ['⛹🏽‍♂️', true], - ['⛹🏽', true], - ['⛹🏾‍♀️', true], - ['⛹🏾‍♂️', true], - ['⛹🏾', true], - ['⛹🏿‍♀️', true], - ['⛹🏿‍♂️', true], - ['⛹🏿', true], - ['⛹️‍♀️', true], - ['⛹️‍♂️', true], - ['⛹️', true], - ['⛺️', true], - ['⛽️', true], - ['✂️', true], - ['✅', true], - ['✈️', true], - ['✉️', true], - ['✊🏻', true], - ['✊🏼', true], - ['✊🏽', true], - ['✊🏾', true], - ['✊🏿', true], - ['✊', true], - ['✋🏻', true], - ['✋🏼', true], - ['✋🏽', true], - ['✋🏾', true], - ['✋🏿', true], - ['✋', true], - ['✌🏻', true], - ['✌🏼', true], - ['✌🏽', true], - ['✌🏾', true], - ['✌🏿', true], - ['✌️', true], - ['✍🏻', true], - ['✍🏼', true], - ['✍🏽', true], - ['✍🏾', true], - ['✍🏿', true], - ['✍️', true], - ['✏️', true], - ['✒️', true], - ['✔️', true], - ['✖️', true], - ['✝️', true], - ['✡️', true], - ['✨', true], - ['✳️', true], - ['✴️', true], - ['❄️', true], - ['❇️', true], - ['❌', true], - ['❎', true], - ['❓', true], - ['❔', true], - ['❕', true], - ['❗️', true], - ['❣️', true], - ['❤️', true], - ['➕', true], - ['➖', true], - ['➗', true], - ['➡️', true], - ['➰', true], - ['➿', true], - ['⤴️', true], - ['⤵️', true], - ['*⃣', true], - ['⬅️', true], - ['⬆️', true], - ['⬇️', true], - ['⬛️', true], - ['⬜️', true], - ['⭐️', true], - ['⭕️', true], - ['0⃣', true], - ['〰️', true], - ['〽️', true], - ['1⃣', true], - ['2⃣', true], - ['㊗️', true], - ['㊙️', true], - ['3⃣', true], - ['4⃣', true], - ['5⃣', true], - ['6⃣', true], - ['7⃣', true], - ['8⃣', true], - ['9⃣', true], - ['©️', true], - ['®️', true], - ]; - } -} --- a/tests/lib/Encryption/DecryptAllTest.php +++ /dev/null @@ -1,386 +0,0 @@ -userManager = $this->getMockBuilder(IUserManager::class) - ->disableOriginalConstructor()->getMock(); - $this->encryptionManager = $this->getMockBuilder('OC\Encryption\Manager') - ->disableOriginalConstructor()->getMock(); - $this->view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor()->getMock(); - $this->inputInterface = $this->getMockBuilder(InputInterface::class) - ->disableOriginalConstructor()->getMock(); - $this->outputInterface = $this->getMockBuilder(OutputInterface::class) - ->disableOriginalConstructor()->getMock(); - $this->outputInterface->expects($this->any())->method('isDecorated') - ->willReturn(false); - $this->userInterface = $this->getMockBuilder(UserInterface::class) - ->disableOriginalConstructor()->getMock(); - - /* We need format method to return a string */ - $outputFormatter = $this->createMock(OutputFormatterInterface::class); - $outputFormatter->method('format')->willReturn('foo'); - $outputFormatter->method('isDecorated')->willReturn(false); - - $this->outputInterface->expects($this->any())->method('getFormatter') - ->willReturn($outputFormatter); - - $this->instance = new DecryptAll($this->encryptionManager, $this->userManager, $this->view); - - $this->invokePrivate($this->instance, 'input', [$this->inputInterface]); - $this->invokePrivate($this->instance, 'output', [$this->outputInterface]); - } - - public static function dataDecryptAll(): array { - return [ - [true, 'user1', true], - [false, 'user1', true], - [true, '0', true], - [false, '0', true], - [true, '', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataDecryptAll')] - public function testDecryptAll(bool $prepareResult, string $user, bool $userExistsChecked): void { - if ($userExistsChecked) { - $this->userManager->expects($this->once())->method('userExists')->willReturn(true); - } else { - $this->userManager->expects($this->never())->method('userExists'); - } - /** @var DecryptAll&MockObject $instance */ - $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') - ->setConstructorArgs( - [ - $this->encryptionManager, - $this->userManager, - $this->view - ] - ) - ->onlyMethods(['prepareEncryptionModules', 'decryptAllUsersFiles']) - ->getMock(); - - $instance->expects($this->once()) - ->method('prepareEncryptionModules') - ->with($this->inputInterface, $this->outputInterface, $user) - ->willReturn($prepareResult); - - if ($prepareResult) { - $instance->expects($this->once()) - ->method('decryptAllUsersFiles') - ->with($this->outputInterface, $user); - } else { - $instance->expects($this->never())->method('decryptAllUsersFiles'); - } - - $instance->decryptAll($this->inputInterface, $this->outputInterface, $user); - } - - /** - * test decrypt all call with a user who doesn't exists - */ - public function testDecryptAllWrongUser(): void { - $this->userManager->expects($this->once())->method('userExists')->willReturn(false); - $this->outputInterface->expects($this->once())->method('writeln') - ->with('User "user1" does not exist. Please check the username and try again'); - - $this->assertFalse( - $this->instance->decryptAll($this->inputInterface, $this->outputInterface, 'user1') - ); - } - - public static function dataTrueFalse(): array { - return [ - [true], - [false], - ]; - } - - /** - * @param bool $success - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')] - public function testPrepareEncryptionModules($success): void { - $user = 'user1'; - - $dummyEncryptionModule = $this->getMockBuilder('OCP\Encryption\IEncryptionModule') - ->disableOriginalConstructor()->getMock(); - - $dummyEncryptionModule->expects($this->once()) - ->method('prepareDecryptAll') - ->with($this->inputInterface, $this->outputInterface, $user) - ->willReturn($success); - - $callback = function () use ($dummyEncryptionModule) { - return $dummyEncryptionModule; - }; - $moduleDescription = [ - 'id' => 'id', - 'displayName' => 'displayName', - 'callback' => $callback - ]; - - $this->encryptionManager->expects($this->once()) - ->method('getEncryptionModules') - ->willReturn([$moduleDescription]); - - $this->assertSame($success, - $this->invokePrivate($this->instance, 'prepareEncryptionModules', [$this->inputInterface, $this->outputInterface, $user]) - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestDecryptAllUsersFiles')] - public function testDecryptAllUsersFiles($user): void { - /** @var DecryptAll&MockObject $instance */ - $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') - ->setConstructorArgs( - [ - $this->encryptionManager, - $this->userManager, - $this->view - ] - ) - ->onlyMethods(['decryptUsersFiles']) - ->getMock(); - - if (empty($user)) { - $this->userManager->expects($this->once()) - ->method('getBackends') - ->willReturn([$this->userInterface]); - $this->userInterface->expects($this->any()) - ->method('getUsers') - ->willReturn(['user1', 'user2']); - $calls = [ - 'user1', - 'user2', - ]; - $instance->expects($this->exactly(2)) - ->method('decryptUsersFiles') - ->willReturnCallback(function ($user) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $user); - }); - } else { - $instance->expects($this->once()) - ->method('decryptUsersFiles') - ->with($user); - } - - $this->invokePrivate($instance, 'decryptAllUsersFiles', [$this->outputInterface, $user]); - } - - public static function dataTestDecryptAllUsersFiles(): array { - return [ - ['user1'], - [''] - ]; - } - - public function testDecryptUsersFiles(): void { - /** @var DecryptAll | \PHPUnit\Framework\MockObject\MockObject $instance */ - $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') - ->setConstructorArgs( - [ - $this->encryptionManager, - $this->userManager, - $this->view - ] - ) - ->onlyMethods(['decryptFile']) - ->getMock(); - - $storage = $this->getMockBuilder(IStorage::class) - ->disableOriginalConstructor()->getMock(); - - $sharedStorage = $this->getMockBuilder(IStorage::class) - ->disableOriginalConstructor()->getMock(); - - $sharedStorage->expects($this->once()) - ->method('instanceOfStorage') - ->with('OCA\Files_Sharing\SharedStorage') - ->willReturn(true); - - $this->view->expects($this->exactly(2)) - ->method('getDirectoryContent') - ->willReturnMap([ - [ - '/user1/files', null, null, - [ - new FileInfo('path', $storage, 'intPath', ['name' => 'foo', 'type' => 'dir'], null), - new FileInfo('path', $storage, 'intPath', ['name' => 'bar', 'type' => 'file', 'encrypted' => true], null), - new FileInfo('path', $sharedStorage, 'intPath', ['name' => 'shared', 'type' => 'file', 'encrypted' => true], null), - ], - ], - [ - '/user1/files/foo', null, null, - [ - new FileInfo('path', $storage, 'intPath', ['name' => 'subfile', 'type' => 'file', 'encrypted' => true], null) - ], - ], - ]); - - $this->view->expects($this->any())->method('is_dir') - ->willReturnCallback( - function ($path) { - if ($path === '/user1/files/foo') { - return true; - } - return false; - } - ); - - $calls = [ - '/user1/files/bar', - '/user1/files/foo/subfile', - ]; - $instance->expects($this->exactly(2)) - ->method('decryptFile') - ->willReturnCallback(function ($path) use (&$calls): bool { - $expected = array_shift($calls); - $this->assertEquals($expected, $path); - return true; - }); - - /* We need format method to return a string */ - $outputFormatter = $this->createMock(OutputFormatterInterface::class); - $outputFormatter->method('isDecorated')->willReturn(false); - $outputFormatter->method('format')->willReturn('foo'); - - $output = $this->createMock(OutputInterface::class); - $output->expects($this->any()) - ->method('getFormatter') - ->willReturn($outputFormatter); - $progressBar = new ProgressBar($output); - - $this->invokePrivate($instance, 'decryptUsersFiles', ['user1', $progressBar, '']); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTrueFalse')] - public function testDecryptFile($isEncrypted): void { - $path = 'test.txt'; - - /** @var DecryptAll&MockObject $instance */ - $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') - ->setConstructorArgs( - [ - $this->encryptionManager, - $this->userManager, - $this->view - ] - ) - ->onlyMethods(['getTimestamp']) - ->getMock(); - - $fileInfo = $this->createMock(FileInfo::class); - $fileInfo->expects($this->any())->method('isEncrypted') - ->willReturn($isEncrypted); - $this->view->expects($this->any())->method('getFileInfo') - ->willReturn($fileInfo); - - if ($isEncrypted) { - $instance->expects($this->any())->method('getTimestamp')->willReturn(42); - - $this->view->expects($this->once()) - ->method('copy') - ->with($path, $path . '.decrypted.42'); - $this->view->expects($this->once()) - ->method('rename') - ->with($path . '.decrypted.42', $path); - } else { - $instance->expects($this->never())->method('getTimestamp'); - $this->view->expects($this->never())->method('copy'); - $this->view->expects($this->never())->method('rename'); - } - $this->assertTrue( - $this->invokePrivate($instance, 'decryptFile', [$path]) - ); - } - - public function testDecryptFileFailure(): void { - $path = 'test.txt'; - - /** @var DecryptAll&MockObject $instance */ - $instance = $this->getMockBuilder('OC\Encryption\DecryptAll') - ->setConstructorArgs( - [ - $this->encryptionManager, - $this->userManager, - $this->view - ] - ) - ->onlyMethods(['getTimestamp']) - ->getMock(); - - $fileInfo = $this->createMock(FileInfo::class); - $fileInfo->expects($this->any())->method('isEncrypted') - ->willReturn(true); - $this->view->expects($this->any())->method('getFileInfo') - ->willReturn($fileInfo); - - $instance->expects($this->any())->method('getTimestamp')->willReturn(42); - - $this->view->expects($this->once()) - ->method('copy') - ->with($path, $path . '.decrypted.42') - ->willReturnCallback(function (): void { - throw new DecryptionFailedException(); - }); - - $this->view->expects($this->never())->method('rename'); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($path . '.decrypted.42') - ->willReturn(true); - $this->view->expects($this->once()) - ->method('unlink') - ->with($path . '.decrypted.42'); - - $this->assertFalse( - $this->invokePrivate($instance, 'decryptFile', [$path]) - ); - } -} --- a/tests/lib/Encryption/EncryptionWrapperTest.php +++ /dev/null @@ -1,78 +0,0 @@ -arrayCache = $this->createMock(ArrayCache::class); - $this->manager = $this->createMock(Manager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->instance = new EncryptionWrapper($this->arrayCache, $this->manager, $this->logger); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('provideWrapStorage')] - public function testWrapStorage($expectedWrapped, $wrappedStorages): void { - $storage = $this->getMockBuilder(Storage::class) - ->disableOriginalConstructor() - ->getMock(); - - $storage->expects($this->any()) - ->method('instanceOfStorage') - ->willReturnCallback(fn (string $storage): bool => in_array($storage, $wrappedStorages, true)); - - $mount = $this->getMockBuilder(IMountPoint::class) - ->disableOriginalConstructor() - ->getMock(); - - $returnedStorage = $this->instance->wrapStorage('mountPoint', $storage, $mount); - - $this->assertEquals( - $expectedWrapped, - $returnedStorage->instanceOfStorage(Encryption::class), - 'Asserted that the storage is (not) wrapped with encryption' - ); - } - - public static function provideWrapStorage(): array { - return [ - // Wrap when not wrapped or not wrapped with storage - [true, []], - [true, [Storage::class]], - - // Do not wrap shared storages - [false, [IDisableEncryptionStorage::class]], - ]; - } -} --- a/tests/lib/Encryption/FileTest.php +++ /dev/null @@ -1,156 +0,0 @@ -util = $this->createMock(Util::class); - $this->shareManager = $this->createMock(IManager::class); - $this->userFolder = $this->createMock(Folder::class); - - $rootFolder = $this->createMock(IRootFolder::class); - $rootFolder->method('getUserFolder') - ->with('user1') - ->willReturn($this->userFolder); - - $appManager = $this->createMock(IAppManager::class); - $appManager->method('isEnabledForUser') - ->willReturn(false); - - $this->file = $this->getMockBuilder(File::class) - ->setConstructorArgs([$this->util, $rootFolder, $this->shareManager]) - ->onlyMethods(['getAppManager']) - ->getMock(); - $this->file->method('getAppManager') - ->willReturn($appManager); - } - - /** - * Let the util mock resolve every path to $owner and $ownerPath - */ - private function mockUtil(string $owner, string $ownerPath): void { - $this->util->method('getUidAndFilename') - ->willReturn([$owner, $ownerPath]); - $this->util->method('isFile') - ->willReturn(true); - $this->util->method('stripPartialFileExtension') - ->willReturnArgument(0); - } - - public function testGetAccessList(): void { - $this->mockUtil('user1', '/files/folder/file.txt'); - - $node = $this->createMock(Node::class); - $parentNode = $this->createMock(Node::class); - $this->userFolder->method('get') - ->willReturnMap([ - ['/folder/file.txt', $node], - ['/folder', $parentNode], - ]); - - $this->shareManager->method('getAccessList') - ->willReturnCallback(fn (Node $requested) => match ($requested) { - $parentNode => ['users' => ['user2'], 'public' => false, 'remote' => false], - $node => ['users' => ['user3'], 'public' => true, 'remote' => false], - }); - - $this->assertSame( - ['users' => ['user1', 'user2', 'user3'], 'public' => true], - $this->file->getAccessList('/user1/files/folder/file.txt') - ); - } - - /** - * Copying a folder creates the target directories on the storage before their - * cache entries exist, so the parent of a file written into it can not be resolved. - */ - public function testGetAccessListWithUncachedParent(): void { - $this->mockUtil('user1', '/files/target/sub'); - - $rootNode = $this->createMock(Node::class); - $this->userFolder->method('get') - ->willReturnCallback(function (string $path) use ($rootNode) { - if ($path !== '/') { - throw new NotFoundException($path); - } - return $rootNode; - }); - - $this->shareManager->expects($this->once()) - ->method('getAccessList') - ->with($rootNode) - ->willReturn(['users' => ['user2'], 'public' => false, 'remote' => false]); - - $this->assertSame( - ['users' => ['user1', 'user2'], 'public' => false], - $this->file->getAccessList('/user1/files/target/sub') - ); - } - - public function testGetAccessListWithoutAnyResolvablePath(): void { - $this->mockUtil('user1', '/files/target/sub'); - - $this->userFolder->method('get') - ->willThrowException(new NotFoundException()); - - $this->shareManager->expects($this->never()) - ->method('getAccessList'); - - $this->assertSame( - ['users' => ['user1'], 'public' => false], - $this->file->getAccessList('/user1/files/target/sub') - ); - } - - public function testGetAccessListCachesTheParentResult(): void { - $this->util->method('getUidAndFilename') - ->willReturnCallback(fn (string $path) => ['user1', substr($path, strlen('/user1'))]); - $this->util->method('isFile') - ->willReturn(true); - $this->util->method('stripPartialFileExtension') - ->willReturnArgument(0); - - $parentNode = $this->createMock(Node::class); - $this->userFolder->method('get') - ->willReturnCallback(function (string $path) use ($parentNode) { - if ($path !== '/folder') { - throw new NotFoundException($path); - } - return $parentNode; - }); - - $this->shareManager->expects($this->once()) - ->method('getAccessList') - ->with($parentNode) - ->willReturn(['users' => ['user2'], 'public' => false, 'remote' => false]); - - $expected = ['users' => ['user1', 'user2'], 'public' => false]; - $this->assertSame($expected, $this->file->getAccessList('/user1/files/folder/first.txt')); - $this->assertSame($expected, $this->file->getAccessList('/user1/files/folder/second.txt')); - } -} --- a/tests/lib/Encryption/Keys/StorageTest.php +++ /dev/null @@ -1,769 +0,0 @@ -util = $this->getMockBuilder(Util::class) - ->disableOriginalConstructor() - ->onlyMethods(array_diff(get_class_methods(Util::class), ['getFileKeyDir'])) - ->getMock(); - - $this->view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->crypto = $this->createMock(ICrypto::class); - $this->crypto->method('encrypt') - ->willReturnCallback(function ($data, $pass) { - return $data; - }); - $this->crypto->method('decrypt') - ->willReturnCallback(function ($data, $pass) { - return $data; - }); - - $this->config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->storage = new Storage($this->view, $this->util, $this->crypto, $this->config); - } - - public function testSetFileKey(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(false); - - $data = json_encode(['key' => base64_encode('key')]); - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), - $this->equalTo($data)) - ->willReturn(strlen($data)); - - $this->assertTrue( - $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') - ); - } - - public function testSetFileOld(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.0'); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(false); - $this->crypto->expects($this->never()) - ->method('encrypt'); - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), - $this->equalTo('key')) - ->willReturn(strlen('key')); - - $this->assertTrue( - $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') - ); - } - - public static function dataTestGetFileKey() { - return [ - ['/files/foo.txt', '/files/foo.txt', true, 'key'], - ['/files/foo.txt.ocTransferId2111130212.part', '/files/foo.txt', true, 'key'], - ['/files/foo.txt.ocTransferId2111130212.part', '/files/foo.txt', false, 'key2'], - ]; - } - - /** - * - * @param string $path - * @param string $strippedPartialName - * @param bool $originalKeyExists - * @param string $expectedKeyContent - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetFileKey')] - public function testGetFileKey($path, $strippedPartialName, $originalKeyExists, $expectedKeyContent): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturnMap([ - ['user1/files/foo.txt', ['user1', '/files/foo.txt']], - ['user1/files/foo.txt.ocTransferId2111130212.part', ['user1', '/files/foo.txt.ocTransferId2111130212.part']], - ]); - // we need to strip away the part file extension in order to reuse a - // existing key if it exists, otherwise versions will break - $this->util->expects($this->once()) - ->method('stripPartialFileExtension') - ->willReturn('user1' . $strippedPartialName); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(false); - - $this->crypto->method('decrypt') - ->willReturnCallback(function ($data, $pass) { - return $data; - }); - - if (!$originalKeyExists) { - $this->view->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey', $originalKeyExists], - ['/user1/files_encryption/keys' . $path . '/encModule/fileKey', true], - ]); - - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with($this->equalTo('/user1/files_encryption/keys' . $path . '/encModule/fileKey')) - ->willReturn(json_encode(['key' => base64_encode('key2')])); - } else { - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) - ->willReturn($originalKeyExists); - - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with($this->equalTo('/user1/files_encryption/keys' . $strippedPartialName . '/encModule/fileKey')) - ->willReturn(json_encode(['key' => base64_encode('key')])); - } - - $this->assertSame($expectedKeyContent, - $this->storage->getFileKey('user1' . $path, 'fileKey', 'encModule') - ); - } - - public function testSetFileKeySystemWide(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(true); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - - $this->crypto->method('encrypt') - ->willReturnCallback(function ($data, $pass) { - return $data; - }); - - $data = json_encode(['key' => base64_encode('key')]); - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey'), - $this->equalTo($data)) - ->willReturn(strlen($data)); - - $this->assertTrue( - $this->storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key', 'encModule') - ); - } - - public function testGetFileKeySystemWide(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(true); - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(json_encode(['key' => base64_encode('key')])); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(true); - - $this->assertSame('key', - $this->storage->getFileKey('user1/files/foo.txt', 'fileKey', 'encModule') - ); - } - - public function testSetSystemUserKey(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $data = json_encode([ - 'key' => base64_encode('key'), - 'uid' => null] - ); - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with($this->equalTo('/files_encryption/encModule/shareKey_56884'), - $this->equalTo($data)) - ->willReturn(strlen($data)); - - $this->assertTrue( - $this->storage->setSystemUserKey('shareKey_56884', 'key', 'encModule') - ); - } - - public function testSetUserKey(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $data = json_encode([ - 'key' => base64_encode('key'), - 'uid' => 'user1'] - ); - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey'), - $this->equalTo($data)) - ->willReturn(strlen($data)); - - $this->assertTrue( - $this->storage->setUserKey('user1', 'publicKey', 'key', 'encModule') - ); - } - - public function testGetSystemUserKey(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $data = json_encode([ - 'key' => base64_encode('key'), - 'uid' => null] - ); - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) - ->willReturn($data); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) - ->willReturn(true); - - $this->assertSame('key', - $this->storage->getSystemUserKey('shareKey_56884', 'encModule') - ); - } - - public function testGetUserKey(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - - $data = json_encode([ - 'key' => base64_encode('key'), - 'uid' => 'user1'] - ); - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) - ->willReturn($data); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) - ->willReturn(true); - - $this->assertSame('key', - $this->storage->getUserKey('user1', 'publicKey', 'encModule') - ); - } - - public function testDeleteUserKey(): void { - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) - ->willReturn(true); - $this->view->expects($this->once()) - ->method('unlink') - ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) - ->willReturn(true); - - $this->assertTrue( - $this->storage->deleteUserKey('user1', 'publicKey', 'encModule') - ); - } - - public function testDeleteSystemUserKey(): void { - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) - ->willReturn(true); - $this->view->expects($this->once()) - ->method('unlink') - ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) - ->willReturn(true); - - $this->assertTrue( - $this->storage->deleteSystemUserKey('shareKey_56884', 'encModule') - ); - } - - public function testDeleteFileKeySystemWide(): void { - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(true); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(true); - $this->view->expects($this->once()) - ->method('unlink') - ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(true); - - $this->assertTrue( - $this->storage->deleteFileKey('user1/files/foo.txt', 'fileKey', 'encModule') - ); - } - - public function testDeleteFileKey(): void { - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', '/files/foo.txt']); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn(false); - $this->view->expects($this->once()) - ->method('file_exists') - ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(true); - $this->view->expects($this->once()) - ->method('unlink') - ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) - ->willReturn(true); - - $this->assertTrue( - $this->storage->deleteFileKey('user1/files/foo.txt', 'fileKey', 'encModule') - ); - } - - /** - * Set up the mocks needed to read file keys for arbitrary paths - */ - private function mockFileKeyEnvironment(): void { - $this->config->method('getSystemValueString') - ->with('version') - ->willReturn('20.0.0.2'); - $this->config->method('getSystemValueBool') - ->willReturn(true); - $this->util->method('getUidAndFilename') - ->willReturnCallback([$this, 'getUidAndFilenameCallback']); - $this->util->method('stripPartialFileExtension') - ->willReturnArgument(0); - $this->util->method('isSystemWideMountPoint') - ->willReturn(false); - $this->view->method('file_exists') - ->willReturn(true); - $this->view->method('is_dir') - ->willReturn(true); - } - - /** - * Make the view return a dummy key for every path and collect the read paths - * - * @param string[] $reads - */ - private function trackKeyReads(array &$reads, ?string $uid = null): void { - $this->view->method('file_get_contents') - ->willReturnCallback(function (string $path) use (&$reads, $uid): string { - $reads[] = $path; - return json_encode(['key' => base64_encode('key'), 'uid' => $uid]); - }); - } - - public function testGetFileKeyIsCached(): void { - $this->mockFileKeyEnvironment(); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - - $this->assertSame(['/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'], $reads); - } - - public function testKeyCacheIsCapped(): void { - $this->mockFileKeyEnvironment(); - $reads = []; - $this->trackKeyReads($reads); - - for ($i = 0; $i < 600; $i++) { - $this->storage->getFileKey('/user1/files/foo' . $i . '.txt', 'fileKey', 'encModule'); - } - - /** @var CappedMemoryCache $keyCache */ - $keyCache = self::invokePrivate($this->storage, 'keyCache'); - $this->assertCount(512, $keyCache->getData()); - } - - public function testDeleteFileKeyInvalidatesCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('unlink')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - $this->assertTrue($this->storage->deleteFileKey('/user1/files/foo.txt', 'fileKey', 'encModule')); - $reads = []; - - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - - $this->assertSame(['/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'], $reads); - } - - public function testDeleteAllFileKeysInvalidatesCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('deleteAll')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/foo.txt', 'otherKey', 'encModule'); - // a sibling sharing the name prefix must stay cached - $this->storage->getFileKey('/user1/files/foobar.txt', 'fileKey', 'encModule'); - $this->assertTrue($this->storage->deleteAllFileKeys('/user1/files/foo.txt')); - $reads = []; - - $this->storage->getFileKey('/user1/files/foo.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/foo.txt', 'otherKey', 'encModule'); - $this->storage->getFileKey('/user1/files/foobar.txt', 'fileKey', 'encModule'); - - $this->assertSame([ - '/user1/files_encryption/keys/files/foo.txt/encModule/fileKey', - '/user1/files_encryption/keys/files/foo.txt/encModule/otherKey', - ], $reads); - } - - public function testRenameKeysInvalidatesCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('rename')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getFileKey('/user1/files/source.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/target.txt', 'fileKey', 'encModule'); - $this->assertTrue($this->storage->renameKeys('/user1/files/source.txt', '/user1/files/target.txt')); - $reads = []; - - $this->storage->getFileKey('/user1/files/source.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/target.txt', 'fileKey', 'encModule'); - - $this->assertSame([ - '/user1/files_encryption/keys/files/source.txt/encModule/fileKey', - '/user1/files_encryption/keys/files/target.txt/encModule/fileKey', - ], $reads); - } - - public function testCopyKeysInvalidatesTargetCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('copy')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getFileKey('/user1/files/source.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/target.txt', 'fileKey', 'encModule'); - $this->assertTrue($this->storage->copyKeys('/user1/files/source.txt', '/user1/files/target.txt')); - $reads = []; - - $this->storage->getFileKey('/user1/files/source.txt', 'fileKey', 'encModule'); - $this->storage->getFileKey('/user1/files/target.txt', 'fileKey', 'encModule'); - - $this->assertSame([ - '/user1/files_encryption/keys/files/target.txt/encModule/fileKey', - ], $reads); - } - - public function testDeleteUserKeyInvalidatesCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('unlink')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads, 'user1'); - - $this->storage->getUserKey('user1', 'publicKey', 'encModule'); - $this->assertTrue($this->storage->deleteUserKey('user1', 'publicKey', 'encModule')); - $reads = []; - - $this->storage->getUserKey('user1', 'publicKey', 'encModule'); - - $this->assertSame(['/user1/files_encryption/encModule/user1.publicKey'], $reads); - } - - public function testDeleteSystemUserKeyInvalidatesCache(): void { - $this->mockFileKeyEnvironment(); - $this->view->method('unlink')->willReturn(true); - $reads = []; - $this->trackKeyReads($reads); - - $this->storage->getSystemUserKey('shareKey_56884', 'encModule'); - $this->assertTrue($this->storage->deleteSystemUserKey('shareKey_56884', 'encModule')); - $reads = []; - - $this->storage->getSystemUserKey('shareKey_56884', 'encModule'); - - $this->assertSame(['/files_encryption/encModule/shareKey_56884'], $reads); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderCopyRename')] - public function testRenameKeys($source, $target, $systemWideMountSource, $systemWideMountTarget, $expectedSource, $expectedTarget): void { - $this->view->expects($this->any()) - ->method('file_exists') - ->willReturn(true); - $this->view->expects($this->any()) - ->method('is_dir') - ->willReturn(true); - $this->view->expects($this->once()) - ->method('rename') - ->with( - $this->equalTo($expectedSource), - $this->equalTo($expectedTarget)) - ->willReturn(true); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturnCallback([$this, 'getUidAndFilenameCallback']); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturnCallback(function ($path, $owner) use ($systemWideMountSource, $systemWideMountTarget) { - if (strpos($path, 'source.txt') !== false) { - return $systemWideMountSource; - } - return $systemWideMountTarget; - }); - - $this->storage->renameKeys($source, $target); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderCopyRename')] - public function testCopyKeys($source, $target, $systemWideMountSource, $systemWideMountTarget, $expectedSource, $expectedTarget): void { - $this->view->expects($this->any()) - ->method('file_exists') - ->willReturn(true); - $this->view->expects($this->any()) - ->method('is_dir') - ->willReturn(true); - $this->view->expects($this->once()) - ->method('copy') - ->with( - $this->equalTo($expectedSource), - $this->equalTo($expectedTarget)) - ->willReturn(true); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturnCallback([$this, 'getUidAndFilenameCallback']); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturnCallback(function ($path, $owner) use ($systemWideMountSource, $systemWideMountTarget) { - if (strpos($path, 'source.txt') !== false) { - return $systemWideMountSource; - } - return $systemWideMountTarget; - }); - - $this->storage->copyKeys($source, $target); - } - - public function getUidAndFilenameCallback() { - $args = func_get_args(); - - $path = $args[0]; - $parts = explode('/', $path); - - return [$parts[1], '/' . implode('/', array_slice($parts, 2))]; - } - - public static function dataProviderCopyRename() { - return [ - ['/user1/files/source.txt', '/user1/files/target.txt', false, false, - '/user1/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - ['/user1/files/foo/source.txt', '/user1/files/target.txt', false, false, - '/user1/files_encryption/keys/files/foo/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - ['/user1/files/source.txt', '/user1/files/foo/target.txt', false, false, - '/user1/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/foo/target.txt/'], - ['/user1/files/source.txt', '/user1/files/foo/target.txt', true, true, - '/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/foo/target.txt/'], - ['/user1/files/source.txt', '/user1/files/target.txt', false, true, - '/user1/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/target.txt/'], - ['/user1/files/source.txt', '/user1/files/target.txt', true, false, - '/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - - ['/user2/files/source.txt', '/user1/files/target.txt', false, false, - '/user2/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - ['/user2/files/foo/source.txt', '/user1/files/target.txt', false, false, - '/user2/files_encryption/keys/files/foo/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - ['/user2/files/source.txt', '/user1/files/foo/target.txt', false, false, - '/user2/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/foo/target.txt/'], - ['/user2/files/source.txt', '/user1/files/foo/target.txt', true, true, - '/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/foo/target.txt/'], - ['/user2/files/source.txt', '/user1/files/target.txt', false, true, - '/user2/files_encryption/keys/files/source.txt/', '/files_encryption/keys/files/target.txt/'], - ['/user2/files/source.txt', '/user1/files/target.txt', true, false, - '/files_encryption/keys/files/source.txt/', '/user1/files_encryption/keys/files/target.txt/'], - ]; - } - - /** - * - * @param string $path - * @param boolean $systemWideMountPoint - * @param string $storageRoot - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPathToKeys')] - public function testGetPathToKeys($path, $systemWideMountPoint, $storageRoot, $expected): void { - $this->invokePrivate($this->storage, 'root_dir', [$storageRoot]); - - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturnCallback([$this, 'getUidAndFilenameCallback']); - $this->util->expects($this->any()) - ->method('isSystemWideMountPoint') - ->willReturn($systemWideMountPoint); - - $this->assertSame($expected, - self::invokePrivate($this->storage, 'getPathToKeys', [$path]) - ); - } - - public static function dataTestGetPathToKeys() { - return [ - ['/user1/files/source.txt', false, '', '/user1/files_encryption/keys/files/source.txt/'], - ['/user1/files/source.txt', true, '', '/files_encryption/keys/files/source.txt/'], - ['/user1/files/source.txt', false, 'storageRoot', '/storageRoot/user1/files_encryption/keys/files/source.txt/'], - ['/user1/files/source.txt', true, 'storageRoot', '/storageRoot/files_encryption/keys/files/source.txt/'], - ]; - } - - public function testKeySetPreparation(): void { - $this->view->expects($this->any()) - ->method('file_exists') - ->willReturn(false); - $this->view->expects($this->any()) - ->method('is_dir') - ->willReturn(false); - $this->view->expects($this->any()) - ->method('mkdir') - ->willReturnCallback([$this, 'mkdirCallback']); - - $this->mkdirStack = [ - '/user1/files_encryption/keys/foo', - '/user1/files_encryption/keys', - '/user1/files_encryption', - '/user1']; - - self::invokePrivate($this->storage, 'keySetPreparation', ['/user1/files_encryption/keys/foo']); - } - - public function mkdirCallback() { - $args = func_get_args(); - $expected = array_pop($this->mkdirStack); - $this->assertSame($expected, $args[0]); - } - - /** - * @param bool $createBackupDir - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestBackupUserKeys')] - public function testBackupUserKeys($createBackupDir): void { - $storage = $this->getMockBuilder('OC\Encryption\Keys\Storage') - ->setConstructorArgs([$this->view, $this->util, $this->crypto, $this->config]) - ->onlyMethods(['getTimestamp']) - ->getMock(); - - $storage->expects($this->any())->method('getTimestamp')->willReturn('1234567'); - - $this->view->expects($this->once())->method('file_exists') - ->with('user1/files_encryption/backup')->willReturn(!$createBackupDir); - - if ($createBackupDir) { - $calls = [ - 'user1/files_encryption/backup', - 'user1/files_encryption/backup/test.encryptionModule.1234567', - ]; - $this->view->expects($this->exactly(2))->method('mkdir') - ->willReturnCallback(function ($path) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $path); - }); - } else { - $this->view->expects($this->once())->method('mkdir') - ->with('user1/files_encryption/backup/test.encryptionModule.1234567'); - } - - $this->view->expects($this->once())->method('copy') - ->with( - 'user1/files_encryption/encryptionModule', - 'user1/files_encryption/backup/test.encryptionModule.1234567' - )->willReturn(true); - - $this->assertTrue($storage->backupUserKeys('encryptionModule', 'test', 'user1')); - } - - public static function dataTestBackupUserKeys() { - return [ - [true], [false] - ]; - } -} --- a/tests/lib/Encryption/ManagerTest.php +++ /dev/null @@ -1,281 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->l10n = $this->createMock(IL10N::class); - $this->view = $this->createMock(View::class); - $this->util = $this->createMock(Util::class); - $this->arrayCache = $this->createMock(ArrayCache::class); - $this->manager = new Manager($this->config, $this->logger, $this->l10n, $this->view, $this->util, $this->arrayCache); - } - - public function testManagerIsDisabled(): void { - $this->assertFalse($this->manager->isEnabled()); - } - - public function testManagerIsDisabledIfEnabledButNoModules(): void { - $this->config->expects($this->any())->method('getAppValue')->willReturn(true); - $this->assertFalse($this->manager->isEnabled()); - } - - public function testManagerIsDisabledIfDisabledButModules(): void { - $this->config->expects($this->any())->method('getAppValue')->willReturn(false); - $em = $this->createMock(IEncryptionModule::class); - $em->expects($this->any())->method('getId')->willReturn('id'); - $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); - $this->manager->registerEncryptionModule('id', 'TestDummyModule0', function () use ($em) { - return $em; - }); - $this->assertFalse($this->manager->isEnabled()); - } - - #[Group(name: 'DB')] - public function testManagerIsEnabled(): void { - $appConfig = Server::get(IAppConfig::class); - $appConfig->setValueBool('core', 'encryption_enabled', true); - - $this->config->expects($this->any())->method('getSystemValueBool')->willReturn(true); - $result = $this->manager->isEnabled(); - - $appConfig->deleteKey('core', 'encryption_enabled'); - $this->assertTrue($result); - } - - public function testModuleRegistration() { - $this->config->expects($this->any())->method('getAppValue')->willReturn('yes'); - - $this->addNewEncryptionModule($this->manager, 0); - $this->assertCount(1, $this->manager->getEncryptionModules()); - - return $this->manager; - } - - #[\PHPUnit\Framework\Attributes\Depends('testModuleRegistration')] - public function testModuleReRegistration($manager): void { - $this->expectException(ModuleAlreadyExistsException::class); - $this->expectExceptionMessage('Id "ID0" already used by encryption module "TestDummyModule0"'); - - $this->addNewEncryptionModule($manager, 0); - } - - public function testModuleUnRegistration(): void { - $this->config->expects($this->any())->method('getAppValue')->willReturn(true); - $this->addNewEncryptionModule($this->manager, 0); - $this->assertCount(1, $this->manager->getEncryptionModules()); - - $this->manager->unregisterEncryptionModule('ID0'); - $this->assertEmpty($this->manager->getEncryptionModules()); - } - - public function testGetEncryptionModuleUnknown(): void { - $this->expectException(ModuleDoesNotExistsException::class); - $this->expectExceptionMessage('Module with ID: unknown does not exist.'); - - $this->config->expects($this->any())->method('getAppValue')->willReturn(true); - $this->addNewEncryptionModule($this->manager, 0); - $this->assertCount(1, $this->manager->getEncryptionModules()); - $this->manager->getEncryptionModule('unknown'); - } - - public function testGetEncryptionModuleEmpty(): void { - global $defaultId; - $defaultId = null; - - $this->config->expects($this->any()) - ->method('getAppValue') - ->with('core', 'default_encryption_module') - ->willReturnCallback(function () { - global $defaultId; - return $defaultId; - }); - - $this->addNewEncryptionModule($this->manager, 0); - $this->assertCount(1, $this->manager->getEncryptionModules()); - $this->addNewEncryptionModule($this->manager, 1); - $this->assertCount(2, $this->manager->getEncryptionModules()); - - // Should return the default module - $defaultId = 'ID0'; - $this->assertEquals('ID0', $this->manager->getEncryptionModule()->getId()); - $defaultId = 'ID1'; - $this->assertEquals('ID1', $this->manager->getEncryptionModule()->getId()); - } - - public function testGetEncryptionModule(): void { - global $defaultId; - $defaultId = null; - - $this->config->expects($this->any()) - ->method('getAppValue') - ->with('core', 'default_encryption_module') - ->willReturnCallback(function () { - global $defaultId; - return $defaultId; - }); - - $this->addNewEncryptionModule($this->manager, 0); - $defaultId = 'ID0'; - $this->assertCount(1, $this->manager->getEncryptionModules()); - - $en0 = $this->manager->getEncryptionModule('ID0'); - $this->assertEquals('ID0', $en0->getId()); - - $en0 = self::invokePrivate($this->manager, 'getDefaultEncryptionModule'); - $this->assertEquals('ID0', $en0->getId()); - - $this->assertEquals('ID0', $this->manager->getDefaultEncryptionModuleId()); - } - - public function testSetDefaultEncryptionModule(): void { - global $defaultId; - $defaultId = null; - - $this->config->expects($this->any()) - ->method('getAppValue') - ->with('core', 'default_encryption_module') - ->willReturnCallback(function () { - global $defaultId; - return $defaultId; - }); - - $this->addNewEncryptionModule($this->manager, 0); - $this->assertCount(1, $this->manager->getEncryptionModules()); - $this->addNewEncryptionModule($this->manager, 1); - $this->assertCount(2, $this->manager->getEncryptionModules()); - - // Default module is the first we set - $defaultId = 'ID0'; - $this->assertEquals('ID0', $this->manager->getDefaultEncryptionModuleId()); - - // Set to an existing module - $this->config->expects($this->once()) - ->method('setAppValue') - ->with('core', 'default_encryption_module', 'ID1'); - $this->assertTrue($this->manager->setDefaultEncryptionModule('ID1')); - $defaultId = 'ID1'; - $this->assertEquals('ID1', $this->manager->getDefaultEncryptionModuleId()); - - // Set to an unexisting module - $this->assertFalse($this->manager->setDefaultEncryptionModule('ID2')); - $this->assertEquals('ID1', $this->manager->getDefaultEncryptionModuleId()); - } - - // /** - // * @expectedException \OC\Encryption\Exceptions\ModuleAlreadyExistsException - // * @expectedExceptionMessage Id "0" already used by encryption module "TestDummyModule0" - // */ - // public function testModuleRegistration() { - // $config = $this->createMock(IConfig::class); - // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); - // $em = $this->createMock(IEncryptionModule::class); - // $em->expects($this->any())->method('getId')->willReturn(0); - // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); - // $m = new Manager($config); - // $m->registerEncryptionModule($em); - // $this->assertTrue($m->isEnabled()); - // $m->registerEncryptionModule($em); - // } - // - // public function testModuleUnRegistration() { - // $config = $this->createMock(IConfig::class); - // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); - // $em = $this->createMock(IEncryptionModule::class); - // $em->expects($this->any())->method('getId')->willReturn(0); - // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); - // $m = new Manager($config); - // $m->registerEncryptionModule($em); - // $this->assertTrue($m->isEnabled()); - // $m->unregisterEncryptionModule($em); - // $this->assertFalse($m->isEnabled()); - // } - // - // /** - // * @expectedException \OC\Encryption\Exceptions\ModuleDoesNotExistsException - // * @expectedExceptionMessage Module with ID: unknown does not exist. - // */ - // public function testGetEncryptionModuleUnknown() { - // $config = $this->createMock(IConfig::class); - // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); - // $em = $this->createMock(IEncryptionModule::class); - // $em->expects($this->any())->method('getId')->willReturn(0); - // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); - // $m = new Manager($config); - // $m->registerEncryptionModule($em); - // $this->assertTrue($m->isEnabled()); - // $m->getEncryptionModule('unknown'); - // } - // - // public function testGetEncryptionModule() { - // $config = $this->createMock(IConfig::class); - // $config->expects($this->any())->method('getSystemValueBool')->willReturn(true); - // $em = $this->createMock(IEncryptionModule::class); - // $em->expects($this->any())->method('getId')->willReturn(0); - // $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); - // $m = new Manager($config); - // $m->registerEncryptionModule($em); - // $this->assertTrue($m->isEnabled()); - // $en0 = $m->getEncryptionModule(0); - // $this->assertEquals(0, $en0->getId()); - // } - - protected function addNewEncryptionModule(Manager $manager, $id) { - $encryptionModule = $this->createMock(IEncryptionModule::class); - $encryptionModule->expects($this->any()) - ->method('getId') - ->willReturn('ID' . $id); - $encryptionModule->expects($this->any()) - ->method('getDisplayName') - ->willReturn('TestDummyModule' . $id); - /** @var IEncryptionModule $encryptionModule */ - $manager->registerEncryptionModule('ID' . $id, 'TestDummyModule' . $id, function () use ($encryptionModule) { - return $encryptionModule; - }); - } -} --- a/tests/lib/Encryption/UpdateTest.php +++ /dev/null @@ -1,191 +0,0 @@ -view = $this->createMock(View::class); - $this->util = $this->createMock(Util::class); - $this->encryptionManager = $this->createMock(\OC\Encryption\Manager::class); - $this->fileHelper = $this->createMock(File::class); - $this->encryptionModule = $this->createMock(IEncryptionModule::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->uid = 'testUser1'; - } - - private function getUserMock(string $uid): IUser&MockObject { - $user = $this->createMock(IUser::class); - $user->expects(self::any()) - ->method('getUID') - ->willReturn($uid); - return $user; - } - - private function getFileMock(string $path, string $owner): OCPFile&MockObject { - $node = $this->createMock(OCPFile::class); - $node->expects(self::atLeastOnce()) - ->method('getPath') - ->willReturn($path); - $node->expects(self::any()) - ->method('getOwner') - ->willReturn($this->getUserMock($owner)); - - return $node; - } - - private function getFolderMock(string $path, string $owner): Folder&MockObject { - $node = $this->createMock(Folder::class); - $node->expects(self::atLeastOnce()) - ->method('getPath') - ->willReturn($path); - $node->expects(self::any()) - ->method('getOwner') - ->willReturn($this->getUserMock($owner)); - - return $node; - } - - /** - * - * @param string $path - * @param boolean $isDir - * @param array $allFiles - * @param integer $numberOfFiles - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestUpdate')] - public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void { - $updateMock = $this->getUpdateMock(['getOwnerPath']); - $updateMock->expects($this->once())->method('getOwnerPath') - ->willReturnCallback(fn (OCPFile|Folder $node) => '/user/' . $node->getPath()); - - $this->encryptionManager->expects($this->once()) - ->method('getEncryptionModule') - ->willReturn($this->encryptionModule); - - if ($isDir) { - $this->util->expects($this->once()) - ->method('getAllFiles') - ->willReturn($allFiles); - $node = $this->getFolderMock($path, 'user'); - } else { - $node = $this->getFileMock($path, 'user'); - } - - $this->fileHelper->expects($this->exactly($numberOfFiles)) - ->method('getAccessList') - ->willReturn(['users' => [], 'public' => false]); - - $this->encryptionModule->expects($this->exactly($numberOfFiles)) - ->method('update') - ->willReturn(true); - - $updateMock->update($node); - } - - public static function dataTestUpdate(): array { - return [ - ['/user/files/foo', true, ['/user/files/foo/file1.txt', '/user/files/foo/file1.txt'], 2], - ['/user/files/test.txt', false, [], 1], - ]; - } - - /** - * - * @param string $source - * @param string $target - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPostRename')] - public function testPostRename($source, $target): void { - $updateMock = $this->getUpdateMock(['update','getOwnerPath']); - - $sourceNode = $this->getFileMock($source, 'user'); - $targetNode = $this->getFileMock($target, 'user'); - - if (dirname($source) === dirname($target)) { - $updateMock->expects($this->never())->method('getOwnerPath'); - $updateMock->expects($this->never())->method('update'); - } else { - $updateMock->expects($this->once())->method('update') - ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( - $target, - $node->getPath(), - 'update needs to be executed for the target destination' - )); - } - - $updateMock->postRename($sourceNode, $targetNode); - } - - public static function dataTestPostRename(): array { - return [ - ['/test.txt', '/testNew.txt'], - ['/folder/test.txt', '/testNew.txt'], - ['/test.txt', '/folder/testNew.txt'], - ]; - } - - public function testPostRestore(): void { - $updateMock = $this->getUpdateMock(['update']); - - $updateMock->expects($this->once())->method('update') - ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( - '/folder/test.txt', - $node->getPath(), - 'update needs to be executed for the target destination' - )); - - $updateMock->postRestore($this->getFileMock('/folder/test.txt', 'user')); - } - - /** - * create mock of the update method - * - * @param array $methods methods which should be set - */ - protected function getUpdateMock(array $methods): Update&MockObject { - return $this->getMockBuilder(Update::class) - ->setConstructorArgs( - [ - $this->util, - $this->encryptionManager, - $this->fileHelper, - $this->logger, - $this->uid - ] - ) - ->onlyMethods($methods) - ->getMock(); - } -} --- a/tests/lib/Encryption/UtilTest.php +++ /dev/null @@ -1,247 +0,0 @@ -view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->userManager = $this->createMock(IUserManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->config = $this->createMock(IConfig::class); - - $this->util = new Util( - $this->view, - $this->userManager, - $this->groupManager, - $this->config - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesHeadersForEncryptionModule')] - public function testGetEncryptionModuleId($expected, $header): void { - $id = $this->util->getEncryptionModuleId($header); - $this->assertEquals($expected, $id); - } - - public static function providesHeadersForEncryptionModule(): array { - return [ - ['', []], - ['', ['1']], - [2, ['oc_encryption_module' => 2]], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesHeaders')] - public function testCreateHeader($expected, $header, $moduleId): void { - $em = $this->createMock(IEncryptionModule::class); - $em->expects($this->any())->method('getId')->willReturn($moduleId); - - $result = $this->util->createHeader($header, $em); - $this->assertEquals($expected, $result); - } - - public static function providesHeaders(): array { - return [ - [str_pad('HBEGIN:oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) - , [], '0'], - [str_pad('HBEGIN:oc_encryption_module:0:custom_header:foo:HEND', self::$headerSize, '-', STR_PAD_RIGHT) - , ['custom_header' => 'foo'], '0'], - ]; - } - - public function testCreateHeaderFailed(): void { - $this->expectException(EncryptionHeaderKeyExistsException::class); - - $header = ['header1' => 1, 'header2' => 2, 'oc_encryption_module' => 'foo']; - - $em = $this->createMock(IEncryptionModule::class); - $em->expects($this->any())->method('getId')->willReturn('moduleId'); - - $this->util->createHeader($header, $em); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providePathsForTestIsExcluded')] - public function testIsExcluded($path, $keyStorageRoot, $expected): void { - $this->config->expects($this->once()) - ->method('getAppValue') - ->with('core', 'encryption_key_storage_root', '') - ->willReturn($keyStorageRoot); - $this->userManager - ->expects($this->any()) - ->method('userExists') - ->willReturnCallback([$this, 'isExcludedCallback']); - - $this->assertSame($expected, - $this->util->isExcluded($path) - ); - } - - public static function providePathsForTestIsExcluded(): array { - return [ - ['/files_encryption', '', true], - ['files_encryption/foo.txt', '', true], - ['test/foo.txt', '', false], - ['/user1/files_encryption/foo.txt', '', true], - ['/user1/files/foo.txt', '', false], - ['/keyStorage/user1/files/foo.txt', 'keyStorage', true], - ['/keyStorage/files_encryption', '/keyStorage', true], - ['keyStorage/user1/files_encryption', '/keyStorage/', true], - ]; - } - - public function isExcludedCallback() { - $args = func_get_args(); - if ($args[0] === 'user1') { - return true; - } - - return false; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsFile')] - public function testIsFile($path, $expected): void { - $this->assertSame($expected, - $this->util->isFile($path) - ); - } - - public static function dataTestIsFile(): array { - return [ - ['/user/files/test.txt', true], - ['/user/files', true], - ['/user/files_versions/test.txt', false], - ['/user/foo/files/test.txt', false], - ['/files/foo/files/test.txt', false], - ['/user', false], - ['/user/test.txt', false], - ]; - } - - /** - * - * @param string $path - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestStripPartialFileExtension')] - public function testStripPartialFileExtension($path, $expected): void { - $this->assertSame($expected, - $this->util->stripPartialFileExtension($path)); - } - - public static function dataTestStripPartialFileExtension(): array { - return [ - ['/foo/test.txt', '/foo/test.txt'], - ['/foo/test.txt.part', '/foo/test.txt'], - ['/foo/test.txt.ocTransferId7567846853.part', '/foo/test.txt'], - ['/foo/test.txt.ocTransferId7567.part', '/foo/test.txt'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestParseRawHeader')] - public function testParseRawHeader($rawHeader, $expected): void { - $result = $this->util->parseRawHeader($rawHeader); - $this->assertSameSize($expected, $result); - foreach ($result as $key => $value) { - $this->assertArrayHasKey($key, $expected); - $this->assertSame($expected[$key], $value); - } - } - - public static function dataTestParseRawHeader(): array { - return [ - [str_pad('HBEGIN:oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) - , [Util::HEADER_ENCRYPTION_MODULE_KEY => '0']], - [str_pad('HBEGIN:oc_encryption_module:0:custom_header:foo:HEND', self::$headerSize, '-', STR_PAD_RIGHT) - , ['custom_header' => 'foo', Util::HEADER_ENCRYPTION_MODULE_KEY => '0']], - [str_pad('HelloWorld', self::$headerSize, '-', STR_PAD_RIGHT), []], - ['', []], - [str_pad('HBEGIN:oc_encryption_module:0', self::$headerSize, '-', STR_PAD_RIGHT) - , []], - [str_pad('oc_encryption_module:0:HEND', self::$headerSize, '-', STR_PAD_RIGHT) - , []], - ]; - } - - /** - * - * @param bool $isSystemWideMountPoint - * @param string $storageRoot - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetFileKeyDir')] - public function testGetFileKeyDir($isSystemWideMountPoint, $storageRoot, $expected): void { - $path = '/user1/files/foo/bar.txt'; - $owner = 'user1'; - $relativePath = '/foo/bar.txt'; - - $util = $this->getMockBuilder(Util::class) - ->onlyMethods(['isSystemWideMountPoint', 'getUidAndFilename', 'getKeyStorageRoot']) - ->setConstructorArgs([ - $this->view, - $this->userManager, - $this->groupManager, - $this->config - ]) - ->getMock(); - - $util->expects($this->once())->method('getKeyStorageRoot') - ->willReturn($storageRoot); - $util->expects($this->once())->method('isSystemWideMountPoint') - ->willReturn($isSystemWideMountPoint); - $util->expects($this->once())->method('getUidAndFilename') - ->with($path)->willReturn([$owner, $relativePath]); - - $this->assertSame($expected, - $util->getFileKeyDir('OC_DEFAULT_MODULE', $path) - ); - } - - public static function dataTestGetFileKeyDir(): array { - return [ - [false, '', '/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], - [true, '', '/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], - [false, 'newStorageRoot', '/newStorageRoot/user1/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], - [true, 'newStorageRoot', '/newStorageRoot/files_encryption/keys/foo/bar.txt/OC_DEFAULT_MODULE/'], - ]; - } -} --- a/tests/lib/ErrorHandlerTest.php +++ /dev/null @@ -1,79 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->errorHandler = new ErrorHandler( - $this->logger - ); - - $this->errorReporting = error_reporting(E_ALL); - } - - #[\Override] - protected function tearDown(): void { - error_reporting($this->errorReporting); - parent::tearDown(); - } - - /** - * provide username, password combinations for testRemovePassword - * @return array - */ - public static function passwordProvider(): array { - return [ - ['us:er', 'pass@word'], - ['us:er', 'password'], - ['user', '-C:R,w)@6*}'], - ['user', 'pass:word'], - ['user', 'pass@word'], - ['user', 'password'], - ['user:test@cloud', 'password'], - ['user@owncloud.org', 'password'], - ['user@test@owncloud.org', 'password'], - ]; - } - - /** - * @param string $username - * @param string $password - */ - #[\PHPUnit\Framework\Attributes\DataProvider('passwordProvider')] - public function testRemovePasswordFromError($username, $password): void { - $url = 'http://' . $username . ':' . $password . '@owncloud.org'; - $expectedResult = 'http://xxx:xxx@owncloud.org'; - $this->logger->expects(self::once()) - ->method('log') - ->with( - ILogger::ERROR, - 'Could not reach ' . $expectedResult . ' at file#4', - ['app' => 'PHP'], - ); - - $result = $this->errorHandler->onError(E_USER_ERROR, 'Could not reach ' . $url, 'file', 4); - - self::assertTrue($result); - } -} --- a/tests/lib/EventSourceFactoryTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createMock(IRequest::class); - $factory = new EventSourceFactory($request); - - $instance = $factory->create(); - $this->assertInstanceOf(IEventSource::class, $instance); - } -} --- a/tests/lib/Federation/CloudIdManagerTest.php +++ /dev/null @@ -1,173 +0,0 @@ -contactsManager = $this->createMock(IManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->userManager = $this->createMock(IUserManager::class); - - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cacheFactory->method('createDistributed') - ->willReturn(new ArrayCache('')); - - $this->cloudIdManager = new CloudIdManager( - $this->cacheFactory, - $this->createMock(IEventDispatcher::class), - $this->contactsManager, - $this->urlGenerator, - $this->userManager, - ); - $this->overwriteService(ICloudIdManager::class, $this->cloudIdManager); - } - - public static function dataGetDisplayNameFromContact(): array { - return [ - ['test1@example.tld', 'test', 'test'], - ['test2@example.tld', null, null], - ['test3@example.tld', 'test3@example', 'test3@example'], - ['test4@example.tld', 'test4@example.tld', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetDisplayNameFromContact')] - public function testGetDisplayNameFromContact(string $cloudId, ?string $displayName, ?string $expected): void { - $returnedContact = [ - 'CLOUD' => [$cloudId], - 'FN' => $expected, - ]; - if ($displayName === null) { - unset($returnedContact['FN']); - } - $this->contactsManager->method('search') - ->with($cloudId, ['CLOUD']) - ->willReturn([$returnedContact]); - - $this->assertEquals($expected, $this->cloudIdManager->getDisplayNameFromContact($cloudId)); - $this->assertEquals($expected, $this->cloudIdManager->getDisplayNameFromContact($cloudId)); - } - - public static function cloudIdProvider(): array { - return [ - ['test@example.com', 'test', 'example.com', 'test@example.com'], - ['test@example.com/cloud', 'test', 'example.com/cloud', 'test@example.com/cloud'], - ['test@example.com/cloud/', 'test', 'example.com/cloud', 'test@example.com/cloud'], - ['test@example.com/cloud/index.php', 'test', 'example.com/cloud', 'test@example.com/cloud'], - ['test@example.com@example.com', 'test@example.com', 'example.com', 'test@example.com@example.com'], - - // Equal signs are not valid on Nextcloud side, but can be used by other federated OCM compatible servers - ['test==@example.com', 'test==', 'example.com', 'test==@example.com'], - ['==@example.com', '==', 'example.com', '==@example.com'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('cloudIdProvider')] - public function testResolveCloudId(string $cloudId, string $user, string $noProtocolRemote, string $cleanId): void { - $displayName = 'Ample Ex'; - - $this->contactsManager->expects($this->any()) - ->method('search') - ->with($cleanId, ['CLOUD']) - ->willReturn([ - [ - 'CLOUD' => [$cleanId], - 'FN' => $displayName, - ] - ]); - - $cloudId = $this->cloudIdManager->resolveCloudId($cloudId); - - $this->assertEquals($user, $cloudId->getUser()); - $this->assertEquals('https://' . $noProtocolRemote, $cloudId->getRemote()); - $this->assertEquals($cleanId, $cloudId->getId()); - $this->assertEquals($displayName . '@' . $noProtocolRemote, $cloudId->getDisplayId()); - } - - public static function invalidCloudIdProvider(): array { - return [ - ['example.com'], - ['test:foo@example.com'], - ['test/foo@example.com'] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('invalidCloudIdProvider')] - public function testInvalidCloudId(string $cloudId): void { - $this->expectException(\InvalidArgumentException::class); - - $this->contactsManager->expects($this->never()) - ->method('search'); - - $this->cloudIdManager->resolveCloudId($cloudId); - } - - public static function getCloudIdProvider(): array { - return [ - ['test', 'example.com', 'test@example.com', null, 'https://example.com', 'https://example.com'], - ['test', 'http://example.com', 'test@http://example.com', 'test@example.com'], - ['test', null, 'test@http://example.com', 'test@example.com', 'http://example.com', 'http://example.com'], - ['test@example.com', 'example.com', 'test@example.com@example.com', null, 'https://example.com', 'https://example.com'], - ['test@example.com', 'https://example.com', 'test@example.com@example.com'], - ['test@example.com', null, 'test@example.com@example.com', null, 'https://example.com', 'https://example.com'], - ['test@example.com', 'https://example.com/index.php/s/shareToken', 'test@example.com@example.com', null, 'https://example.com', 'https://example.com'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('getCloudIdProvider')] - public function testGetCloudId(string $user, ?string $remote, string $id, ?string $searchCloudId = null, ?string $localHost = 'https://example.com', ?string $expectedRemoteId = null): void { - if ($remote !== null) { - $this->contactsManager->expects($this->any()) - ->method('search') - ->with($searchCloudId ?? $id, ['CLOUD']) - ->willReturn([ - [ - 'CLOUD' => [$searchCloudId ?? $id], - 'FN' => 'Ample Ex', - ] - ]); - } else { - $this->urlGenerator->expects(self::once()) - ->method('getAbsoluteUrl') - ->willReturn($localHost); - } - $expectedRemoteId ??= $remote; - - $cloudId = $this->cloudIdManager->getCloudId($user, $remote); - - $this->assertEquals($id, $cloudId->getId(), 'Cloud ID'); - $this->assertEquals($expectedRemoteId, $cloudId->getRemote(), 'Remote URL'); - } -} --- a/tests/lib/Federation/CloudIdTest.php +++ /dev/null @@ -1,48 +0,0 @@ -cloudIdManager = $this->createMock(CloudIdManager::class); - $this->overwriteService(ICloudIdManager::class, $this->cloudIdManager); - } - - public static function dataGetDisplayCloudId(): array { - return [ - ['test@example.com', 'test', 'example.com', 'test@example.com'], - ['test@http://example.com', 'test', 'http://example.com', 'test@example.com'], - ['test@https://example.com', 'test', 'https://example.com', 'test@example.com'], - ['test@https://example.com', 'test', 'https://example.com', 'Beloved Amy@example.com', 'Beloved Amy'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetDisplayCloudId')] - public function testGetDisplayCloudId(string $id, string $user, string $remote, string $display, ?string $addressbookName = null): void { - $this->cloudIdManager->expects($this->once()) - ->method('getDisplayNameFromContact') - ->willReturn($addressbookName); - - $cloudId = new CloudId($id, $user, $remote); - $this->assertEquals($display, $cloudId->getDisplayId()); - } -} --- a/tests/lib/Files/AppData/AppDataTest.php +++ /dev/null @@ -1,96 +0,0 @@ -rootFolder = $this->createMock(IRootFolder::class); - $this->systemConfig = $this->createMock(SystemConfig::class); - $this->appData = new AppData($this->rootFolder, $this->systemConfig, 'myApp'); - - $this->systemConfig->expects($this->any()) - ->method('getValue') - ->with('instanceid', null) - ->willReturn('iid'); - } - - private function setupAppFolder() { - $appFolder = $this->createMock(Folder::class); - - $this->rootFolder->expects($this->any()) - ->method('get') - ->with($this->equalTo('appdata_iid/myApp')) - ->willReturn($appFolder); - - return $appFolder; - } - - public function testGetFolder(): void { - $folder = $this->createMock(Folder::class); - - $this->rootFolder->expects($this->once()) - ->method('get') - ->with($this->equalTo('appdata_iid/myApp/folder')) - ->willReturn($folder); - - $result = $this->appData->getFolder('folder'); - $this->assertInstanceOf(ISimpleFolder::class, $result); - } - - public function testNewFolder(): void { - $appFolder = $this->setupAppFolder(); - - $folder = $this->createMock(Folder::class); - - $appFolder->expects($this->once()) - ->method('newFolder') - ->with($this->equalTo('folder')) - ->willReturn($folder); - - $result = $this->appData->newFolder('folder'); - $this->assertInstanceOf(ISimpleFolder::class, $result); - } - - public function testGetDirectoryListing(): void { - $appFolder = $this->setupAppFolder(); - - $file = $this->createMock(File::class); - $folder = $this->createMock(Folder::class); - $node = $this->createMock(Node::class); - - $appFolder->expects($this->once()) - ->method('getDirectoryListing') - ->willReturn([$file, $folder, $node]); - - $result = $this->appData->getDirectoryListing(); - - $this->assertCount(1, $result); - $this->assertInstanceOf(ISimpleFolder::class, $result[0]); - } -} --- a/tests/lib/Files/AppData/FactoryTest.php +++ /dev/null @@ -1,41 +0,0 @@ -rootFolder = $this->createMock(IRootFolder::class); - $this->systemConfig = $this->createMock(SystemConfig::class); - $this->factory = new Factory($this->rootFolder, $this->systemConfig); - } - - public function testGet(): void { - $this->rootFolder->expects($this->never()) - ->method($this->anything()); - $this->systemConfig->expects($this->never()) - ->method($this->anything()); - - $this->factory->get('foo'); - } -} --- a/tests/lib/Files/Cache/CacheTest.php +++ /dev/null @@ -1,1086 +0,0 @@ -storage = new Temporary([]); - $this->storage2 = new Temporary([]); - $this->cache = new Cache($this->storage); - $this->cache2 = new Cache($this->storage2); - $this->cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $this->cache2->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - } - - #[\Override] - protected function tearDown(): void { - if ($this->cache) { - $this->cache->clear(); - } - - parent::tearDown(); - } - - public function testGetNumericId(): void { - $this->assertNotNull($this->cache->getNumericStorageId()); - } - - public function testSimple(): void { - $file1 = 'foo'; - $file2 = 'foo/bar'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - $data2 = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - - $this->assertFalse($this->cache->inCache($file1)); - $this->assertEquals($this->cache->get($file1), null); - - $id1 = $this->cache->put($file1, $data1); - $this->assertTrue($this->cache->inCache($file1)); - $cacheData1 = $this->cache->get($file1); - foreach ($data1 as $key => $value) { - $this->assertEquals($value, $cacheData1[$key]); - } - $this->assertEquals($cacheData1['mimepart'], 'foo'); - $this->assertEquals($cacheData1['fileid'], $id1); - $this->assertEquals($id1, $this->cache->getId($file1)); - - $this->assertFalse($this->cache->inCache($file2)); - $id2 = $this->cache->put($file2, $data2); - $this->assertTrue($this->cache->inCache($file2)); - $cacheData2 = $this->cache->get($file2); - foreach ($data2 as $key => $value) { - $this->assertEquals($value, $cacheData2[$key]); - } - $this->assertEquals($cacheData1['fileid'], $cacheData2['parent']); - $this->assertEquals($cacheData2['fileid'], $id2); - $this->assertEquals($id2, $this->cache->getId($file2)); - $this->assertEquals($id1, $this->cache->getParentId($file2)); - - $newSize = 1050; - $newId2 = $this->cache->put($file2, ['size' => $newSize]); - $cacheData2 = $this->cache->get($file2); - $this->assertEquals($newId2, $id2); - $this->assertEquals($cacheData2['size'], $newSize); - $this->assertEquals($cacheData1, $this->cache->get($file1)); - - $this->cache->remove($file2); - $this->assertFalse($this->cache->inCache($file2)); - $this->assertEquals($this->cache->get($file2), null); - $this->assertTrue($this->cache->inCache($file1)); - - $this->assertEquals($cacheData1, $this->cache->get($id1)); - } - - public function testCacheEntryGetters(): void { - $file1 = 'foo'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/file']; - - $id1 = $this->cache->put($file1, $data1); - $entry = $this->cache->get($file1); - - $this->assertEquals($entry->getId(), $id1); - $this->assertEquals($entry->getStorageId(), $this->cache->getNumericStorageId()); - $this->assertEquals($entry->getPath(), 'foo'); - $this->assertEquals($entry->getName(), 'foo'); - $this->assertEquals($entry->getMimeType(), 'foo/file'); - $this->assertEquals($entry->getMimePart(), 'foo'); - $this->assertEquals($entry->getSize(), 100); - $this->assertEquals($entry->getMTime(), 50); - $this->assertEquals($entry->getStorageMTime(), 50); - $this->assertEquals($entry->getEtag(), null); - $this->assertEquals($entry->getPermissions(), 0); - $this->assertEquals($entry->isEncrypted(), false); - $this->assertEquals($entry->getMetadataEtag(), null); - $this->assertEquals($entry->getCreationTime(), null); - $this->assertEquals($entry->getUploadTime(), null); - $this->assertEquals($entry->getUnencryptedSize(), 100); - } - - public function testGetUnencryptedSizeEncryptedZeroByte(): void { - $file1 = 'encrypted_zero'; - $this->cache->put($file1, ['size' => 8192, 'mtime' => 50, 'mimetype' => 'application/octet-stream', 'encrypted' => 1, 'unencrypted_size' => 0]); - $entry = $this->cache->get($file1); - - // getUnencryptedSize() must return 0 (the true plaintext size), not 8192 (the encrypted on-disk size) - $this->assertEquals(0, $entry->getUnencryptedSize()); - $this->assertTrue($entry->isEncrypted()); - } - - public function testPartial(): void { - $file1 = 'foo'; - - $this->cache->put($file1, ['size' => 10]); - $this->assertEquals(new CacheEntry(['size' => 10]), $this->cache->get($file1)); - - $this->cache->put($file1, ['mtime' => 15]); - $this->assertEquals(new CacheEntry(['size' => 10, 'mtime' => 15]), $this->cache->get($file1)); - - $this->cache->put($file1, ['size' => 12]); - $this->assertEquals(new CacheEntry(['size' => 12, 'mtime' => 15]), $this->cache->get($file1)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('folderDataProvider')] - public function testFolder($folder): void { - if (strpos($folder, 'F09F9890')) { - // 4 byte UTF doesn't work on mysql - $params = Server::get(\OC\DB\Connection::class)->getParams(); - if (Server::get(IDBConnection::class)->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL && $params['charset'] !== 'utf8mb4') { - $this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8'); - } - } - $file2 = $folder . '/bar'; - $file3 = $folder . '/foo'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData = []; - $fileData['bar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file']; - - $this->cache->put($folder, $data1); - $this->cache->put($file2, $fileData['bar']); - $this->cache->put($file3, $fileData['foo']); - - $content = $this->cache->getFolderContents($folder); - $this->assertEquals(count($content), 2); - foreach ($content as $cachedData) { - $data = $fileData[$cachedData['name']]; - foreach ($data as $name => $value) { - $this->assertEquals($value, $cachedData[$name]); - } - } - - $file4 = $folder . '/unkownSize'; - $fileData['unkownSize'] = ['size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file']; - $this->cache->put($file4, $fileData['unkownSize']); - - $this->assertEquals(-1, $this->cache->calculateFolderSize($folder)); - - $fileData['unkownSize'] = ['size' => 5, 'mtime' => 25, 'mimetype' => 'foo/file']; - $this->cache->put($file4, $fileData['unkownSize']); - - $this->assertEquals(1025, $this->cache->calculateFolderSize($folder)); - - $this->cache->remove($file2); - $this->cache->remove($file3); - $this->cache->remove($file4); - $this->assertEquals(0, $this->cache->calculateFolderSize($folder)); - - $this->cache->remove($folder); - $this->assertFalse($this->cache->inCache($folder . '/foo')); - $this->assertFalse($this->cache->inCache($folder . '/bar')); - } - - public function testRemoveRecursive(): void { - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'text/plain']; - $folders = ['folder', 'folder/subfolder', 'folder/sub2', 'folder/sub2/sub3']; - $files = ['folder/foo.txt', 'folder/bar.txt', 'folder/subfolder/asd.txt', 'folder/sub2/qwerty.txt', 'folder/sub2/sub3/foo.txt']; - - foreach ($folders as $folder) { - $this->cache->put($folder, $folderData); - } - foreach ($files as $file) { - $this->cache->put($file, $fileData); - } - - $this->cache->remove('folder'); - foreach ($files as $file) { - $this->assertFalse($this->cache->inCache($file)); - } - } - - /** - * @return array{0: Cache, 1: \Closure(): list} - */ - private function cacheWithRecordedEvents(IStorage $storage): array { - $events = []; - $dispatcher = $this->createMock(IEventDispatcher::class); - $dispatcher->method('dispatchTyped') - ->willReturnCallback(function (object $event) use (&$events): void { - if ($event instanceof CacheEntriesRemovedEvent) { - $events[] = $event; - } - }); - - $dependencies = new CacheDependencies( - Server::get(IMimeTypeLoader::class), - Server::get(IDBConnection::class), - $dispatcher, - Server::get(QuerySearchHelper::class), - Server::get(SystemConfig::class), - Server::get(LoggerInterface::class), - Server::get(IFilesMetadataManager::class), - Server::get(DisplayNameCache::class), - ); - - return [new Cache($storage, $dependencies), function () use (&$events): array { - return $events; - }]; - } - - /** - * @param list $events - * @return list - */ - private function announcedFileIds(array $events): array { - $fileIds = []; - foreach ($events as $event) { - foreach ($event->getCacheEntryRemovedEvents() as $removed) { - $fileIds[] = $removed->getFileId(); - } - } - return $fileIds; - } - - public function testRemoveSingleFileAnnouncesBatchEvent(): void { - $storage = new Temporary([]); - [$cache, $recorded] = $this->cacheWithRecordedEvents($storage); - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $fileId = $cache->put('foo.txt', ['size' => 1, 'mtime' => 20, 'mimetype' => 'text/plain']); - - $cache->remove('foo.txt'); - - $this->assertEquals([$fileId], $this->announcedFileIds($recorded())); - } - - public function testRemoveRecursiveAnnouncesRealFileIds(): void { - $storage = new Temporary([]); - [$cache, $recorded] = $this->cacheWithRecordedEvents($storage); - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'text/plain']; - - $expected = [$cache->put('folder', $folderData)]; - $expected[] = $cache->put('folder/sub', $folderData); - $expected[] = $cache->put('folder/foo.txt', $fileData); - $expected[] = $cache->put('folder/sub/bar.txt', $fileData); - - $cache->remove('folder'); - - $announced = $this->announcedFileIds($recorded()); - sort($expected); - sort($announced); - $this->assertEquals($expected, $announced); - } - - public function testRemoveRecursiveAnnouncesEveryChildExactlyOnce(): void { - $storage = new Temporary([]); - [$cache, $recorded] = $this->cacheWithRecordedEvents($storage); - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $fileData = ['size' => 1, 'mtime' => 20, 'mimetype' => 'text/plain']; - - $expected = [$cache->put('folder', ['size' => 0, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE])]; - for ($i = 0; $i < IQueryBuilder::MAX_IN_PARAMETERS + 1; $i++) { - $expected[] = $cache->insert("folder/child$i.txt", $fileData); - } - - $cache->remove('folder'); - - $announced = $this->announcedFileIds($recorded()); - $this->assertCount(count($expected), $announced, 'every removed entry is announced exactly once'); - sort($expected); - sort($announced); - $this->assertEquals($expected, $announced); - } - - public function testClearRemovesExtendedAndMetadataEntries(): void { - $cache = new Cache($this->storage); - $fileId = $cache->put('foo', ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'upload_time' => 30]); - - $metadataManager = Server::get(IFilesMetadataManager::class); - $metadata = $metadataManager->getMetadata($fileId, true); - $metadata->setString('test-key', 'value', true); - $metadataManager->saveMetadata($metadata); - - $cache->clear(); - - $this->assertFalse($cache->inCache('foo')); - $connection = Server::get(IDBConnection::class); - foreach (['filecache_extended' => 'fileid', 'files_metadata' => 'file_id', 'files_metadata_index' => 'file_id'] as $table => $column) { - $query = $connection->getQueryBuilder(); - $query->select($column) - ->from($table) - ->where($query->expr()->eq($column, $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - $this->assertSame([], $query->executeQuery()->fetchFirstColumn(), "$table rows of the cleared storage remain"); - } - } - - public static function folderDataProvider(): array { - return [ - ['folder'], - // that was too easy, try something harder - ['☺, WHITE SMILING FACE, UTF-8 hex E298BA'], - // what about 4 byte utf-8 - ['😐, NEUTRAL_FACE, UTF-8 hex F09F9890'], - // now the crazy stuff - [', UNASSIGNED PRIVATE USE, UTF-8 hex EF9890'], - // and my favorite - ['w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝'] - ]; - } - - public function testEncryptedFolder(): void { - $file1 = 'folder'; - $file2 = 'folder/bar'; - $file3 = 'folder/foo'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData = []; - $fileData['bar'] = ['size' => 1000, 'encrypted' => 1, 'mtime' => 20, 'mimetype' => 'foo/file']; - $fileData['foo'] = ['size' => 20, 'encrypted' => 1, 'mtime' => 25, 'mimetype' => 'foo/file']; - - $this->cache->put($file1, $data1); - $this->cache->put($file2, $fileData['bar']); - $this->cache->put($file3, $fileData['foo']); - - $content = $this->cache->getFolderContents($file1); - $this->assertEquals(count($content), 2); - foreach ($content as $cachedData) { - $data = $fileData[$cachedData['name']]; - } - - $file4 = 'folder/unkownSize'; - $fileData['unkownSize'] = ['size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file']; - $this->cache->put($file4, $fileData['unkownSize']); - - $this->assertEquals(-1, $this->cache->calculateFolderSize($file1)); - - $fileData['unkownSize'] = ['size' => 5, 'mtime' => 25, 'mimetype' => 'foo/file']; - $this->cache->put($file4, $fileData['unkownSize']); - - $this->assertEquals(1025, $this->cache->calculateFolderSize($file1)); - // direct cache entry retrieval returns the original values - $entry = $this->cache->get($file1); - $this->assertEquals(1025, $entry['size']); - - $this->cache->remove($file2); - $this->cache->remove($file3); - $this->cache->remove($file4); - $this->assertEquals(0, $this->cache->calculateFolderSize($file1)); - - $this->cache->remove('folder'); - $this->assertFalse($this->cache->inCache('folder/foo')); - $this->assertFalse($this->cache->inCache('folder/bar')); - } - - public function testCalculateFolderSizeWithEncryptedZeroByte(): void { - $folder = 'enc_folder'; - $this->cache->put($folder, ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - - // Child 1: zero-byte encrypted file — on-disk 8192 (header only), plaintext 0 - $child1 = $folder . '/empty.enc'; - $this->cache->put($child1, [ - 'size' => 8192, - 'mtime' => 20, - 'mimetype' => 'application/octet-stream', - 'encrypted' => 1, - 'unencrypted_size' => 0, - ]); - - // Child 2: non-zero encrypted file — opens the write-back gate ($unencryptedMax > 0) - $child2 = $folder . '/small.enc'; - $this->cache->put($child2, [ - 'size' => 8292, - 'mtime' => 20, - 'mimetype' => 'application/octet-stream', - 'encrypted' => 1, - 'unencrypted_size' => 100, - ]); - - $this->cache->calculateFolderSize($folder); - - $entry = $this->cache->get($folder); - // Must sum plaintext sizes (0 + 100 = 100), not fall back to on-disk size for - // the zero-byte child (8192 + 100 = 8292 with the old buggy code) - $this->assertEquals(100, $entry['unencrypted_size'], 'Folder unencrypted_size should sum plaintext sizes'); - $this->assertEquals(16484, $entry['size'], 'Folder size should sum on-disk sizes (8192 + 8292)'); - } - - public function testRootFolderSizeForNonHomeStorage(): void { - $dir1 = 'knownsize'; - $dir2 = 'unknownsize'; - $fileData = []; - $fileData[''] = ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData[$dir1] = ['size' => 1000, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $fileData[$dir2] = ['size' => -1, 'mtime' => 25, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->cache->put($dir1, $fileData[$dir1]); - $this->cache->put($dir2, $fileData[$dir2]); - - $this->assertTrue($this->cache->inCache($dir1)); - $this->assertTrue($this->cache->inCache($dir2)); - - // check that root size ignored the unknown sizes - $this->assertEquals(-1, $this->cache->calculateFolderSize('')); - - // clean up - $this->cache->remove(''); - $this->cache->remove($dir1); - $this->cache->remove($dir2); - - $this->assertFalse($this->cache->inCache($dir1)); - $this->assertFalse($this->cache->inCache($dir2)); - } - - public function testStatus(): void { - $this->assertEquals(Cache::NOT_FOUND, $this->cache->getStatus('foo')); - $this->cache->put('foo', ['size' => -1]); - $this->assertEquals(Cache::PARTIAL, $this->cache->getStatus('foo')); - $this->cache->put('foo', ['size' => -1, 'mtime' => 20, 'mimetype' => 'foo/file']); - $this->assertEquals(Cache::SHALLOW, $this->cache->getStatus('foo')); - $this->cache->put('foo', ['size' => 10]); - $this->assertEquals(Cache::COMPLETE, $this->cache->getStatus('foo')); - } - - public static function putWithAllKindOfQuotesData(): array { - return [ - ['`backtick`'], - ['´forward´'], - ['\'single\''], - ]; - } - - /** - * @param $fileName - */ - #[\PHPUnit\Framework\Attributes\DataProvider('putWithAllKindOfQuotesData')] - public function testPutWithAllKindOfQuotes($fileName): void { - $this->assertEquals(Cache::NOT_FOUND, $this->cache->get($fileName)); - $this->cache->put($fileName, ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file', 'etag' => $fileName]); - - $cacheEntry = $this->cache->get($fileName); - $this->assertEquals($fileName, $cacheEntry['etag']); - $this->assertEquals($fileName, $cacheEntry['path']); - } - - public function testSearch(): void { - $file1 = 'folder'; - $file2 = 'folder/foobar'; - $file3 = 'folder/foo'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - $fileData = []; - $fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file']; - - $this->cache->put($file1, $data1); - $this->cache->put($file2, $fileData['foobar']); - $this->cache->put($file3, $fileData['foo']); - - $this->assertEquals(2, count($this->cache->search('%foo%'))); - $this->assertEquals(1, count($this->cache->search('foo'))); - $this->assertEquals(1, count($this->cache->search('%folder%'))); - $this->assertEquals(1, count($this->cache->search('folder%'))); - - // case insensitive search should match the same files - $this->assertEquals(2, count($this->cache->search('%Foo%'))); - $this->assertEquals(1, count($this->cache->search('Foo'))); - $this->assertEquals(1, count($this->cache->search('%Folder%'))); - $this->assertEquals(1, count($this->cache->search('Folder%'))); - - $this->assertEquals(3, count($this->cache->searchByMime('foo'))); - $this->assertEquals(2, count($this->cache->searchByMime('foo/file'))); - } - - public function testSearchQueryByTag(): void { - $userId = static::getUniqueID('user'); - Server::get(IUserManager::class)->createUser($userId, $userId); - static::loginAsUser($userId); - $user = new User($userId, null, Server::get(IEventDispatcher::class)); - - $file1 = 'folder'; - $file2 = 'folder/foobar'; - $file3 = 'folder/foo'; - $file4 = 'folder/foo2'; - $file5 = 'folder/foo3'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - $fileData = []; - $fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file']; - $fileData['foo2'] = ['size' => 25, 'mtime' => 28, 'mimetype' => 'foo/file']; - $fileData['foo3'] = ['size' => 88, 'mtime' => 34, 'mimetype' => 'foo/file']; - - $id1 = $this->cache->put($file1, $data1); - $id2 = $this->cache->put($file2, $fileData['foobar']); - $id3 = $this->cache->put($file3, $fileData['foo']); - $id4 = $this->cache->put($file4, $fileData['foo2']); - $id5 = $this->cache->put($file5, $fileData['foo3']); - - $tagManager = Server::get(ITagManager::class)->load('files', [], false, $userId); - $this->assertTrue($tagManager->tagAs($id1, 'tag1')); - $this->assertTrue($tagManager->tagAs($id1, 'tag2')); - $this->assertTrue($tagManager->tagAs($id2, 'tag2')); - $this->assertTrue($tagManager->tagAs($id3, 'tag1')); - $this->assertTrue($tagManager->tagAs($id4, 'tag2')); - - $results = $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', 'tag2'), - 0, 0, [], $user - )); - $this->assertEquals(3, count($results)); - - usort($results, function ($value1, $value2) { - return $value1['name'] <=> $value2['name']; - }); - - $this->assertEquals('folder', $results[0]['name']); - $this->assertEquals('foo2', $results[1]['name']); - $this->assertEquals('foobar', $results[2]['name']); - - $tagManager->delete('tag1'); - $tagManager->delete('tag2'); - - static::logout(); - $user = Server::get(IUserManager::class)->get($userId); - if ($user !== null) { - try { - $user->delete(); - } catch (\Exception $e) { - } - } - } - - public function testSearchByQuery(): void { - $file1 = 'folder'; - $file2 = 'folder/foobar'; - $file3 = 'folder/foo'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - $fileData = []; - $fileData['foobar'] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $fileData['foo'] = ['size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file']; - - $this->cache->put($file1, $data1); - $this->cache->put($file2, $fileData['foobar']); - $this->cache->put($file3, $fileData['foo']); - /** @var IUser $user */ - $user = $this->createMock(IUser::class); - - $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo'), 10, 0, [], $user))); - $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), 10, 0, [], $user))); - $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file'), 10, 0, [], $user))); - $this->assertCount(3, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'foo/%'), 10, 0, [], $user))); - $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', 100), 10, 0, [], $user))); - $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100), 10, 0, [], $user))); - } - - public static function movePathProvider(): array { - return [ - ['folder/foo', 'folder/foobar', ['1', '2']], - ['folder/foo', 'foo', ['1', '2']], - ['files/Индустрия_Инженерные системы ЦОД', 'files/Индустрия_Инженерные системы ЦОД1', ['1', '2']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('movePathProvider')] - public function testMove($sourceFolder, $targetFolder, $children): void { - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar']; - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - // create folders - foreach ([$sourceFolder, $targetFolder] as $current) { - while (strpos($current, '/') > 0) { - $current = dirname($current); - $this->cache->put($current, $folderData); - $this->cache2->put($current, $folderData); - } - } - - $this->cache->put($sourceFolder, $folderData); - $this->cache2->put($sourceFolder, $folderData); - foreach ($children as $child) { - $this->cache->put($sourceFolder . '/' . $child, $data); - $this->cache2->put($sourceFolder . '/' . $child, $data); - } - - $this->cache->move($sourceFolder, $targetFolder); - - $this->assertFalse($this->cache->inCache($sourceFolder)); - $this->assertTrue($this->cache2->inCache($sourceFolder)); - $this->assertTrue($this->cache->inCache($targetFolder)); - $this->assertFalse($this->cache2->inCache($targetFolder)); - foreach ($children as $child) { - $this->assertFalse($this->cache->inCache($sourceFolder . '/' . $child)); - $this->assertTrue($this->cache2->inCache($sourceFolder . '/' . $child)); - $this->assertTrue($this->cache->inCache($targetFolder . '/' . $child)); - $this->assertFalse($this->cache2->inCache($targetFolder . '/' . $child)); - } - } - - public function testMoveFromCache(): void { - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar']; - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->cache2->put('folder', $folderData); - $this->cache2->put('folder/sub', $data); - - $this->cache->moveFromCache($this->cache2, 'folder', 'targetfolder'); - - $this->assertFalse($this->cache2->inCache('folder')); - $this->assertFalse($this->cache2->inCache('folder/sub')); - - $this->assertTrue($this->cache->inCache('targetfolder')); - $this->assertTrue($this->cache->inCache('targetfolder/sub')); - } - - public function testMoveFromCacheJail(): void { - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar']; - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->cache2->put('folder', $folderData); - $this->cache2->put('folder/sub', $data); - - $jail = new CacheJail($this->cache2, 'folder'); - - $this->cache->moveFromCache($jail, 'sub', 'targetsub'); - - $this->assertTrue($this->cache2->inCache('folder')); - $this->assertFalse($this->cache2->inCache('folder/sub')); - - $this->assertTrue($this->cache->inCache('targetsub')); - $this->assertEquals($this->cache->getId(''), $this->cache->get('targetsub')->getParentId()); - } - - public function testCopyFromCachePreservesEncryptedVersion(): void { - $data = [ - 'size' => 100, 'mtime' => 50, 'mimetype' => 'foo/bar', - 'encrypted' => true, 'encryptedVersion' => 3, - ]; - $this->cache->put('source', $data); - $sourceEntry = $this->cache->get('source'); - $this->assertSame(3, $sourceEntry['encryptedVersion']); - - $this->cache->copyFromCache($this->cache, $sourceEntry, 'target'); - - $targetEntry = $this->cache->get('target'); - $this->assertTrue($targetEntry->isEncrypted()); - $this->assertSame(3, $targetEntry['encryptedVersion']); - } - - public function testCopyFromCachePreservesUnencryptedSize(): void { - $data = [ - 'size' => 128, 'mtime' => 50, 'mimetype' => 'foo/bar', - 'encrypted' => true, 'encryptedVersion' => 3, 'unencrypted_size' => 100, - ]; - $this->cache->put('source', $data); - $sourceEntry = $this->cache->get('source'); - $this->assertEquals(100, $sourceEntry->getUnencryptedSize()); - - $this->cache->copyFromCache($this->cache, $sourceEntry, 'target'); - - $targetEntry = $this->cache->get('target'); - $this->assertEquals(100, $targetEntry->getUnencryptedSize()); - } - - public function testCopyFromCacheClearsEncryptedVersionWhenCopyingToNonEncryptedStorage(): void { - $data = [ - 'size' => 128, 'mtime' => 50, 'mimetype' => 'foo/bar', - 'encrypted' => true, 'encryptedVersion' => 3, 'unencrypted_size' => 100, - ]; - $this->cache2->put('source', $data); - $sourceEntry = $this->cache2->get('source'); - - $sourceCache = $this->getMockBuilder(Cache::class) - ->setConstructorArgs([$this->storage2]) - ->onlyMethods(['hasEncryptionWrapper']) - ->getMock(); - $sourceCache->method('hasEncryptionWrapper')->willReturn(true); - - $targetCache = $this->getMockBuilder(Cache::class) - ->setConstructorArgs([$this->storage]) - ->onlyMethods(['shouldEncrypt']) - ->getMock(); - $targetCache->method('shouldEncrypt')->willReturn(false); - - $targetCache->copyFromCache($sourceCache, $sourceEntry, 'target'); - - $targetEntry = $targetCache->get('target'); - $this->assertFalse($targetEntry->isEncrypted()); - $this->assertSame(0, $targetEntry['encryptedVersion']); - // the target is not marked as encrypted, so its size is read from `size` - $this->assertEquals(0, $targetEntry['unencrypted_size']); - $this->assertEquals(128, $targetEntry->getSize()); - } - - public function testGetIncomplete(): void { - $file1 = 'folder1'; - $file2 = 'folder2'; - $file3 = 'folder3'; - $file4 = 'folder4'; - $data = ['size' => 10, 'mtime' => 50, 'mimetype' => 'foo/bar']; - - $this->cache->put($file1, $data); - $data['size'] = -1; - $this->cache->put($file2, $data); - $this->cache->put($file3, $data); - $data['size'] = 12; - $this->cache->put($file4, $data); - - $this->assertEquals($file3, $this->cache->getIncomplete()); - } - - public function testNonExisting(): void { - $this->assertFalse($this->cache->get('foo.txt')); - $this->assertFalse($this->cache->get(-1)); - $this->assertEquals([], $this->cache->getFolderContents('foo')); - } - - public function testGetById(): void { - $storageId = $this->storage->getId(); - $data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $id = $this->cache->put('foo', $data); - - if (strlen($storageId) > 64) { - $storageId = md5($storageId); - } - $this->assertEquals([$storageId, 'foo'], Cache::getById($id)); - } - - public function testStorageMTime(): void { - $data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $this->cache->put('foo', $data); - $cachedData = $this->cache->get('foo'); - $this->assertEquals($data['mtime'], $cachedData['storage_mtime']); //if no storage_mtime is saved, mtime should be used - - $this->cache->put('foo', ['storage_mtime' => 30]); //when setting storage_mtime, mtime is also set - $cachedData = $this->cache->get('foo'); - $this->assertEquals(30, $cachedData['storage_mtime']); - $this->assertEquals(30, $cachedData['mtime']); - - $this->cache->put('foo', ['mtime' => 25]); //setting mtime does not change storage_mtime - $cachedData = $this->cache->get('foo'); - $this->assertEquals(30, $cachedData['storage_mtime']); - $this->assertEquals(25, $cachedData['mtime']); - } - - public function testLongId(): void { - $storage = new LongId([]); - $cache = $storage->getCache(); - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $storageId = $storage->getId(); - $data = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file']; - $id = $cache->put('foo', $data); - $this->assertEquals([md5($storageId), 'foo'], Cache::getById($id)); - } - - /** - * this test show the bug resulting if we have no normalizer installed - */ - public function testWithoutNormalizer(): void { - // folder name "Schön" with U+00F6 (normalized) - $folderWith00F6 = "\x53\x63\x68\xc3\xb6\x6e"; - - // folder name "Schön" with U+0308 (un-normalized) - $folderWith0308 = "\x53\x63\x68\x6f\xcc\x88\x6e"; - - /** - * @var Cache|\PHPUnit\Framework\MockObject\MockObject $cacheMock - */ - $cacheMock = $this->getMockBuilder(Cache::class) - ->onlyMethods(['normalize']) - ->setConstructorArgs([$this->storage]) - ->getMock(); - - $cacheMock->expects($this->any()) - ->method('normalize') - ->willReturnArgument(0); - - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - // put root folder - $this->assertFalse($cacheMock->get('folder')); - $this->assertGreaterThan(0, $cacheMock->put('folder', $data)); - - // put un-normalized folder - $this->assertFalse($cacheMock->get('folder/' . $folderWith0308)); - $this->assertGreaterThan(0, $cacheMock->put('folder/' . $folderWith0308, $data)); - - // get un-normalized folder by name - $unNormalizedFolderName = $cacheMock->get('folder/' . $folderWith0308); - - // check if database layer normalized the folder name (this should not happen) - $this->assertEquals($folderWith0308, $unNormalizedFolderName['name']); - - // put normalized folder - $this->assertFalse($cacheMock->get('folder/' . $folderWith00F6)); - $this->assertGreaterThan(0, $cacheMock->put('folder/' . $folderWith00F6, $data)); - - // this is our bug, we have two different hashes with the same name (Schön) - $this->assertEquals(2, count($cacheMock->getFolderContents('folder'))); - } - - /** - * this test shows that there is no bug if we use the normalizer - */ - public function testWithNormalizer(): void { - if (!class_exists('Patchwork\PHP\Shim\Normalizer')) { - $this->markTestSkipped('The 3rdparty Normalizer extension is not available.'); - return; - } - - // folder name "Schön" with U+00F6 (normalized) - $folderWith00F6 = "\x53\x63\x68\xc3\xb6\x6e"; - - // folder name "Schön" with U+0308 (un-normalized) - $folderWith0308 = "\x53\x63\x68\x6f\xcc\x88\x6e"; - - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - // put root folder - $this->assertFalse($this->cache->get('folder')); - $this->assertGreaterThan(0, $this->cache->put('folder', $data)); - - // put un-normalized folder - $this->assertFalse($this->cache->get('folder/' . $folderWith0308)); - $this->assertGreaterThan(0, $this->cache->put('folder/' . $folderWith0308, $data)); - - // get un-normalized folder by name - $unNormalizedFolderName = $this->cache->get('folder/' . $folderWith0308); - - // check if folder name was normalized - $this->assertEquals($folderWith00F6, $unNormalizedFolderName['name']); - - // put normalized folder - $this->assertInstanceOf('\OCP\Files\Cache\ICacheEntry', $this->cache->get('folder/' . $folderWith00F6)); - $this->assertGreaterThan(0, $this->cache->put('folder/' . $folderWith00F6, $data)); - - // at this point we should have only one folder named "Schön" - $this->assertEquals(1, count($this->cache->getFolderContents('folder'))); - } - - public static function bogusPathNamesProvider(): array { - return [ - ['/bogus.txt', 'bogus.txt'], - ['//bogus.txt', 'bogus.txt'], - ['bogus/', 'bogus'], - ['bogus//', 'bogus'], - ]; - } - - /** - * Test bogus paths with leading or doubled slashes - */ - #[\PHPUnit\Framework\Attributes\DataProvider('bogusPathNamesProvider')] - public function testBogusPaths($bogusPath, $fixedBogusPath): void { - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $parentId = $this->cache->getId(''); - - $this->assertGreaterThan(0, $this->cache->put($bogusPath, $data)); - - $newData = $this->cache->get($fixedBogusPath); - $this->assertNotFalse($newData); - - $this->assertEquals($fixedBogusPath, $newData['path']); - // parent is the correct one, resolved properly (they used to not be) - $this->assertEquals($parentId, $newData['parent']); - - $newDataFromBogus = $this->cache->get($bogusPath); - // same entry - $this->assertEquals($newData, $newDataFromBogus); - } - - public function testNoReuseOfFileId(): void { - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain']; - $this->cache->put('somefile.txt', $data1); - $info = $this->cache->get('somefile.txt'); - $fileId = $info['fileid']; - $this->cache->remove('somefile.txt'); - $data2 = ['size' => 200, 'mtime' => 100, 'mimetype' => 'text/plain']; - $this->cache->put('anotherfile.txt', $data2); - $info2 = $this->cache->get('anotherfile.txt'); - $fileId2 = $info2['fileid']; - $this->assertNotEquals($fileId, $fileId2); - } - - public static function escapingProvider(): array { - return [ - ['foo'], - ['o%'], - ['oth_r'], - ]; - } - - /** - * @param string $name - */ - #[\PHPUnit\Framework\Attributes\DataProvider('escapingProvider')] - public function testEscaping($name): void { - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain']; - $this->cache->put($name, $data); - $this->assertTrue($this->cache->inCache($name)); - $retrievedData = $this->cache->get($name); - foreach ($data as $key => $value) { - $this->assertEquals($value, $retrievedData[$key]); - } - $this->cache->move($name, $name . 'asd'); - $this->assertFalse($this->cache->inCache($name)); - $this->assertTrue($this->cache->inCache($name . 'asd')); - $this->cache->remove($name . 'asd'); - $this->assertFalse($this->cache->inCache($name . 'asd')); - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $this->cache->put($name, $folderData); - $this->cache->put('other', $folderData); - $childs = ['asd', 'bar', 'foo', 'sub/folder']; - $this->cache->put($name . '/sub', $folderData); - $this->cache->put('other/sub', $folderData); - foreach ($childs as $child) { - $this->cache->put($name . '/' . $child, $data); - $this->cache->put('other/' . $child, $data); - $this->assertTrue($this->cache->inCache($name . '/' . $child)); - } - $this->cache->move($name, $name . 'asd'); - foreach ($childs as $child) { - $this->assertTrue($this->cache->inCache($name . 'asd/' . $child)); - $this->assertTrue($this->cache->inCache('other/' . $child)); - } - foreach ($childs as $child) { - $this->cache->remove($name . 'asd/' . $child); - $this->assertFalse($this->cache->inCache($name . 'asd/' . $child)); - $this->assertTrue($this->cache->inCache('other/' . $child)); - } - } - - public function testExtended(): void { - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'creation_time' => 20]; - $id1 = $this->cache->put('foo1', $data); - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'upload_time' => 30]; - $this->cache->put('foo2', $data); - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'metadata_etag' => 'foo']; - $this->cache->put('foo3', $data); - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain']; - $id4 = $this->cache->put('foo4', $data); - - $entry = $this->cache->get($id1); - $this->assertEquals(20, $entry->getCreationTime()); - $this->assertEquals(0, $entry->getUploadTime()); - $this->assertEquals(null, $entry->getMetadataEtag()); - - $entries = $this->cache->getFolderContents(''); - $this->assertCount(4, $entries); - - $entriesByName = []; - foreach ($entries as $entry) { - $entriesByName[$entry->getName()] = $entry; - } - - $this->assertArrayHasKey('foo1', $entriesByName); - $this->assertArrayHasKey('foo2', $entriesByName); - $this->assertArrayHasKey('foo3', $entriesByName); - $this->assertArrayHasKey('foo4', $entriesByName); - - $this->assertEquals(20, $entriesByName['foo1']->getCreationTime()); - $this->assertEquals(0, $entriesByName['foo1']->getUploadTime()); - $this->assertEquals(null, $entriesByName['foo1']->getMetadataEtag()); - - $this->assertEquals(0, $entriesByName['foo2']->getCreationTime()); - $this->assertEquals(30, $entriesByName['foo2']->getUploadTime()); - $this->assertEquals(null, $entriesByName['foo2']->getMetadataEtag()); - - $this->assertEquals(0, $entriesByName['foo3']->getCreationTime()); - $this->assertEquals(0, $entriesByName['foo3']->getUploadTime()); - $this->assertEquals('foo', $entriesByName['foo3']->getMetadataEtag()); - - $this->assertEquals(0, $entriesByName['foo4']->getCreationTime()); - $this->assertEquals(0, $entriesByName['foo4']->getUploadTime()); - $this->assertEquals(null, $entriesByName['foo4']->getMetadataEtag()); - - $this->cache->update($id1, ['upload_time' => 25]); - - $entry = $this->cache->get($id1); - $this->assertEquals(20, $entry->getCreationTime()); - $this->assertEquals(25, $entry->getUploadTime()); - $this->assertEquals(null, $entry->getMetadataEtag()); - - $this->cache->put('sub', $folderData); - - $this->cache->move('foo1', 'sub/foo1'); - - $entries = $this->cache->getFolderContents('sub'); - $this->assertCount(1, $entries); - - $this->assertEquals('foo1', $entries[0]->getName()); - $this->assertEquals(20, $entries[0]->getCreationTime()); - $this->assertEquals(25, $entries[0]->getUploadTime()); - $this->assertEquals(null, $entries[0]->getMetadataEtag()); - - $this->cache->update($id4, ['upload_time' => 25]); - - $entry = $this->cache->get($id4); - $this->assertEquals(0, $entry->getCreationTime()); - $this->assertEquals(25, $entry->getUploadTime()); - $this->assertEquals(null, $entry->getMetadataEtag()); - - $this->cache->remove('sub'); - } -} --- a/tests/lib/Files/Cache/FileAccessTest.php +++ /dev/null @@ -1,453 +0,0 @@ -dbConnection = Server::get(IDBConnection::class); - - // Ensure FileAccess is instantiated with the real connection - $this->fileAccess = new FileAccess( - $this->dbConnection, - Server::get(SystemConfig::class), - Server::get(LoggerInterface::class), - Server::get(FilesMetadataManager::class), - Server::get(IMimeTypeLoader::class) - ); - - // Clear and prepare `filecache` table for tests - $queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards(); - $queryBuilder->delete('filecache')->executeStatement(); - - // Clean up potential leftovers from other tests - $queryBuilder = $this->dbConnection->getQueryBuilder(); - $queryBuilder->delete('mounts')->executeStatement(); - - $this->setUpTestDatabaseForGetDistinctMounts(); - $this->setUpTestDatabaseForGetByAncestorInStorage(); - } - - private function setUpTestDatabaseForGetDistinctMounts(): void { - $queryBuilder = $this->dbConnection->getQueryBuilder(); - - // Insert test data - $queryBuilder->insert('mounts') - ->values([ - 'storage_id' => $queryBuilder->createNamedParameter(1, IQueryBuilder::PARAM_INT), - 'root_id' => $queryBuilder->createNamedParameter(10, IQueryBuilder::PARAM_INT), - 'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'), - 'mount_point' => $queryBuilder->createNamedParameter('/files'), - 'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/files')), - 'user_id' => $queryBuilder->createNamedParameter('test'), - ]) - ->executeStatement(); - - $queryBuilder->insert('mounts') - ->values([ - 'storage_id' => $queryBuilder->createNamedParameter(3, IQueryBuilder::PARAM_INT), - 'root_id' => $queryBuilder->createNamedParameter(30, IQueryBuilder::PARAM_INT), - 'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'), - 'mount_point' => $queryBuilder->createNamedParameter('/documents'), - 'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/documents')), - 'user_id' => $queryBuilder->createNamedParameter('test'), - ]) - ->executeStatement(); - - $queryBuilder->insert('mounts') - ->values([ - 'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT), - 'root_id' => $queryBuilder->createNamedParameter(31, IQueryBuilder::PARAM_INT), - 'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass2'), - 'mount_point' => $queryBuilder->createNamedParameter('/foobar'), - 'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/foobar')), - 'user_id' => $queryBuilder->createNamedParameter('test'), - ]) - ->executeStatement(); - } - - /** - * Test that getDistinctMounts returns all mounts without filters - */ - public function testGetDistinctMountsWithoutFilters(): void { - $result = iterator_to_array($this->fileAccess->getDistinctMounts([], false)); - - $this->assertCount(3, $result); - - $this->assertEquals([ - 'storage_id' => 1, - 'root_id' => 10, - 'overridden_root' => 10, - ], $result[0]); - - $this->assertEquals([ - 'storage_id' => 3, - 'root_id' => 30, - 'overridden_root' => 30, - ], $result[1]); - - $this->assertEquals([ - 'storage_id' => 4, - 'root_id' => 31, - 'overridden_root' => 31, - ], $result[2]); - } - - /** - * Test that getDistinctMounts applies filtering by mount providers - */ - public function testGetDistinctMountsWithMountProviderFilter(): void { - $result = iterator_to_array($this->fileAccess->getDistinctMounts(['TestProviderClass1'], false)); - - $this->assertCount(2, $result); - - $this->assertEquals([ - 'storage_id' => 1, - 'root_id' => 10, - 'overridden_root' => 10, - ], $result[0]); - - $this->assertEquals([ - 'storage_id' => 3, - 'root_id' => 30, - 'overridden_root' => 30, - ], $result[1]); - } - - /** - * Test that getDistinctMounts rewrites home directory paths - */ - public function testGetDistinctMountsWithRewriteHomeDirectories(): void { - // Add additional test data for a home directory mount - $queryBuilder = $this->dbConnection->getQueryBuilder(); - $queryBuilder->insert('mounts') - ->values([ - 'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT), - 'root_id' => $queryBuilder->createNamedParameter(40, IQueryBuilder::PARAM_INT), - 'mount_provider_class' => $queryBuilder->createNamedParameter(LocalHomeMountProvider::class), - 'mount_point' => $queryBuilder->createNamedParameter('/home/user'), - 'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/home/user')), - 'user_id' => $queryBuilder->createNamedParameter('test'), - ]) - ->executeStatement(); - - // Add a mount that is mounted in the home directory - $queryBuilder = $this->dbConnection->getQueryBuilder(); - $queryBuilder->insert('mounts') - ->values([ - 'storage_id' => $queryBuilder->createNamedParameter(5, IQueryBuilder::PARAM_INT), - 'root_id' => $queryBuilder->createNamedParameter(41, IQueryBuilder::PARAM_INT), - 'mount_provider_class' => $queryBuilder->createNamedParameter('TestMountProvider3'), - 'mount_point' => $queryBuilder->createNamedParameter('/test/files/foobar'), - 'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/test/files/foobar')), - 'user_id' => $queryBuilder->createNamedParameter('test'), - ]) - ->executeStatement(); - - // Simulate adding a "files" directory to the filecache table - $queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards(); - $queryBuilder->delete('filecache')->executeStatement(); - $queryBuilder = $this->dbConnection->getQueryBuilder(); - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => $queryBuilder->createNamedParameter(99, IQueryBuilder::PARAM_INT), - 'storage' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT), - 'parent' => $queryBuilder->createNamedParameter(40), - 'name' => $queryBuilder->createNamedParameter('files'), - 'path' => $queryBuilder->createNamedParameter('files'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files')), - ]) - ->executeStatement(); - - $result = iterator_to_array($this->fileAccess->getDistinctMounts()); - - $this->assertCount(2, $result); - - $this->assertEquals([ - 'storage_id' => 4, - 'root_id' => 40, - 'overridden_root' => 99, - ], $result[0]); - - $this->assertEquals([ - 'storage_id' => 5, - 'root_id' => 41, - 'overridden_root' => 41, - ], $result[1]); - } - - private function setUpTestDatabaseForGetByAncestorInStorage(): void { - // prepare `filecache` table for tests - $queryBuilder = $this->dbConnection->getQueryBuilder(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 1, - 'parent' => 0, - 'path' => $queryBuilder->createNamedParameter('files'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files')), - 'storage' => $queryBuilder->createNamedParameter(1), - 'name' => $queryBuilder->createNamedParameter('files'), - 'mimetype' => 1, - 'encrypted' => 0, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 2, - 'parent' => 1, - 'path' => $queryBuilder->createNamedParameter('files/documents'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/documents')), - 'storage' => $queryBuilder->createNamedParameter(1), - 'name' => $queryBuilder->createNamedParameter('documents'), - 'mimetype' => 2, - 'encrypted' => 1, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 3, - 'parent' => 1, - 'path' => $queryBuilder->createNamedParameter('files/photos'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos')), - 'storage' => $queryBuilder->createNamedParameter(1), - 'name' => $queryBuilder->createNamedParameter('photos'), - 'mimetype' => 3, - 'encrypted' => 1, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 4, - 'parent' => 3, - 'path' => $queryBuilder->createNamedParameter('files/photos/endtoendencrypted'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos/endtoendencrypted')), - 'storage' => $queryBuilder->createNamedParameter(1), - 'name' => $queryBuilder->createNamedParameter('endtoendencrypted'), - 'mimetype' => 4, - 'encrypted' => 0, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 5, - 'parent' => 1, - 'path' => $queryBuilder->createNamedParameter('files/serversideencrypted'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/serversideencrypted')), - 'storage' => $queryBuilder->createNamedParameter(1), - 'name' => $queryBuilder->createNamedParameter('serversideencrypted'), - 'mimetype' => 4, - 'encrypted' => 1, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 6, - 'parent' => 0, - 'path' => $queryBuilder->createNamedParameter('files/storage2'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2')), - 'storage' => $queryBuilder->createNamedParameter(2), - 'name' => $queryBuilder->createNamedParameter('storage2'), - 'mimetype' => 5, - 'encrypted' => 0, - 'size' => 1, - ]) - ->executeStatement(); - - $queryBuilder->insert('filecache') - ->values([ - 'fileid' => 7, - 'parent' => 6, - 'path' => $queryBuilder->createNamedParameter('files/storage2/file'), - 'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2/file')), - 'storage' => $queryBuilder->createNamedParameter(2), - 'name' => $queryBuilder->createNamedParameter('file'), - 'mimetype' => 6, - 'encrypted' => 0, - 'size' => 1, - ]) - ->executeStatement(); - } - - /** - * Test fetching files by ancestor in storage. - */ - public function testGetByAncestorInStorage(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, // storageId - 1, // rootId - 0, // lastFileId - 10, // maxResults - [], // mimeTypes - true, // include end-to-end encrypted files - true, // include server-side encrypted files - ); - - $result = iterator_to_array($generator); - - $this->assertCount(4, $result); - - $paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result); - $this->assertEquals([ - 'files/documents', - 'files/photos', - 'files/photos/endtoendencrypted', - 'files/serversideencrypted', - ], $paths); - } - - /** - * Test filtering by mime types. - */ - public function testGetByAncestorInStorageWithMimeTypes(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, - 1, - 0, - 10, - [2], // Only include documents (mimetype=2) - true, - true, - ); - - $result = iterator_to_array($generator); - - $this->assertCount(1, $result); - $this->assertEquals('files/documents', $result[0]->getPath()); - } - - /** - * Test excluding end-to-end encrypted files. - */ - public function testGetByAncestorInStorageWithoutEndToEndEncrypted(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, - 1, - 0, - 10, - [], - false, // exclude end-to-end encrypted files - true, - ); - - $result = iterator_to_array($generator); - - $this->assertCount(3, $result); - $paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result); - $this->assertEquals(['files/documents', 'files/photos', 'files/serversideencrypted'], $paths); - } - - /** - * Test excluding server-side encrypted files. - */ - public function testGetByAncestorInStorageWithoutServerSideEncrypted(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, - 1, - 0, - 10, - [], - true, - false, // exclude server-side encrypted files - ); - - $result = iterator_to_array($generator); - - $this->assertCount(1, $result); - $this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath()); - } - - /** - * Test max result limits. - */ - public function testGetByAncestorInStorageWithMaxResults(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, - 1, - 0, - 1, // Limit to 1 result - [], - true, - true, - ); - - $result = iterator_to_array($generator); - - $this->assertCount(1, $result); - $this->assertEquals('files/documents', $result[0]->getPath()); - } - - /** - * Test rootId filter - */ - public function testGetByAncestorInStorageWithRootIdFilter(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 1, - 3, // Filter by rootId - 0, - 10, - [], - true, - true, - ); - - $result = iterator_to_array($generator); - - $this->assertCount(1, $result); - $this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath()); - } - - /** - * Test rootId filter - */ - public function testGetByAncestorInStorageWithStorageFilter(): void { - $generator = $this->fileAccess->getByAncestorInStorage( - 2, // Filter by storage - 6, // and by rootId - 0, - 10, - [], - true, - true, - ); - - $result = iterator_to_array($generator); - - $this->assertCount(1, $result); - $this->assertEquals('files/storage2/file', $result[0]->getPath()); - } -} --- a/tests/lib/Files/Cache/HomeCacheTest.php +++ /dev/null @@ -1,117 +0,0 @@ -home; - } - - /** - * @return string - */ - #[\Override] - public function getUID(): string { - return $this->uid; - } -} - -#[Group('DB')] -class HomeCacheTest extends TestCase { - private Home $storage; - private ICache $cache; - private User $user; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->user = new DummyUser('foo', Server::get(ITempManager::class)->getTemporaryFolder()); - $this->storage = new Home(['user' => $this->user]); - $this->cache = $this->storage->getCache(); - } - - /** - * Tests that the root and files folder size calculation ignores the subdirs - * that have an unknown size. This makes sure that quota calculation still - * works as it's based on the "files" folder size. - */ - public function testRootFolderSizeIgnoresUnknownUpdate(): void { - $dir1 = 'files/knownsize'; - $dir2 = 'files/unknownsize'; - $fileData = []; - $fileData[''] = ['size' => -1, 'mtime' => 20, 'mimetype' => 'httpd/unix-directory']; - $fileData['files'] = ['size' => -1, 'mtime' => 20, 'mimetype' => 'httpd/unix-directory']; - $fileData[$dir1] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'httpd/unix-directory']; - $fileData[$dir2] = ['size' => -1, 'mtime' => 25, 'mimetype' => 'httpd/unix-directory']; - - $this->cache->put('', $fileData['']); - $this->cache->put('files', $fileData['files']); - $this->cache->put($dir1, $fileData[$dir1]); - $this->cache->put($dir2, $fileData[$dir2]); - - $this->assertTrue($this->cache->inCache('files')); - $this->assertTrue($this->cache->inCache($dir1)); - $this->assertTrue($this->cache->inCache($dir2)); - - // check that files and root size ignored the unknown sizes - $this->assertEquals(1000, $this->cache->calculateFolderSize('files')); - - // clean up - $this->cache->remove(''); - $this->cache->remove('files'); - $this->cache->remove($dir1); - $this->cache->remove($dir2); - - $this->assertFalse($this->cache->inCache('files')); - $this->assertFalse($this->cache->inCache($dir1)); - $this->assertFalse($this->cache->inCache($dir2)); - } - - public function testRootFolderSizeIsFilesSize(): void { - $dir1 = 'files'; - $afile = 'test.txt'; - $fileData = []; - $fileData[''] = ['size' => 1500, 'mtime' => 20, 'mimetype' => 'httpd/unix-directory']; - $fileData[$dir1] = ['size' => 1000, 'mtime' => 20, 'mimetype' => 'httpd/unix-directory']; - $fileData[$afile] = ['size' => 500, 'mtime' => 20]; - - $this->cache->put('', $fileData['']); - $this->cache->put($dir1, $fileData[$dir1]); - - $this->assertTrue($this->cache->inCache($dir1)); - - // check that root size ignored the unknown sizes - $data = $this->cache->get('files'); - $this->assertEquals(1000, $data['size']); - $data = $this->cache->get(''); - $this->assertEquals(1000, $data['size']); - - // clean up - $this->cache->remove(''); - $this->cache->remove($dir1); - - $this->assertFalse($this->cache->inCache($dir1)); - } -} --- a/tests/lib/Files/Cache/LocalRootScannerTest.php +++ /dev/null @@ -1,45 +0,0 @@ -getTemporaryFolder(); - $this->storage = new LocalRootStorage(['datadir' => $folder]); - } - - public function testDontScanUsers(): void { - $this->storage->mkdir('foo'); - $this->storage->mkdir('foo/bar'); - - $this->storage->getScanner()->scan(''); - $this->assertFalse($this->storage->getCache()->inCache('foo')); - } - - public function testDoScanAppData(): void { - $this->storage->mkdir('appdata_foo'); - $this->storage->mkdir('appdata_foo/bar'); - - $this->storage->getScanner()->scan(''); - $this->assertTrue($this->storage->getCache()->inCache('appdata_foo')); - $this->assertTrue($this->storage->getCache()->inCache('appdata_foo/bar')); - } -} --- a/tests/lib/Files/Cache/MoveFromCacheTraitTest.php +++ /dev/null @@ -1,39 +0,0 @@ -storage = new Temporary([]); - $this->storage2 = new Temporary([]); - $this->cache = new FallBackCrossCacheMoveCache($this->storage); - $this->cache2 = new FallBackCrossCacheMoveCache($this->storage2); - - $this->cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $this->cache2->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - } -} --- a/tests/lib/Files/Cache/PropagatorTest.php +++ /dev/null @@ -1,134 +0,0 @@ -storage = new Temporary(); - $this->storage->mkdir('foo/bar'); - $this->storage->file_put_contents('foo/bar/file.txt', 'bar'); - $this->storage->getScanner()->scan(''); - } - - /** - * @param $paths - * @return ICacheEntry[] - */ - private function getFileInfos($paths) { - $values = array_map(function ($path) { - return $this->storage->getCache()->get($path); - }, $paths); - return array_combine($paths, $values); - } - - public function testEtagPropagation(): void { - $paths = ['', 'foo', 'foo/bar']; - $oldInfos = $this->getFileInfos($paths); - $this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time()); - $newInfos = $this->getFileInfos($paths); - - foreach ($oldInfos as $i => $oldInfo) { - $this->assertNotEquals($oldInfo->getEtag(), $newInfos[$i]->getEtag()); - } - } - - public function testTimePropagation(): void { - $paths = ['', 'foo', 'foo/bar']; - $oldTime = time() - 200; - $targetTime = time() - 100; - $now = time(); - $cache = $this->storage->getCache(); - $cache->put('', ['mtime' => $now]); - $cache->put('foo', ['mtime' => $now]); - $cache->put('foo/bar', ['mtime' => $oldTime]); - $cache->put('foo/bar/file.txt', ['mtime' => $oldTime]); - $this->storage->getPropagator()->propagateChange('foo/bar/file.txt', $targetTime); - $newInfos = $this->getFileInfos($paths); - - $this->assertEquals($targetTime, $newInfos['foo/bar']->getMTime()); - - // dont lower mtimes - $this->assertEquals($now, $newInfos['foo']->getMTime()); - $this->assertEquals($now, $newInfos['']->getMTime()); - } - - public function testSizePropagation(): void { - $paths = ['', 'foo', 'foo/bar']; - $oldInfos = $this->getFileInfos($paths); - $this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time(), 10); - $newInfos = $this->getFileInfos($paths); - - foreach ($oldInfos as $i => $oldInfo) { - $this->assertEquals($oldInfo->getSize() + 10, $newInfos[$i]->getSize()); - } - } - - public function testSizePropagationNoNegative(): void { - $paths = ['', 'foo', 'foo/bar']; - $oldInfos = $this->getFileInfos($paths); - $this->storage->getPropagator()->propagateChange('foo/bar/file.txt', time(), -100); - $newInfos = $this->getFileInfos($paths); - - foreach ($oldInfos as $i => $oldInfo) { - $this->assertEquals(-1, $newInfos[$i]->getSize()); - } - } - - public function testBatchedPropagation(): void { - $this->storage->mkdir('foo/baz'); - $this->storage->mkdir('asd'); - $this->storage->file_put_contents('asd/file.txt', 'bar'); - $this->storage->file_put_contents('foo/baz/file.txt', 'bar'); - $this->storage->getScanner()->scan(''); - - $paths = ['', 'foo', 'foo/bar', 'asd', 'foo/baz']; - - $oldInfos = $this->getFileInfos($paths); - $propagator = $this->storage->getPropagator(); - - $propagator->beginBatch(); - $propagator->propagateChange('asd/file.txt', time(), 10); - $propagator->propagateChange('foo/bar/file.txt', time(), 2); - - $newInfos = $this->getFileInfos($paths); - - // no changes until we finish the batch - foreach ($oldInfos as $i => $oldInfo) { - $this->assertEquals($oldInfo->getSize(), $newInfos[$i]->getSize()); - $this->assertEquals($oldInfo->getEtag(), $newInfos[$i]->getEtag()); - $this->assertEquals($oldInfo->getMTime(), $newInfos[$i]->getMTime()); - } - - $propagator->commitBatch(); - - $newInfos = $this->getFileInfos($paths); - - foreach ($oldInfos as $i => $oldInfo) { - if ($oldInfo->getPath() !== 'foo/baz') { - $this->assertNotEquals($oldInfo->getEtag(), $newInfos[$i]->getEtag(), "etag for {$oldInfo->getPath()} not updated"); - } - } - - $this->assertEquals($oldInfos['']->getSize() + 12, $newInfos['']->getSize()); - $this->assertEquals($oldInfos['asd']->getSize() + 10, $newInfos['asd']->getSize()); - $this->assertEquals($oldInfos['foo']->getSize() + 2, $newInfos['foo']->getSize()); - $this->assertEquals($oldInfos['foo/bar']->getSize() + 2, $newInfos['foo/bar']->getSize()); - $this->assertEquals($oldInfos['foo/baz']->getSize(), $newInfos['foo/baz']->getSize()); - } -} --- a/tests/lib/Files/Cache/ScannerTest.php +++ /dev/null @@ -1,455 +0,0 @@ -storage = new Temporary([]); - $this->scanner = new Scanner($this->storage); - $this->cache = new Cache($this->storage); - } - - #[\Override] - protected function tearDown(): void { - $this->cache->clear(); - - parent::tearDown(); - } - - public function testFile(): void { - $data = "dummy file data\n"; - $this->storage->file_put_contents('foo.txt', $data); - $this->scanner->scanFile('foo.txt'); - - $this->assertEquals($this->cache->inCache('foo.txt'), true); - $cachedData = $this->cache->get('foo.txt'); - $this->assertEquals($cachedData['size'], strlen($data)); - $this->assertEquals($cachedData['mimetype'], 'text/plain'); - $this->assertNotEquals($cachedData['parent'], -1); //parent folders should be scanned automatically - - $data = file_get_contents(OC::$SERVERROOT . '/core/img/logo/logo.png'); - $this->storage->file_put_contents('foo.png', $data); - $this->scanner->scanFile('foo.png'); - - $this->assertEquals($this->cache->inCache('foo.png'), true); - $cachedData = $this->cache->get('foo.png'); - $this->assertEquals($cachedData['size'], strlen($data)); - $this->assertEquals($cachedData['mimetype'], 'image/png'); - } - - public function testFile4Byte(): void { - $data = "dummy file data\n"; - $this->storage->file_put_contents('foo🙈.txt', $data); - - if (Server::get(IDBConnection::class)->supports4ByteText()) { - $this->assertNotNull($this->scanner->scanFile('foo🙈.txt')); - $this->assertTrue($this->cache->inCache('foo🙈.txt'), true); - - $cachedData = $this->cache->get('foo🙈.txt'); - $this->assertEquals(strlen($data), $cachedData['size']); - $this->assertEquals('text/plain', $cachedData['mimetype']); - $this->assertNotEquals(-1, $cachedData['parent']); //parent folders should be scanned automatically - } else { - $this->assertNull($this->scanner->scanFile('foo🙈.txt')); - $this->assertFalse($this->cache->inCache('foo🙈.txt'), true); - } - } - - public function testFileInvalidChars(): void { - $data = "dummy file data\n"; - $this->storage->file_put_contents("foo\nbar.txt", $data); - - $this->assertNull($this->scanner->scanFile("foo\nbar.txt")); - $this->assertFalse($this->cache->inCache("foo\nbar.txt"), true); - } - - private function fillTestFolders() { - $textData = "dummy file data\n"; - $imgData = file_get_contents(OC::$SERVERROOT . '/core/img/logo/logo.png'); - $this->storage->mkdir('folder'); - $this->storage->file_put_contents('foo.txt', $textData); - $this->storage->file_put_contents('foo.png', $imgData); - $this->storage->file_put_contents('folder/bar.txt', $textData); - } - - public function testFolder(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - $this->assertEquals($this->cache->inCache(''), true); - $this->assertEquals($this->cache->inCache('foo.txt'), true); - $this->assertEquals($this->cache->inCache('foo.png'), true); - $this->assertEquals($this->cache->inCache('folder'), true); - $this->assertEquals($this->cache->inCache('folder/bar.txt'), true); - - $cachedDataText = $this->cache->get('foo.txt'); - $cachedDataText2 = $this->cache->get('foo.txt'); - $cachedDataImage = $this->cache->get('foo.png'); - $cachedDataFolder = $this->cache->get(''); - $cachedDataFolder2 = $this->cache->get('folder'); - - $this->assertEquals($cachedDataImage['parent'], $cachedDataText['parent']); - $this->assertEquals($cachedDataFolder['fileid'], $cachedDataImage['parent']); - $this->assertEquals($cachedDataFolder['size'], $cachedDataImage['size'] + $cachedDataText['size'] + $cachedDataText2['size']); - $this->assertEquals($cachedDataFolder2['size'], $cachedDataText2['size']); - } - - public function testShallow(): void { - $this->fillTestFolders(); - - $this->scanner->scan('', IScanner::SCAN_SHALLOW); - $this->assertEquals($this->cache->inCache(''), true); - $this->assertEquals($this->cache->inCache('foo.txt'), true); - $this->assertEquals($this->cache->inCache('foo.png'), true); - $this->assertEquals($this->cache->inCache('folder'), true); - $this->assertEquals($this->cache->inCache('folder/bar.txt'), false); - - $cachedDataFolder = $this->cache->get(''); - $cachedDataFolder2 = $this->cache->get('folder'); - - $this->assertEquals(-1, $cachedDataFolder['size']); - $this->assertEquals(-1, $cachedDataFolder2['size']); - - $this->scanner->scan('folder', IScanner::SCAN_SHALLOW); - - $cachedDataFolder2 = $this->cache->get('folder'); - - $this->assertNotEquals($cachedDataFolder2['size'], -1); - - $this->cache->correctFolderSize('folder'); - - $cachedDataFolder = $this->cache->get(''); - $this->assertNotEquals($cachedDataFolder['size'], -1); - } - - public function testBackgroundScan(): void { - $this->fillTestFolders(); - $this->storage->mkdir('folder2'); - $this->storage->file_put_contents('folder2/bar.txt', 'foobar'); - - $this->scanner->scan('', IScanner::SCAN_SHALLOW); - $this->assertFalse($this->cache->inCache('folder/bar.txt')); - $this->assertFalse($this->cache->inCache('folder/2bar.txt')); - $cachedData = $this->cache->get(''); - $this->assertEquals(-1, $cachedData['size']); - - $this->scanner->backgroundScan(); - - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - - $cachedData = $this->cache->get(''); - $this->assertnotEquals(-1, $cachedData['size']); - - $this->assertFalse($this->cache->getIncomplete()); - } - - public function testBackgroundScanOnlyRecurseIncomplete(): void { - $this->fillTestFolders(); - $this->storage->mkdir('folder2'); - $this->storage->file_put_contents('folder2/bar.txt', 'foobar'); - - $this->scanner->scan('', IScanner::SCAN_SHALLOW); - $this->assertFalse($this->cache->inCache('folder/bar.txt')); - $this->assertFalse($this->cache->inCache('folder/2bar.txt')); - $this->assertFalse($this->cache->inCache('folder2/bar.txt')); - $this->cache->put('folder2', ['size' => 1]); // mark as complete - - $cachedData = $this->cache->get(''); - $this->assertEquals(-1, $cachedData['size']); - - $this->scanner->scan('', IScanner::SCAN_RECURSIVE_INCOMPLETE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE); - - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $this->assertFalse($this->cache->inCache('folder2/bar.txt')); - - $cachedData = $this->cache->get(''); - $this->assertNotEquals(-1, $cachedData['size']); - - $this->assertFalse($this->cache->getIncomplete()); - } - - public function testBackgroundScanNestedIncompleteFolders(): void { - $this->storage->mkdir('folder'); - $this->scanner->backgroundScan(); - - $this->storage->mkdir('folder/subfolder1'); - $this->storage->mkdir('folder/subfolder2'); - - $this->storage->mkdir('folder/subfolder1/subfolder3'); - $this->cache->put('folder', ['size' => -1]); - $this->cache->put('folder/subfolder1', ['size' => -1]); - - // do a scan to get the folders into the cache. - $this->scanner->backgroundScan(); - - $this->assertTrue($this->cache->inCache('folder/subfolder1/subfolder3')); - - $this->storage->file_put_contents('folder/subfolder1/bar1.txt', 'foobar'); - $this->storage->file_put_contents('folder/subfolder1/subfolder3/bar3.txt', 'foobar'); - $this->storage->file_put_contents('folder/subfolder2/bar2.txt', 'foobar'); - - //mark folders as incomplete. - $this->cache->put('folder/subfolder1', ['size' => -1]); - $this->cache->put('folder/subfolder2', ['size' => -1]); - $this->cache->put('folder/subfolder1/subfolder3', ['size' => -1]); - - $this->scanner->backgroundScan(); - - $this->assertTrue($this->cache->inCache('folder/subfolder1/bar1.txt')); - $this->assertTrue($this->cache->inCache('folder/subfolder2/bar2.txt')); - $this->assertTrue($this->cache->inCache('folder/subfolder1/subfolder3/bar3.txt')); - - //check if folder sizes are correct. - $this->assertEquals(18, $this->cache->get('folder')['size']); - $this->assertEquals(12, $this->cache->get('folder/subfolder1')['size']); - $this->assertEquals(6, $this->cache->get('folder/subfolder1/subfolder3')['size']); - $this->assertEquals(6, $this->cache->get('folder/subfolder2')['size']); - } - - public function testReuseExisting(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - $oldData = $this->cache->get(''); - $this->storage->unlink('folder/bar.txt'); - $this->cache->put('folder', ['mtime' => $this->storage->filemtime('folder'), 'storage_mtime' => $this->storage->filemtime('folder')]); - $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_SIZE); - $newData = $this->cache->get(''); - $this->assertIsString($oldData['etag']); - $this->assertIsString($newData['etag']); - $this->assertNotSame($oldData['etag'], $newData['etag']); - $this->assertEquals($oldData['size'], $newData['size']); - - $oldData = $newData; - $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG); - $newData = $this->cache->get(''); - $this->assertSame($oldData['etag'], $newData['etag']); - $this->assertEquals(-1, $newData['size']); - - $this->scanner->scan('', IScanner::SCAN_RECURSIVE); - $oldData = $this->cache->get(''); - $this->assertNotEquals(-1, $oldData['size']); - $this->scanner->scanFile('', IScanner::REUSE_ETAG + IScanner::REUSE_SIZE); - $newData = $this->cache->get(''); - $this->assertSame($oldData['etag'], $newData['etag']); - $this->assertEquals($oldData['size'], $newData['size']); - - $this->scanner->scan('', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG + IScanner::REUSE_SIZE); - $newData = $this->cache->get(''); - $this->assertSame($oldData['etag'], $newData['etag']); - $this->assertEquals($oldData['size'], $newData['size']); - - $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG + IScanner::REUSE_SIZE); - $newData = $this->cache->get(''); - $this->assertSame($oldData['etag'], $newData['etag']); - $this->assertEquals($oldData['size'], $newData['size']); - } - - public function testRemovedFile(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - $this->assertTrue($this->cache->inCache('foo.txt')); - $this->storage->unlink('foo.txt'); - $this->scanner->scan('', IScanner::SCAN_SHALLOW); - $this->assertFalse($this->cache->inCache('foo.txt')); - } - - public function testRemovedFolder(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $this->storage->rmdir('/folder'); - $this->scanner->scan('', IScanner::SCAN_SHALLOW); - $this->assertFalse($this->cache->inCache('folder')); - $this->assertFalse($this->cache->inCache('folder/bar.txt')); - } - - public function testScanRemovedFile(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $this->storage->unlink('folder/bar.txt'); - $this->scanner->scanFile('folder/bar.txt'); - $this->assertFalse($this->cache->inCache('folder/bar.txt')); - } - - public function testETagRecreation(): void { - $this->fillTestFolders(); - - $this->scanner->scan('folder/bar.txt'); - - // manipulate etag to simulate an empty etag - $this->scanner->scan('', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG); - /** @var CacheEntry $data0 */ - $data0 = $this->cache->get('folder/bar.txt'); - $this->assertIsString($data0['etag']); - $data1 = $this->cache->get('folder'); - $this->assertIsString($data1['etag']); - $data2 = $this->cache->get(''); - $this->assertIsString($data2['etag']); - $data0['etag'] = ''; - $this->cache->put('folder/bar.txt', $data0->getData()); - - // rescan - $this->scanner->scan('folder/bar.txt', IScanner::SCAN_SHALLOW, IScanner::REUSE_ETAG); - - // verify cache content - $newData0 = $this->cache->get('folder/bar.txt'); - $this->assertIsString($newData0['etag']); - $this->assertNotEmpty($newData0['etag']); - } - - public function testRepairParent(): void { - $this->fillTestFolders(); - $this->scanner->scan(''); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $oldFolderId = $this->cache->getId('folder'); - - // delete the folder without removing the children - $query = Server::get(IDBConnection::class)->getQueryBuilder(); - $query->delete('filecache') - ->where($query->expr()->eq('fileid', $query->createNamedParameter($oldFolderId))); - $query->executeStatement(); - - $cachedData = $this->cache->get('folder/bar.txt'); - $this->assertEquals($oldFolderId, $cachedData['parent']); - $this->assertFalse($this->cache->inCache('folder')); - - $this->scanner->scan(''); - - $this->assertTrue($this->cache->inCache('folder')); - $newFolderId = $this->cache->getId('folder'); - $this->assertNotEquals($oldFolderId, $newFolderId); - - $cachedData = $this->cache->get('folder/bar.txt'); - $this->assertEquals($newFolderId, $cachedData['parent']); - } - - public function testRepairParentShallow(): void { - $this->fillTestFolders(); - $this->scanner->scan(''); - $this->assertTrue($this->cache->inCache('folder/bar.txt')); - $oldFolderId = $this->cache->getId('folder'); - - // delete the folder without removing the children - $query = Server::get(IDBConnection::class)->getQueryBuilder(); - $query->delete('filecache') - ->where($query->expr()->eq('fileid', $query->createNamedParameter($oldFolderId))); - $query->executeStatement(); - - $cachedData = $this->cache->get('folder/bar.txt'); - $this->assertEquals($oldFolderId, $cachedData['parent']); - $this->assertFalse($this->cache->inCache('folder')); - - $this->scanner->scan('folder', IScanner::SCAN_SHALLOW); - - $this->assertTrue($this->cache->inCache('folder')); - $newFolderId = $this->cache->getId('folder'); - $this->assertNotEquals($oldFolderId, $newFolderId); - - $cachedData = $this->cache->get('folder/bar.txt'); - $this->assertEquals($newFolderId, $cachedData['parent']); - } - - /** - * - * @param string $path - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsPartialFile')] - public function testIsPartialFile($path, $expected): void { - $this->assertSame($expected, - $this->scanner->isPartialFile($path) - ); - } - - public static function dataTestIsPartialFile(): array { - return [ - ['foo.txt.part', true], - ['/sub/folder/foo.txt.part', true], - ['/sub/folder.part/foo.txt', true], - ['foo.txt', false], - ['/sub/folder/foo.txt', false], - ]; - } - - public function testNoETagUnscannedFolder(): void { - $this->fillTestFolders(); - - $this->scanner->scan(''); - - $oldFolderEntry = $this->cache->get('folder'); - // create a new file in a folder by keeping the mtime unchanged, but mark the folder as unscanned - $this->storage->file_put_contents('folder/new.txt', 'foo'); - $this->storage->touch('folder', $oldFolderEntry->getMTime()); - $this->cache->update($oldFolderEntry->getId(), ['size' => -1]); - - $this->scanner->scan(''); - - $this->cache->inCache('folder/new.txt'); - - $newFolderEntry = $this->cache->get('folder'); - $this->assertNotEquals($newFolderEntry->getEtag(), $oldFolderEntry->getEtag()); - } - - public function testNoETagUnscannedSubFolder(): void { - $this->fillTestFolders(); - $this->storage->mkdir('folder/sub'); - - $this->scanner->scan(''); - - $oldFolderEntry1 = $this->cache->get('folder'); - $oldFolderEntry2 = $this->cache->get('folder/sub'); - // create a new file in a folder by keeping the mtime unchanged, but mark the folder as unscanned - $this->storage->file_put_contents('folder/sub/new.txt', 'foo'); - $this->storage->touch('folder/sub', $oldFolderEntry1->getMTime()); - - // we only mark the direct parent as unscanned, which is the current "notify" behavior - $this->cache->update($oldFolderEntry2->getId(), ['size' => -1]); - - $this->scanner->scan(''); - - $this->cache->inCache('folder/new.txt'); - - $newFolderEntry1 = $this->cache->get('folder'); - $this->assertNotEquals($newFolderEntry1->getEtag(), $oldFolderEntry1->getEtag()); - $newFolderEntry2 = $this->cache->get('folder/sub'); - $this->assertNotEquals($newFolderEntry2->getEtag(), $oldFolderEntry2->getEtag()); - } -} --- a/tests/lib/Files/Cache/SearchBuilderTest.php +++ /dev/null @@ -1,212 +0,0 @@ -builder = Server::get(IDBConnection::class)->getQueryBuilder(); - $this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class); - $this->filesMetadataManager = $this->createMock(IFilesMetadataManager::class); - - $this->mimetypeLoader->expects($this->any()) - ->method('getId') - ->willReturnMap([ - ['text', 1], - ['text/plain', 2], - ['text/xml', 3], - ['image/jpg', 4], - ['image/png', 5], - ['image', 6], - ]); - - $this->mimetypeLoader->expects($this->any()) - ->method('getMimetypeById') - ->willReturnMap([ - [1, 'text'], - [2, 'text/plain'], - [3, 'text/xml'], - [4, 'image/jpg'], - [5, 'image/png'], - [6, 'image'] - ]); - - $this->searchBuilder = new SearchBuilder($this->mimetypeLoader, $this->filesMetadataManager); - $this->numericStorageId = 10000; - - $this->builder->select(['fileid']) - ->from('filecache', 'file') // alias needed for QuerySearchHelper#getOperatorFieldAndValue - ->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId))); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - - $builder = Server::get(IDBConnection::class)->getQueryBuilder(); - - $builder->delete('filecache') - ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT))); - - $builder->executeStatement(); - } - - private function addCacheEntry(array $data) { - $data['storage'] = $this->numericStorageId; - $data['etag'] = 'unimportant'; - $data['storage_mtime'] = $data['mtime']; - if (!isset($data['path'])) { - $data['path'] = 'random/' . $this->getUniqueID(); - } - $data['path_hash'] = md5($data['path']); - if (!isset($data['mtime'])) { - $data['mtime'] = 100; - } - if (!isset($data['size'])) { - $data['size'] = 100; - } - $data['name'] = basename($data['path']); - $data['parent'] = -1; - if (isset($data['mimetype'])) { - [$mimepart,] = explode('/', $data['mimetype']); - $data['mimepart'] = $this->mimetypeLoader->getId($mimepart); - $data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']); - } else { - $data['mimepart'] = 1; - $data['mimetype'] = 1; - } - - $builder = Server::get(IDBConnection::class)->getQueryBuilder(); - - $values = []; - foreach ($data as $key => $value) { - $values[$key] = $builder->createNamedParameter($value); - } - - $builder->insert('filecache') - ->values($values) - ->executeStatement(); - - return $builder->getLastInsertId(); - } - - private function search(ISearchOperator $operator) { - $dbOperator = $this->searchBuilder->searchOperatorToDBExpr($this->builder, $operator); - $this->builder->andWhere($dbOperator); - - $result = $this->builder->executeQuery(); - $rows = $result->fetchFirstColumn(); - $result->closeCursor(); - - return $rows; - } - - public static function comparisonProvider(): array { - return [ - [new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1]], - [new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [0]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [0, 1]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [0]], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [0, 1]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [0]], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [0, 1]], - [new SearchComparison(ISearchComparison::COMPARE_IN, 'mimetype', ['image/jpg', 'image/png']), [0, 1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125) - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'text/%') - ]), []], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), - ]), [0, 1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), - ]), [1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'), - ]), [1]], - ]; - } - - /** - * - * @param ISearchOperator $operator - * @param array $fileIds - */ - #[\PHPUnit\Framework\Attributes\DataProvider('comparisonProvider')] - public function testComparison(ISearchOperator $operator, array $fileIds): void { - $fileId = []; - $fileId[] = $this->addCacheEntry([ - 'path' => 'foobar', - 'mtime' => 100, - 'size' => 50, - 'mimetype' => 'image/jpg' - ]); - - $fileId[] = $this->addCacheEntry([ - 'path' => 'fooasd', - 'mtime' => 150, - 'size' => 50, - 'mimetype' => 'image/png' - ]); - - $fileIds = array_map(function ($i) use ($fileId) { - return $fileId[$i]; - }, $fileIds); - - $results = $this->search($operator); - - sort($fileIds); - sort($results); - - $this->assertEquals($fileIds, $results); - } -} --- a/tests/lib/Files/Cache/StorageTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getCache(); - $rootId = $cache->put('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $fileId = $cache->put('foo.txt', ['size' => 1, 'mtime' => 1, 'mimetype' => 'text/plain', 'upload_time' => 25]); - - $metadataManager = Server::get(IFilesMetadataManager::class); - $metadata = $metadataManager->getMetadata($fileId, true); - $metadata->setString('test-key', 'value', true); - $metadataManager->saveMetadata($metadata); - - $mountId = random_int(100000000, 999999999); - $mountPoint = '/' . $this->getUniqueID('user') . '/files/mount/'; - $qb = Server::get(IDBConnection::class)->getQueryBuilder(); - $qb->insert('mounts') - ->values([ - 'storage_id' => $qb->createNamedParameter($cache->getNumericStorageId(), IQueryBuilder::PARAM_INT), - 'root_id' => $qb->createNamedParameter($rootId, IQueryBuilder::PARAM_INT), - 'user_id' => $qb->createNamedParameter('test'), - 'mount_point' => $qb->createNamedParameter($mountPoint), - 'mount_point_hash' => $qb->createNamedParameter(hash('xxh128', $mountPoint)), - 'mount_id' => $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT), - ]) - ->executeStatement(); - - $this->assertSame(1, $this->countRows('filecache_extended', 'fileid', $fileId)); - $this->assertSame(1, $this->countRows('files_metadata', 'file_id', $fileId)); - $this->assertSame(1, $this->countRows('files_metadata_index', 'file_id', $fileId)); - - Storage::cleanByMountId($mountId); - - $this->assertSame(0, $this->countRows('filecache', 'fileid', $fileId)); - $this->assertSame(0, $this->countRows('filecache_extended', 'fileid', $fileId)); - $this->assertSame(0, $this->countRows('files_metadata', 'file_id', $fileId)); - $this->assertSame(0, $this->countRows('files_metadata_index', 'file_id', $fileId)); - } - - private function countRows(string $table, string $column, int $fileId): int { - $qb = Server::get(IDBConnection::class)->getQueryBuilder(); - $qb->select($qb->func()->count()) - ->from($table) - ->where($qb->expr()->eq($column, $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - return (int)$qb->executeQuery()->fetchOne(); - } -} --- a/tests/lib/Files/Cache/UpdaterLegacyTest.php +++ /dev/null @@ -1,321 +0,0 @@ -storage = new Temporary([]); - $textData = "dummy file data\n"; - $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $this->storage->mkdir('folder'); - $this->storage->file_put_contents('foo.txt', $textData); - $this->storage->file_put_contents('foo.png', $imgData); - $this->storage->file_put_contents('folder/bar.txt', $textData); - $this->storage->file_put_contents('folder/bar2.txt', $textData); - - $this->scanner = $this->storage->getScanner(); - $this->scanner->scan(''); - $this->cache = $this->storage->getCache(); - - if (!self::$user) { - self::$user = $this->getUniqueID(); - } - - Server::get(IUserManager::class)->createUser(self::$user, 'NotAnEasyPassword123456+'); - $this->loginAsUser(self::$user); - - Filesystem::init(self::$user, '/' . self::$user . '/files'); - - /** @var IMountManager $manager */ - $manager = Server::get(IMountManager::class); - $manager->removeMount('/' . self::$user); - - Filesystem::mount($this->storage, [], '/' . self::$user . '/files'); - - \OC_Hook::clear('OC_Filesystem'); - } - - #[\Override] - protected function tearDown(): void { - if ($this->cache) { - $this->cache->clear(); - } - - $result = false; - $user = Server::get(IUserManager::class)->get(self::$user); - if ($user !== null) { - $result = $user->delete(); - } - $this->assertTrue($result); - - $this->logout(); - parent::tearDown(); - } - - public function testWrite(): void { - $textSize = strlen("dummy file data\n"); - $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $this->cache->put('foo.txt', ['mtime' => 100, 'storage_mtime' => 150]); - $rootCachedData = $this->cache->get(''); - $this->assertEquals(3 * $textSize + $imageSize, $rootCachedData['size']); - - $fooCachedData = $this->cache->get('foo.txt'); - Filesystem::file_put_contents('foo.txt', 'asd'); - $cachedData = $this->cache->get('foo.txt'); - $this->assertEquals(3, $cachedData['size']); - $this->assertIsString($fooCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($fooCachedData['etag'], $cachedData['etag']); - $cachedData = $this->cache->get(''); - $this->assertEquals(2 * $textSize + $imageSize + 3, $cachedData['size']); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($rootCachedData['etag'], $cachedData['etag']); - $rootCachedData = $cachedData; - - $this->assertFalse($this->cache->inCache('bar.txt')); - Filesystem::file_put_contents('bar.txt', 'asd'); - $this->assertTrue($this->cache->inCache('bar.txt')); - $cachedData = $this->cache->get('bar.txt'); - $this->assertEquals(3, $cachedData['size']); - $mtime = $cachedData['mtime']; - $cachedData = $this->cache->get(''); - $this->assertEquals(2 * $textSize + $imageSize + 2 * 3, $cachedData['size']); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($rootCachedData['etag'], $cachedData['etag']); - $this->assertGreaterThanOrEqual($rootCachedData['mtime'], $mtime); - } - - public function testWriteWithMountPoints(): void { - $storage2 = new Temporary([]); - $storage2->getScanner()->scan(''); //initialize etags - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/' . self::$user . '/files/folder/substorage'); - $view = new View('/' . self::$user . '/files'); - $folderCachedData = $view->getFileInfo('folder'); - $substorageCachedData = $cache2->get(''); - Filesystem::file_put_contents('folder/substorage/foo.txt', 'asd'); - $this->assertTrue($cache2->inCache('foo.txt')); - $cachedData = $cache2->get('foo.txt'); - $oldEtag = $substorageCachedData['etag']; - $this->assertEquals(3, $cachedData['size']); - $mtime = $cachedData['mtime']; - - $cachedData = $cache2->get(''); - $this->assertIsString($substorageCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - - $cachedData = $view->getFileInfo('folder'); - $this->assertIsString($folderCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - } - - public function testDelete(): void { - $textSize = strlen("dummy file data\n"); - $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $rootCachedData = $this->cache->get(''); - $oldEtag = $rootCachedData['etag']; - $this->assertEquals(3 * $textSize + $imageSize, $rootCachedData['size']); - - $this->assertTrue($this->cache->inCache('foo.txt')); - Filesystem::unlink('foo.txt'); - $this->assertFalse($this->cache->inCache('foo.txt')); - $cachedData = $this->cache->get(''); - $this->assertEquals(2 * $textSize + $imageSize, $cachedData['size']); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - $this->assertGreaterThanOrEqual($rootCachedData['mtime'], $cachedData['mtime']); - $rootCachedData = $cachedData; - - Filesystem::mkdir('bar_folder'); - $this->assertTrue($this->cache->inCache('bar_folder')); - $cachedData = $this->cache->get(''); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - $rootCachedData = $cachedData; - $oldEtag = $rootCachedData['etag']; - Filesystem::rmdir('bar_folder'); - $this->assertFalse($this->cache->inCache('bar_folder')); - $cachedData = $this->cache->get(''); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - $this->assertGreaterThanOrEqual($rootCachedData['mtime'], $cachedData['mtime']); - } - - public function testDeleteWithMountPoints(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/' . self::$user . '/files/folder/substorage'); - Filesystem::file_put_contents('folder/substorage/foo.txt', 'asd'); - $view = new View('/' . self::$user . '/files'); - $this->assertTrue($cache2->inCache('foo.txt')); - $folderCachedData = $view->getFileInfo('folder'); - $substorageCachedData = $cache2->get(''); - $oldEtag = $folderCachedData['etag']; - Filesystem::unlink('folder/substorage/foo.txt'); - $this->assertFalse($cache2->inCache('foo.txt')); - - $cachedData = $cache2->get(''); - $this->assertIsString($substorageCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($substorageCachedData, $cachedData['etag']); - $this->assertGreaterThanOrEqual($substorageCachedData['mtime'], $cachedData['mtime']); - - $cachedData = $view->getFileInfo('folder'); - $this->assertIsString($folderCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - $this->assertGreaterThanOrEqual($folderCachedData['mtime'], $cachedData['mtime']); - } - - public function testRename(): void { - $textSize = strlen("dummy file data\n"); - $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $rootCachedData = $this->cache->get(''); - $this->assertEquals(3 * $textSize + $imageSize, $rootCachedData['size']); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $fooCachedData = $this->cache->get('foo.txt'); - $this->assertFalse($this->cache->inCache('bar.txt')); - Filesystem::rename('foo.txt', 'bar.txt'); - $this->assertFalse($this->cache->inCache('foo.txt')); - $this->assertTrue($this->cache->inCache('bar.txt')); - $cachedData = $this->cache->get('bar.txt'); - $this->assertEquals($fooCachedData['fileid'], $cachedData['fileid']); - $mtime = $cachedData['mtime']; - $cachedData = $this->cache->get(''); - $this->assertEquals(3 * $textSize + $imageSize, $cachedData['size']); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($rootCachedData['etag'], $cachedData['etag']); - } - - public function testRenameExtension(): void { - $fooCachedData = $this->cache->get('foo.txt'); - $this->assertEquals('text/plain', $fooCachedData['mimetype']); - Filesystem::rename('foo.txt', 'foo.abcd'); - $fooCachedData = $this->cache->get('foo.abcd'); - $this->assertEquals('application/octet-stream', $fooCachedData['mimetype']); - } - - public function testRenameWithMountPoints(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/' . self::$user . '/files/folder/substorage'); - Filesystem::file_put_contents('folder/substorage/foo.txt', 'asd'); - $view = new View('/' . self::$user . '/files'); - $this->assertTrue($cache2->inCache('foo.txt')); - $folderCachedData = $view->getFileInfo('folder'); - $oldEtag = $folderCachedData['etag']; - $substorageCachedData = $cache2->get(''); - $fooCachedData = $cache2->get('foo.txt'); - Filesystem::rename('folder/substorage/foo.txt', 'folder/substorage/bar.txt'); - $this->assertFalse($cache2->inCache('foo.txt')); - $this->assertTrue($cache2->inCache('bar.txt')); - $cachedData = $cache2->get('bar.txt'); - $this->assertEquals($fooCachedData['fileid'], $cachedData['fileid']); - $mtime = $cachedData['mtime']; - - $cachedData = $cache2->get(''); - $this->assertIsString($substorageCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - // rename can cause mtime change - invalid assert - // $this->assertEquals($mtime, $cachedData['mtime']); - - $cachedData = $view->getFileInfo('folder'); - $this->assertIsString($folderCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($oldEtag, $cachedData['etag']); - // rename can cause mtime change - invalid assert - // $this->assertEquals($mtime, $cachedData['mtime']); - } - - public function testTouch(): void { - $rootCachedData = $this->cache->get(''); - $fooCachedData = $this->cache->get('foo.txt'); - Filesystem::touch('foo.txt'); - $cachedData = $this->cache->get('foo.txt'); - $this->assertIsString($fooCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertGreaterThanOrEqual($fooCachedData['mtime'], $cachedData['mtime']); - - $cachedData = $this->cache->get(''); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($rootCachedData['etag'], $cachedData['etag']); - $this->assertGreaterThanOrEqual($rootCachedData['mtime'], $cachedData['mtime']); - $rootCachedData = $cachedData; - - $time = 1371006070; - $barCachedData = $this->cache->get('folder/bar.txt'); - $folderCachedData = $this->cache->get('folder'); - $this->cache->put('', ['mtime' => $time - 100]); - Filesystem::touch('folder/bar.txt', $time); - $cachedData = $this->cache->get('folder/bar.txt'); - $this->assertIsString($barCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($barCachedData['etag'], $cachedData['etag']); - $this->assertEquals($time, $cachedData['mtime']); - - $cachedData = $this->cache->get('folder'); - $this->assertIsString($folderCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($folderCachedData['etag'], $cachedData['etag']); - - $cachedData = $this->cache->get(''); - $this->assertIsString($rootCachedData['etag']); - $this->assertIsString($cachedData['etag']); - $this->assertNotSame($rootCachedData['etag'], $cachedData['etag']); - $this->assertEquals($time, $cachedData['mtime']); - } -} --- a/tests/lib/Files/Cache/UpdaterTest.php +++ /dev/null @@ -1,386 +0,0 @@ -loginAsUser(); - - $this->storage = new Temporary([]); - $this->updater = $this->storage->getUpdater(); - $this->cache = $this->storage->getCache(); - } - - #[\Override] - protected function tearDown(): void { - $this->logout(); - parent::tearDown(); - } - - public function testNewFile(): void { - $this->storage->file_put_contents('foo.txt', 'bar'); - $this->assertFalse($this->cache->inCache('foo.txt')); - - $this->updater->update('foo.txt'); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $cached = $this->cache->get('foo.txt'); - $this->assertEquals(3, $cached['size']); - $this->assertEquals('text/plain', $cached['mimetype']); - } - - public function testUpdatedFile(): void { - $this->storage->file_put_contents('foo.txt', 'bar'); - $this->updater->update('foo.txt'); - - $cached = $this->cache->get('foo.txt'); - $this->assertEquals(3, $cached['size']); - $this->assertEquals('text/plain', $cached['mimetype']); - - $this->storage->file_put_contents('foo.txt', 'qwerty'); - - $cached = $this->cache->get('foo.txt'); - $this->assertEquals(3, $cached['size']); - - $this->updater->update('/foo.txt'); - - $cached = $this->cache->get('foo.txt'); - $this->assertEquals(6, $cached['size']); - } - - public function testParentSize(): void { - $this->storage->getScanner()->scan(''); - - $parentCached = $this->cache->get(''); - $this->assertEquals(0, $parentCached['size']); - - $this->storage->file_put_contents('foo.txt', 'bar'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(0, $parentCached['size']); - - $this->updater->update('foo.txt'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(3, $parentCached['size']); - - $this->storage->file_put_contents('foo.txt', 'qwerty'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(3, $parentCached['size']); - - $this->updater->update('foo.txt'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(6, $parentCached['size']); - - $this->storage->unlink('foo.txt'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(6, $parentCached['size']); - - $this->updater->remove('foo.txt'); - - $parentCached = $this->cache->get(''); - $this->assertEquals(0, $parentCached['size']); - } - - public function testMove(): void { - $this->storage->file_put_contents('foo.txt', 'qwerty'); - $this->updater->update('foo.txt'); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $this->assertFalse($this->cache->inCache('bar.txt')); - $cached = $this->cache->get('foo.txt'); - - $this->storage->rename('foo.txt', 'bar.txt'); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $this->assertFalse($this->cache->inCache('bar.txt')); - - $this->updater->renameFromStorage($this->storage, 'foo.txt', 'bar.txt'); - - $this->assertFalse($this->cache->inCache('foo.txt')); - $this->assertTrue($this->cache->inCache('bar.txt')); - - $cachedTarget = $this->cache->get('bar.txt'); - $this->assertEquals($cached['etag'], $cachedTarget['etag']); - $this->assertEquals($cached['mtime'], $cachedTarget['mtime']); - $this->assertEquals($cached['size'], $cachedTarget['size']); - $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); - } - - public function testMoveNonExistingOverwrite(): void { - $this->storage->file_put_contents('bar.txt', 'qwerty'); - $this->updater->update('bar.txt'); - - $cached = $this->cache->get('bar.txt'); - - $this->updater->renameFromStorage($this->storage, 'foo.txt', 'bar.txt'); - - $this->assertFalse($this->cache->inCache('foo.txt')); - $this->assertTrue($this->cache->inCache('bar.txt')); - - $cachedTarget = $this->cache->get('bar.txt'); - $this->assertEquals($cached['etag'], $cachedTarget['etag']); - $this->assertEquals($cached['mtime'], $cachedTarget['mtime']); - $this->assertEquals($cached['size'], $cachedTarget['size']); - $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); - } - - public function testUpdateStorageMTime(): void { - $this->storage->mkdir('sub'); - $this->storage->mkdir('sub2'); - $this->storage->file_put_contents('sub/foo.txt', 'qwerty'); - - $this->updater->update('sub'); - $this->updater->update('sub/foo.txt'); - $this->updater->update('sub2'); - - $cachedSourceParent = $this->cache->get('sub'); - $cachedSource = $this->cache->get('sub/foo.txt'); - - $this->storage->rename('sub/foo.txt', 'sub2/bar.txt'); - - // simulate storage having a different mtime - $testmtime = 1433323578; - - // source storage mtime change - $this->storage->touch('sub', $testmtime); - - // target storage mtime change - $this->storage->touch('sub2', $testmtime); - // some storages (like Dropbox) change storage mtime on rename - $this->storage->touch('sub2/bar.txt', $testmtime); - - $this->updater->renameFromStorage($this->storage, 'sub/foo.txt', 'sub2/bar.txt'); - - $cachedTargetParent = $this->cache->get('sub2'); - $cachedTarget = $this->cache->get('sub2/bar.txt'); - - $this->assertEquals($cachedSource['mtime'], $cachedTarget['mtime'], 'file mtime preserved'); - - $this->assertNotEquals($cachedTarget['storage_mtime'], $cachedTarget['mtime'], 'mtime is not storage_mtime for moved file'); - - $this->assertEquals($testmtime, $cachedTarget['storage_mtime'], 'target file storage_mtime propagated'); - $this->assertNotEquals($testmtime, $cachedTarget['mtime'], 'target file mtime changed, not from storage'); - - $this->assertEquals($testmtime, $cachedTargetParent['storage_mtime'], 'target parent storage_mtime propagated'); - $this->assertNotEquals($testmtime, $cachedTargetParent['mtime'], 'target folder mtime changed, not from storage'); - } - - public function testNewFileDisabled(): void { - $this->storage->file_put_contents('foo.txt', 'bar'); - $this->assertFalse($this->cache->inCache('foo.txt')); - - $this->updater->disable(); - $this->updater->update('/foo.txt'); - - $this->assertFalse($this->cache->inCache('foo.txt')); - } - - public function testMoveCrossStorage(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/bar'); - $this->storage->file_put_contents('foo.txt', 'qwerty'); - - $this->updater->update('foo.txt'); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $this->assertFalse($cache2->inCache('bar.txt')); - $cached = $this->cache->get('foo.txt'); - - // "rename" - $storage2->file_put_contents('bar.txt', 'qwerty'); - $this->storage->unlink('foo.txt'); - - $this->assertTrue($this->cache->inCache('foo.txt')); - $this->assertFalse($cache2->inCache('bar.txt')); - - $storage2->getUpdater()->renameFromStorage($this->storage, 'foo.txt', 'bar.txt'); - - $this->assertFalse($this->cache->inCache('foo.txt')); - $this->assertTrue($cache2->inCache('bar.txt')); - - $cachedTarget = $cache2->get('bar.txt'); - $this->assertEquals($cached['mtime'], $cachedTarget['mtime']); - $this->assertEquals($cached['size'], $cachedTarget['size']); - $this->assertEquals($cached['etag'], $cachedTarget['etag']); - $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); - } - - public function testMoveCrossStorageMissingTargetParent(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/bar'); - $this->storage->file_put_contents('foo.txt', 'qwerty'); - $this->updater->update('foo.txt'); - $cached = $this->cache->get('foo.txt'); - - // the target parent exists on disk but is missing from the cache - $storage2->mkdir('sub'); - $this->assertFalse($cache2->inCache('sub')); - - $storage2->moveFromStorage($this->storage, 'foo.txt', 'sub/bar.txt'); - $storage2->getUpdater()->renameFromStorage($this->storage, 'foo.txt', 'sub/bar.txt'); - - $this->assertFalse($this->cache->inCache('foo.txt')); - $this->assertTrue($cache2->inCache('sub')); - $this->assertTrue($cache2->inCache('sub/bar.txt')); - - // the moved entry is attached to the scanned parent instead of being orphaned - $cachedTarget = $cache2->get('sub/bar.txt'); - $this->assertEquals($cache2->getId('sub'), $cachedTarget['parent']); - $this->assertEquals($cached['fileid'], $cachedTarget['fileid']); - } - - public function testMoveCrossStorageSourceNotInCache(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/bar'); - $this->storage->file_put_contents('foo.txt', 'qwerty'); - // the source was never scanned into the cache - $this->assertFalse($this->cache->inCache('foo.txt')); - - $storage2->moveFromStorage($this->storage, 'foo.txt', 'bar.txt'); - $storage2->getUpdater()->renameFromStorage($this->storage, 'foo.txt', 'bar.txt'); - - // the target still gets a cache entry - $this->assertTrue($cache2->inCache('bar.txt')); - $cachedTarget = $cache2->get('bar.txt'); - $this->assertEquals(6, $cachedTarget['size']); - } - - public function testMoveFolderCrossStorage(): void { - $storage2 = new Temporary([]); - $cache2 = $storage2->getCache(); - Filesystem::mount($storage2, [], '/bar'); - $this->storage->mkdir('foo'); - $this->storage->mkdir('foo/bar'); - $this->storage->file_put_contents('foo/foo.txt', 'qwerty'); - $this->storage->file_put_contents('foo/bar.txt', 'foo'); - $this->storage->file_put_contents('foo/bar/bar.txt', 'qwertyuiop'); - - $this->storage->getScanner()->scan(''); - - $this->assertTrue($this->cache->inCache('foo')); - $this->assertTrue($this->cache->inCache('foo/foo.txt')); - $this->assertTrue($this->cache->inCache('foo/bar.txt')); - $this->assertTrue($this->cache->inCache('foo/bar')); - $this->assertTrue($this->cache->inCache('foo/bar/bar.txt')); - $cached = []; - $cached[] = $this->cache->get('foo'); - $cached[] = $this->cache->get('foo/foo.txt'); - $cached[] = $this->cache->get('foo/bar.txt'); - $cached[] = $this->cache->get('foo/bar'); - $cached[] = $this->cache->get('foo/bar/bar.txt'); - - // add extension to trigger the possible mimetype change - $storage2->moveFromStorage($this->storage, 'foo', 'foo.b'); - $storage2->getUpdater()->renameFromStorage($this->storage, 'foo', 'foo.b'); - - $this->assertFalse($this->cache->inCache('foo')); - $this->assertFalse($this->cache->inCache('foo/foo.txt')); - $this->assertFalse($this->cache->inCache('foo/bar.txt')); - $this->assertFalse($this->cache->inCache('foo/bar')); - $this->assertFalse($this->cache->inCache('foo/bar/bar.txt')); - $this->assertTrue($cache2->inCache('foo.b')); - $this->assertTrue($cache2->inCache('foo.b/foo.txt')); - $this->assertTrue($cache2->inCache('foo.b/bar.txt')); - $this->assertTrue($cache2->inCache('foo.b/bar')); - $this->assertTrue($cache2->inCache('foo.b/bar/bar.txt')); - - $cachedTarget = []; - $cachedTarget[] = $cache2->get('foo.b'); - $cachedTarget[] = $cache2->get('foo.b/foo.txt'); - $cachedTarget[] = $cache2->get('foo.b/bar.txt'); - $cachedTarget[] = $cache2->get('foo.b/bar'); - $cachedTarget[] = $cache2->get('foo.b/bar/bar.txt'); - - foreach ($cached as $i => $old) { - $new = $cachedTarget[$i]; - $this->assertEquals($old['mtime'], $new['mtime']); - $this->assertEquals($old['size'], $new['size']); - $this->assertEquals($old['etag'], $new['etag']); - $this->assertEquals($old['fileid'], $new['fileid']); - $this->assertEquals($old['mimetype'], $new['mimetype']); - } - } - - public static function changeExtensionProvider(): array { - return [ - [new Temporary()], - [new ObjectStoreStorage(['objectstore' => new StorageObjectStore(new Temporary())])] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('changeExtensionProvider')] - public function testChangeExtension(IStorage $storage) { - $updater = $storage->getUpdater(); - $cache = $storage->getCache(); - $storage->file_put_contents('foo', 'qwerty'); - $updater->update('foo'); - - $bareCached = $cache->get('foo'); - $this->assertEquals('application/octet-stream', $bareCached->getMimeType()); - - $storage->rename('foo', 'foo.txt'); - $updater->renameFromStorage($storage, 'foo', 'foo.txt'); - - $cached = $cache->get('foo.txt'); - $this->assertEquals('text/plain', $cached->getMimeType()); - - $storage->rename('foo.txt', 'foo.md'); - $updater->renameFromStorage($storage, 'foo.txt', 'foo.md'); - - $cachedTarget = $cache->get('foo.md'); - $this->assertEquals('text/markdown', $cachedTarget->getMimeType()); - } -} --- a/tests/lib/Files/Cache/WatcherTest.php +++ /dev/null @@ -1,238 +0,0 @@ -loginAsUser(); - } - - #[\Override] - protected function tearDown(): void { - foreach ($this->storages as $storage) { - $cache = $storage->getCache(); - $ids = $cache->getAll(); - $cache->clear(); - } - - $this->logout(); - parent::tearDown(); - } - - public function testWatcher(): void { - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - $updater->setPolicy(Watcher::CHECK_ONCE); - - //set the mtime to the past so it can detect an mtime change - $cache->put('', ['storage_mtime' => 10]); - - $this->assertTrue($cache->inCache('folder/bar.txt')); - $this->assertTrue($cache->inCache('folder/bar2.txt')); - - $this->assertFalse($cache->inCache('bar.test')); - $storage->file_put_contents('bar.test', 'foo'); - $updater->checkUpdate(''); - $this->assertTrue($cache->inCache('bar.test')); - $cachedData = $cache->get('bar.test'); - $this->assertEquals(3, $cachedData['size']); - - $cache->put('bar.test', ['storage_mtime' => 10]); - $storage->file_put_contents('bar.test', 'test data'); - - // make sure that PHP can read the new size correctly - clearstatcache(); - - $updater->checkUpdate('bar.test'); - $cachedData = $cache->get('bar.test'); - $this->assertEquals(9, $cachedData['size']); - - $cache->put('folder', ['storage_mtime' => 10]); - - $storage->unlink('folder/bar2.txt'); - $updater->checkUpdate('folder'); - - $this->assertTrue($cache->inCache('folder/bar.txt')); - $this->assertFalse($cache->inCache('folder/bar2.txt')); - } - - public function testFileToFolder(): void { - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - $updater->setPolicy(Watcher::CHECK_ONCE); - - //set the mtime to the past so it can detect an mtime change - $cache->put('', ['storage_mtime' => 10]); - - $storage->unlink('foo.txt'); - $storage->rename('folder', 'foo.txt'); - $updater->checkUpdate(''); - - $entry = $cache->get('foo.txt'); - $this->assertEquals('httpd/unix-directory', $entry['mimetype']); - $this->assertFalse($cache->inCache('folder')); - $this->assertFalse($cache->inCache('folder/bar.txt')); - - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - $updater->setPolicy(Watcher::CHECK_ONCE); - - //set the mtime to the past so it can detect an mtime change - $cache->put('foo.txt', ['storage_mtime' => 10]); - - $storage->unlink('foo.txt'); - $storage->rename('folder', 'foo.txt'); - $updater->checkUpdate('foo.txt'); - - $entry = $cache->get('foo.txt'); - $this->assertEquals('httpd/unix-directory', $entry['mimetype']); - $this->assertTrue($cache->inCache('foo.txt/bar.txt')); - } - - public function testPolicyNever(): void { - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - - //set the mtime to the past so it can detect an mtime change - $cache->put('foo.txt', ['storage_mtime' => 10]); - - $updater->setPolicy(Watcher::CHECK_NEVER); - - $storage->file_put_contents('foo.txt', 'q'); - $this->assertFalse($updater->checkUpdate('foo.txt')); - - $cache->put('foo.txt', ['storage_mtime' => 20]); - $storage->file_put_contents('foo.txt', 'w'); - $this->assertFalse($updater->checkUpdate('foo.txt')); - } - - public function testPolicyOnce(): void { - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - - //set the mtime to the past so it can detect an mtime change - $cache->put('foo.txt', ['storage_mtime' => 10]); - - $updater->setPolicy(Watcher::CHECK_ONCE); - - $storage->file_put_contents('foo.txt', 'q'); - $this->assertTrue($updater->checkUpdate('foo.txt')); - - $cache->put('foo.txt', ['storage_mtime' => 20]); - $storage->file_put_contents('foo.txt', 'w'); - $this->assertFalse($updater->checkUpdate('foo.txt')); - } - - public function testPolicyAlways(): void { - $storage = $this->getTestStorage(); - $cache = $storage->getCache(); - $updater = $storage->getWatcher(); - - //set the mtime to the past so it can detect an mtime change - $cache->put('foo.txt', ['storage_mtime' => 10]); - - $updater->setPolicy(Watcher::CHECK_ALWAYS); - - $storage->file_put_contents('foo.txt', 'q'); - $this->assertTrue($updater->checkUpdate('foo.txt')); - - $cache->put('foo.txt', ['storage_mtime' => 20]); - $storage->file_put_contents('foo.txt', 'w'); - $this->assertTrue($updater->checkUpdate('foo.txt')); - } - - private function getTestStorage(bool $scan = true): IStorage { - $storage = new Temporary([]); - $textData = "dummy file data\n"; - $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $storage->mkdir('folder'); - $storage->file_put_contents('foo.txt', $textData); - $storage->file_put_contents('foo.png', $imgData); - $storage->file_put_contents('folder/bar.txt', $textData); - $storage->file_put_contents('folder/bar2.txt', $textData); - - if ($scan) { - $scanner = $storage->getScanner(); - $scanner->scan(''); - } - $this->storages[] = $storage; - return $storage; - } - - public static function checkFilterProvider(): array { - return [ - [null, [ - '' => true, - 'foo' => true, - 'foo.txt' => true, - ]], - ['/^.+$/', [ - '' => false, - 'foo' => true, - 'foo.txt' => true, - ]], - ['/^.+\..+$/', [ - '' => false, - 'foo' => false, - 'foo.txt' => true, - ]] - ]; - } - - #[DataProvider(methodName: 'checkFilterProvider')] - public function testCheckFilter(?string $filter, array $paths): void { - $storage = $this->createMock(IStorage::class); - $storage->method('hasUpdated') - ->willReturn(true); - $storage->method('getCache') - ->willReturn($this->createMock(ICache::class)); - $storage->method('getScanner') - ->willReturn($this->createMock(IScanner::class)); - $watcher = new Watcher($storage); - $watcher->setPolicy(IWatcher::CHECK_ALWAYS); - - $watcher->setCheckFilter($filter); - - $entry = new CacheEntry([ - 'storage_mtime' => 0, - ]); - - foreach ($paths as $patch => $shouldUpdate) { - $this->assertEquals($shouldUpdate, $watcher->needsUpdate($patch, $entry)); - } - } -} --- a/tests/lib/Files/Cache/Wrapper/CacheJailTest.php +++ /dev/null @@ -1,270 +0,0 @@ -storage->mkdir('jail'); - $this->sourceCache = $this->cache; - $this->cache = new CacheJail($this->sourceCache, 'jail'); - $this->cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - } - - public function testSearchOutsideJail(): void { - $this->storage->getScanner()->scan(''); - $file1 = 'jail/foobar'; - $file2 = 'folder/foobar'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - - $this->sourceCache->insert('folder', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - - $this->assertCount(2, $this->sourceCache->search('%foobar')); - - $result = $this->cache->search('%foobar%'); - $this->assertCount(1, $result); - $this->assertEquals('foobar', $result[0]['path']); - - $result = $this->cache->search('%foo%'); - $this->assertCount(1, $result); - usort($result, function ($a, $b) { - return $a['path'] <=> $b['path']; - }); - $this->assertEquals('foobar', $result[0]['path']); - } - - public function testSearchMimeOutsideJail(): void { - $this->storage->getScanner()->scan(''); - $file1 = 'jail/foobar'; - $file2 = 'folder/foobar'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - - $this->sourceCache->insert('folder', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - - $this->assertCount(2, $this->sourceCache->searchByMime('foo/folder')); - - $result = $this->cache->search('%foobar%'); - $this->assertCount(1, $result); - $this->assertEquals('foobar', $result[0]['path']); - } - - public function testSearchQueryOutsideJail(): void { - $this->storage->getScanner()->scan(''); - $file1 = 'jail/foobar'; - $file2 = 'folder/foobar'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - - $this->sourceCache->insert('folder', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - - $user = new User('foo', null, $this->createMock(IEventDispatcher::class)); - $query = new SearchQuery(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), 10, 0, [], $user); - $result = $this->cache->searchQuery($query); - - $this->assertCount(1, $result); - $this->assertEquals('foobar', $result[0]['path']); - - $query = new SearchQuery(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'jail'), 10, 0, [], $user); - $result = $this->cache->searchQuery($query); - $this->assertCount(1, $result); - $this->assertEquals('', $result[0]['path']); - } - - public function testClearKeepEntriesOutsideJail(): void { - $file1 = 'jail/foobar'; - $file2 = 'jail/foobar/asd'; - $file3 = 'folder/foobar'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->sourceCache->put('folder', $data1); - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - $this->sourceCache->put($file3, $data1); - - $this->cache->clear(); - - $this->assertFalse($this->cache->inCache('foobar')); - $this->assertTrue($this->sourceCache->inCache('folder/foobar')); - } - - #[\Override] - public function testGetById(): void { - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - $id = $this->sourceCache->put('jail/bar', $data1); - - // path from jailed foo of foo/bar is bar - $path = $this->cache->getPathById($id); - $this->assertEquals('bar', $path); - - // path from jailed '' of foo/bar is foo/bar - $this->cache = new CacheJail($this->sourceCache, ''); - $path = $this->cache->getPathById($id); - $this->assertEquals('jail/bar', $path); - } - - #[\Override] - public function testGetIncomplete(): void { - //not supported - $this->addToAssertionCount(1); - } - - public function testMoveFromJail(): void { - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->sourceCache->put('source', $folderData); - $this->sourceCache->put('source/foo', $folderData); - $this->sourceCache->put('source/foo/bar', $folderData); - $this->sourceCache->put('target', $folderData); - - $jail = new CacheJail($this->sourceCache, 'source'); - - $this->sourceCache->moveFromCache($jail, 'foo', 'target/foo'); - - $this->assertTrue($this->sourceCache->inCache('target/foo')); - $this->assertTrue($this->sourceCache->inCache('target/foo/bar')); - } - - public function testMoveToJail(): void { - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->sourceCache->put('source', $folderData); - $this->sourceCache->put('source/foo', $folderData); - $this->sourceCache->put('source/foo/bar', $folderData); - $this->sourceCache->put('target', $folderData); - - $jail = new CacheJail($this->sourceCache, 'target'); - - $jail->moveFromCache($this->sourceCache, 'source/foo', 'foo'); - - $this->assertTrue($this->sourceCache->inCache('target/foo')); - $this->assertTrue($this->sourceCache->inCache('target/foo/bar')); - } - - public function testMoveBetweenJail(): void { - $folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]; - - $this->sourceCache->put('source', $folderData); - $this->sourceCache->put('source/foo', $folderData); - $this->sourceCache->put('source/foo/bar', $folderData); - $this->sourceCache->put('target', $folderData); - - $jail = new CacheJail($this->sourceCache, 'target'); - $sourceJail = new CacheJail($this->sourceCache, 'source'); - - $jail->moveFromCache($sourceJail, 'foo', 'foo'); - - $this->assertTrue($this->sourceCache->inCache('target/foo')); - $this->assertTrue($this->sourceCache->inCache('target/foo/bar')); - } - - public function testSearchNested(): void { - $this->storage->getScanner()->scan(''); - $file1 = 'jail'; - $file2 = 'jail/bar'; - $file3 = 'jail/bar/asd'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - $this->sourceCache->put($file3, $data1); - - $nested = new CacheJail($this->cache, 'bar'); - - $result = $nested->search('%asd%'); - $this->assertCount(1, $result); - $this->assertEquals('asd', $result[0]['path']); - } - - public function testRootJail(): void { - $this->storage->getScanner()->scan(''); - $file1 = 'jail'; - $file2 = 'jail/bar'; - $file3 = 'jail/bar/asd'; - $data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder']; - - $this->sourceCache->put($file1, $data1); - $this->sourceCache->put($file2, $data1); - $this->sourceCache->put($file3, $data1); - - $nested = new CacheJail($this->sourceCache, ''); - - $result = $nested->search('%asd%'); - $this->assertCount(1, $result); - $this->assertEquals('jail/bar/asd', $result[0]['path']); - } - - public function testWatcher(): void { - $storage = new Jail([ - 'storage' => $this->storage, - 'root' => 'jail' - ]); - $storage->getScanner()->scan(''); - $storage->file_put_contents('bar', 'asd'); - - $this->assertFalse($this->cache->inCache('bar')); - $storage->getWatcher()->update('bar', ['mimetype' => 'text/plain']); - $this->assertTrue($this->cache->inCache('bar')); - } - - public function testWatcherAfterInnerWatcher(): void { - $storage = new Jail([ - 'storage' => $this->storage, - 'root' => 'jail' - ]); - $storage->getScanner()->scan(''); - $storage->file_put_contents('bar', 'asd'); - - // let the underlying storage create it's watcher first - $this->storage->getWatcher(); - - $this->assertFalse($this->cache->inCache('bar')); - $storage->getWatcher()->update('bar', ['mimetype' => 'text/plain']); - $this->assertTrue($this->cache->inCache('bar')); - } - - public function testUnJailedRoot(): void { - $jail1 = new CacheJail($this->sourceCache, 'foo'); - $jail2 = new CacheJail($jail1, 'bar'); - $this->assertEquals('foo/bar', $jail2->getGetUnjailedRoot()); - - $middleWrapper = new CacheWrapper($jail1); - $jail3 = new CacheJail($middleWrapper, 'bar'); - $this->assertEquals('foo/bar', $jail3->getGetUnjailedRoot()); - } -} --- a/tests/lib/Files/Cache/Wrapper/CachePermissionsMaskTest.php +++ /dev/null @@ -1,104 +0,0 @@ -storage->mkdir('foo'); - $this->sourceCache = $this->cache; - $this->cache = $this->getMaskedCached(Constants::PERMISSION_ALL); - } - - protected function getMaskedCached($mask) { - return new CachePermissionsMask($this->sourceCache, $mask); - } - - public static function maskProvider(): array { - return [ - [Constants::PERMISSION_ALL], - [Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE], - [Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE], - [Constants::PERMISSION_READ] - ]; - } - - /** - * @param int $mask - */ - #[\PHPUnit\Framework\Attributes\DataProvider('maskProvider')] - public function testGetMasked($mask): void { - $cache = $this->getMaskedCached($mask); - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'permissions' => Constants::PERMISSION_ALL]; - $this->sourceCache->put('foo', $data); - $result = $cache->get('foo'); - $this->assertEquals($mask, $result['permissions']); - - $data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE]; - $this->sourceCache->put('bar', $data); - $result = $cache->get('bar'); - $this->assertEquals($mask & ~Constants::PERMISSION_DELETE, $result['permissions']); - } - - /** - * @param int $mask - */ - #[\PHPUnit\Framework\Attributes\DataProvider('maskProvider')] - public function testGetFolderContentMasked($mask): void { - $this->storage->mkdir('foo'); - $this->storage->file_put_contents('foo/bar', 'asd'); - $this->storage->file_put_contents('foo/asd', 'bar'); - $this->storage->getScanner()->scan(''); - - $cache = $this->getMaskedCached($mask); - $files = $cache->getFolderContents('foo'); - $this->assertCount(2, $files); - - foreach ($files as $file) { - $this->assertEquals($mask & ~Constants::PERMISSION_CREATE, $file['permissions']); - } - } - - /** - * @param int $mask - */ - #[\PHPUnit\Framework\Attributes\DataProvider('maskProvider')] - public function testSearchMasked($mask): void { - $this->storage->mkdir('foo'); - $this->storage->file_put_contents('foo/bar', 'asd'); - $this->storage->file_put_contents('foo/foobar', 'bar'); - $this->storage->getScanner()->scan(''); - - $cache = $this->getMaskedCached($mask); - $files = $cache->search('%bar'); - $this->assertCount(2, $files); - - foreach ($files as $file) { - $this->assertEquals($mask & ~Constants::PERMISSION_CREATE, $file['permissions']); - } - } -} --- a/tests/lib/Files/Config/UserMountCacheTest.php +++ /dev/null @@ -1,675 +0,0 @@ -fileIds = []; - - $this->connection = Server::get(IDBConnection::class); - - $config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $config - ->expects($this->any()) - ->method('getUserValue') - ->willReturnArgument(3); - $config - ->expects($this->any()) - ->method('getAppValue') - ->willReturnArgument(2); - - $this->userManager = new Manager($config, $this->createMock(ICacheFactory::class), $this->createMock(IEventDispatcher::class), $this->createMock(LoggerInterface::class)); - $userBackend = new Dummy(); - $userBackend->createUser('u1', ''); - $userBackend->createUser('u2', ''); - $userBackend->createUser('u3', ''); - $this->userManager->registerBackend($userBackend); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - - $this->cache = new UserMountCache($this->connection, - $this->userManager, - $this->createMock(LoggerInterface::class), - $this->createMock(IEventLogger::class), - $this->eventDispatcher, - ); - } - - #[\Override] - protected function tearDown(): void { - $builder = $this->connection->getQueryBuilder(); - - $builder->delete('mounts')->executeStatement(); - - $builder = $this->connection->getQueryBuilder(); - - foreach ($this->fileIds as $fileId) { - $builder->delete('filecache') - ->where($builder->expr()->eq('fileid', new Literal($fileId))) - ->executeStatement(); - } - parent::tearDown(); - } - - private function getStorage($storageId, $rootInternalPath = '') { - $rootId = $this->createCacheEntry($rootInternalPath, $storageId); - - $storageCache = $this->createMock(\OC\Files\Cache\Storage::class); - $storageCache->expects($this->any()) - ->method('getNumericId') - ->willReturn($storageId); - - $cache = $this->createMock(Cache::class); - $cache->expects($this->any()) - ->method('getId') - ->willReturn($rootId); - $cache->method('getNumericStorageId') - ->willReturn($storageId); - - $storage = $this->createMock(Storage::class); - $storage->expects($this->any()) - ->method('getStorageCache') - ->willReturn($storageCache); - $storage->expects($this->any()) - ->method('getCache') - ->willReturn($cache); - - return [$storage, $rootId]; - } - - private function clearCache() { - $this->invokePrivate($this->cache, 'mountsForUsers', [new CappedMemoryCache()]); - } - - private function keyForMount(MountPoint $mount): string { - return $mount->getStorageRootId() . '::' . $mount->getMountPoint(); - } - - public function testNewMounts(): void { - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatchTyped') - ->with($this->callback(fn (UserMountAddedEvent $event) => $event->mountPoint->getMountPoint() === '/asd/')); - - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount = new MountPoint($storage, '/asd/'); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user); - - $this->assertCount(1, $cachedMounts); - $cachedMount = $cachedMounts[$this->keyForMount($mount)]; - $this->assertEquals('/asd/', $cachedMount->getMountPoint()); - $this->assertEquals($user->getUID(), $cachedMount->getUser()->getUID()); - $this->assertEquals($storage->getCache()->getId(''), $cachedMount->getRootId()); - $this->assertEquals($storage->getStorageCache()->getNumericId(), $cachedMount->getStorageId()); - } - - public function testSameMounts(): void { - $this->eventDispatcher - ->expects($this->once()) - ->method('dispatchTyped') - ->with($this->callback(fn (UserMountAddedEvent $event) => $event->mountPoint->getMountPoint() === '/asd/')); - - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount = new MountPoint($storage, '/asd/'); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user); - - $this->assertCount(1, $cachedMounts); - $cachedMount = $cachedMounts[$this->keyForMount($mount)]; - $this->assertEquals('/asd/', $cachedMount->getMountPoint()); - $this->assertEquals($user->getUID(), $cachedMount->getUser()->getUID()); - $this->assertEquals($storage->getCache()->getId(''), $cachedMount->getRootId()); - $this->assertEquals($storage->getStorageCache()->getNumericId(), $cachedMount->getStorageId()); - } - - public function testRemoveMounts(): void { - $operation = 0; - $this->eventDispatcher - ->expects($this->exactly(2)) - ->method('dispatchTyped') - ->with($this->callback(function ( - UserMountAddedEvent|UserMountRemovedEvent $event, - ) use (&$operation) { - return match (++$operation) { - 1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/asd/', - 2 => $event instanceof UserMountRemovedEvent && $event->mountPoint->getMountPoint() === '/asd/', - default => false, - }; - })); - - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount = new MountPoint($storage, '/asd/'); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $this->cache->registerMounts($user, []); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user); - - $this->assertCount(0, $cachedMounts); - } - - public function testChangeMounts(): void { - $operation = 0; - $this->eventDispatcher - ->expects($this->exactly(3)) - ->method('dispatchTyped') - ->with($this->callback(function ( - UserMountAddedEvent|UserMountRemovedEvent $event, - ) use (&$operation) { - return match (++$operation) { - 1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/bar/', - 2 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/foo/', - 3 => $event instanceof UserMountRemovedEvent && $event->mountPoint->getMountPoint() === '/bar/', - default => false, - }; - })); - - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount = new MountPoint($storage, '/bar/'); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $mount = new MountPoint($storage, '/foo/'); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user); - - $this->assertCount(1, $cachedMounts); - $cachedMount = $cachedMounts[$this->keyForMount($mount)]; - $this->assertEquals('/foo/', $cachedMount->getMountPoint()); - } - - public function testChangeMountId(): void { - $operation = 0; - $this->eventDispatcher - ->expects($this->exactly(2)) - ->method('dispatchTyped') - ->with($this->callback(function ( - UserMountAddedEvent|UserMountUpdatedEvent $event, - ) use (&$operation) { - return match (++$operation) { - 1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/foo/', - 2 => $event instanceof UserMountUpdatedEvent && $event->oldMountPoint->getMountId() === null && $event->newMountPoint->getMountId() === 1, - default => false, - }; - })); - - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount = new MountPoint($storage, '/foo/', null, null, null, null); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $mount = new MountPoint($storage, '/foo/', null, null, null, 1); - - $this->cache->registerMounts($user, [$mount]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user); - - $this->assertCount(1, $cachedMounts); - $cachedMount = $cachedMounts[$this->keyForMount($mount)]; - $this->assertEquals(1, $cachedMount->getMountId()); - } - - public function testGetMountsForUser(): void { - $user1 = $this->userManager->get('u1'); - $user2 = $this->userManager->get('u2'); - $user3 = $this->userManager->get('u3'); - - [$storage1, $id1] = $this->getStorage(1); - [$storage2, $id2] = $this->getStorage(2, 'foo/bar'); - $mount1 = new MountPoint($storage1, '/foo/'); - $mount2 = new MountPoint($storage2, '/bar/'); - - $this->cache->registerMounts($user1, [$mount1, $mount2]); - $this->cache->registerMounts($user2, [$mount2]); - $this->cache->registerMounts($user3, [$mount2]); - - $this->clearCache(); - - $user3->delete(); - - $cachedMounts = $this->cache->getMountsForUser($user1); - - $this->assertCount(2, $cachedMounts); - $this->assertEquals('/foo/', $cachedMounts[$this->keyForMount($mount1)]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[$this->keyForMount($mount1)]->getUser()->getUID()); - $this->assertEquals($id1, $cachedMounts[$this->keyForMount($mount1)]->getRootId()); - $this->assertEquals(1, $cachedMounts[$this->keyForMount($mount1)]->getStorageId()); - $this->assertEquals('', $cachedMounts[$this->keyForMount($mount1)]->getRootInternalPath()); - - $this->assertEquals('/bar/', $cachedMounts[$this->keyForMount($mount2)]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[$this->keyForMount($mount2)]->getUser()->getUID()); - $this->assertEquals($id2, $cachedMounts[$this->keyForMount($mount2)]->getRootId()); - $this->assertEquals(2, $cachedMounts[$this->keyForMount($mount2)]->getStorageId()); - $this->assertEquals('foo/bar', $cachedMounts[$this->keyForMount($mount2)]->getRootInternalPath()); - - $cachedMounts = $this->cache->getMountsForUser($user3); - $this->assertEmpty($cachedMounts); - } - - public function testGetMountsByStorageId(): void { - $user1 = $this->userManager->get('u1'); - $user2 = $this->userManager->get('u2'); - - [$storage1, $id1] = $this->getStorage(1); - [$storage2, $id2] = $this->getStorage(2); - $mount1 = new MountPoint($storage1, '/foo/'); - $mount2 = new MountPoint($storage2, '/bar/'); - - $this->cache->registerMounts($user1, [$mount1, $mount2]); - $this->cache->registerMounts($user2, [$mount2]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForStorageId(2); - $this->sortMounts($cachedMounts); - - $this->assertCount(2, $cachedMounts); - - $this->assertEquals('/bar/', $cachedMounts[0]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[0]->getUser()->getUID()); - $this->assertEquals($id2, $cachedMounts[0]->getRootId()); - $this->assertEquals(2, $cachedMounts[0]->getStorageId()); - - $this->assertEquals('/bar/', $cachedMounts[1]->getMountPoint()); - $this->assertEquals($user2->getUID(), $cachedMounts[1]->getUser()->getUID()); - $this->assertEquals($id2, $cachedMounts[1]->getRootId()); - $this->assertEquals(2, $cachedMounts[1]->getStorageId()); - } - - public function testGetMountsByRootId(): void { - $user1 = $this->userManager->get('u1'); - $user2 = $this->userManager->get('u2'); - - [$storage1, $id1] = $this->getStorage(1); - [$storage2, $id2] = $this->getStorage(2); - $mount1 = new MountPoint($storage1, '/foo/'); - $mount2 = new MountPoint($storage2, '/bar/'); - - $this->cache->registerMounts($user1, [$mount1, $mount2]); - $this->cache->registerMounts($user2, [$mount2]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForRootId($id2); - $this->sortMounts($cachedMounts); - - $this->assertCount(2, $cachedMounts); - - $this->assertEquals('/bar/', $cachedMounts[0]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[0]->getUser()->getUID()); - $this->assertEquals($id2, $cachedMounts[0]->getRootId()); - $this->assertEquals(2, $cachedMounts[0]->getStorageId()); - - $this->assertEquals('/bar/', $cachedMounts[1]->getMountPoint()); - $this->assertEquals($user2->getUID(), $cachedMounts[1]->getUser()->getUID()); - $this->assertEquals($id2, $cachedMounts[1]->getRootId()); - $this->assertEquals(2, $cachedMounts[1]->getStorageId()); - } - - private function sortMounts(&$mounts) { - usort($mounts, function (ICachedMountInfo $a, ICachedMountInfo $b) { - return strcmp($a->getUser()->getUID(), $b->getUser()->getUID()); - }); - } - - private function createCacheEntry($internalPath, $storageId, $size = 0) { - $internalPath = trim($internalPath, '/'); - try { - $query = $this->connection->getQueryBuilder(); - $query->insert('filecache') - ->values([ - 'storage' => $query->createNamedParameter($storageId), - 'path' => $query->createNamedParameter($internalPath), - 'path_hash' => $query->createNamedParameter(md5($internalPath)), - 'parent' => $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT), - 'name' => $query->createNamedParameter(basename($internalPath)), - 'mimetype' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'mimepart' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'size' => $query->createNamedParameter($size), - 'storage_mtime' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'encrypted' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'unencrypted_size' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'etag' => $query->createNamedParameter(''), - 'permissions' => $query->createNamedParameter(31, IQueryBuilder::PARAM_INT), - ]); - $query->executeStatement(); - $id = $query->getLastInsertId(); - $this->fileIds[] = $id; - } catch (DbalException $e) { - if ($e->getReason() === DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { - $query = $this->connection->getQueryBuilder(); - $query->select('fileid') - ->from('filecache') - ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId))) - ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5($internalPath)))); - $id = (int)$query->executeQuery()->fetchOne(); - } else { - throw $e; - } - } - return $id; - } - - public function testGetMountsForFileIdRootId(): void { - $user1 = $this->userManager->get('u1'); - - [$storage1, $rootId] = $this->getStorage(2); - $mount1 = new MountPoint($storage1, '/foo/'); - - $this->cache->registerMounts($user1, [$mount1]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForFileId($rootId); - - $this->assertCount(1, $cachedMounts); - - $this->assertEquals('/foo/', $cachedMounts[0]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[0]->getUser()->getUID()); - $this->assertEquals($rootId, $cachedMounts[0]->getRootId()); - $this->assertEquals(2, $cachedMounts[0]->getStorageId()); - } - - public function testGetMountsForFileIdSubFolder(): void { - $user1 = $this->userManager->get('u1'); - - $fileId = $this->createCacheEntry('/foo/bar', 2); - - [$storage1, $rootId] = $this->getStorage(2); - $mount1 = new MountPoint($storage1, '/foo/'); - - $this->cache->registerMounts($user1, [$mount1]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForFileId($fileId); - - $this->assertCount(1, $cachedMounts); - - $this->assertEquals('/foo/', $cachedMounts[0]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[0]->getUser()->getUID()); - $this->assertEquals($rootId, $cachedMounts[0]->getRootId()); - $this->assertEquals(2, $cachedMounts[0]->getStorageId()); - $this->assertEquals('foo/bar', $cachedMounts[0]->getInternalPath()); - $this->assertEquals('/foo/foo/bar', $cachedMounts[0]->getPath()); - } - - public function testGetMountsForFileIdSubFolderMount(): void { - $user1 = $this->userManager->get('u1'); - - [$storage1, $rootId] = $this->getStorage(2); - $folderId = $this->createCacheEntry('/foo', 2); - $fileId = $this->createCacheEntry('/foo/bar', 2); - - $mount1 = $this->getMockBuilder(MountPoint::class) - ->setConstructorArgs([$storage1, '/']) - ->onlyMethods(['getStorageRootId']) - ->getMock(); - - $mount1->expects($this->any()) - ->method('getStorageRootId') - ->willReturn($folderId); - - $this->cache->registerMounts($user1, [$mount1]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForFileId($fileId); - - $this->assertCount(1, $cachedMounts); - - $this->assertEquals('/', $cachedMounts[0]->getMountPoint()); - $this->assertEquals($user1->getUID(), $cachedMounts[0]->getUser()->getUID()); - $this->assertEquals($folderId, $cachedMounts[0]->getRootId()); - $this->assertEquals(2, $cachedMounts[0]->getStorageId()); - $this->assertEquals('foo', $cachedMounts[0]->getRootInternalPath()); - $this->assertEquals('bar', $cachedMounts[0]->getInternalPath()); - $this->assertEquals('/bar', $cachedMounts[0]->getPath()); - } - - public function testGetMountsForFileIdSubFolderMountOutside(): void { - $user1 = $this->userManager->get('u1'); - - [$storage1, $rootId] = $this->getStorage(2); - $folderId = $this->createCacheEntry('/foo', 2); - $fileId = $this->createCacheEntry('/bar/asd', 2); - - $mount1 = $this->getMockBuilder(MountPoint::class) - ->setConstructorArgs([$storage1, '/foo/']) - ->onlyMethods(['getStorageRootId']) - ->getMock(); - - $mount1->expects($this->any()) - ->method('getStorageRootId') - ->willReturn($folderId); - - $this->cache->registerMounts($user1, [$mount1]); - - $this->cache->registerMounts($user1, [$mount1]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForFileId($fileId); - - $this->assertCount(0, $cachedMounts); - } - - public function testGetMountsForFileIdDeletedUser(): void { - $user1 = $this->userManager->get('u1'); - - [$storage1, $rootId] = $this->getStorage(2); - $rootId = $this->createCacheEntry('', 2); - $mount1 = new MountPoint($storage1, '/foo/'); - $this->cache->registerMounts($user1, [$mount1]); - - $user1->delete(); - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForFileId($rootId); - $this->assertEmpty($cachedMounts); - } - - public function testGetUsedSpaceForUsers(): void { - $user1 = $this->userManager->get('u1'); - $user2 = $this->userManager->get('u2'); - - /** @var Storage $storage1 */ - [$storage1, $rootId] = $this->getStorage(2); - $folderId = $this->createCacheEntry('files', 2, 100); - $fileId = $this->createCacheEntry('files/foo', 2, 7); - $storage1->getCache()->put($folderId, ['size' => 100]); - $storage1->getCache()->update($fileId, ['size' => 70]); - - $mount1 = $this->getMockBuilder(MountPoint::class) - ->setConstructorArgs([$storage1, '/u1/']) - ->onlyMethods(['getStorageRootId', 'getNumericStorageId']) - ->getMock(); - - $mount1->expects($this->any()) - ->method('getStorageRootId') - ->willReturn($rootId); - - $mount1->expects($this->any()) - ->method('getNumericStorageId') - ->willReturn(2); - - $this->cache->registerMounts($user1, [$mount1]); - - $result = $this->cache->getUsedSpaceForUsers([$user1, $user2]); - $this->assertEquals(['u1' => 100], $result); - } - - public function testMigrateMountProvider(): void { - $user1 = $this->userManager->get('u1'); - - [$storage1, $rootId] = $this->getStorage(2); - $rootId = $this->createCacheEntry('', 2); - $mount1 = new MountPoint($storage1, '/foo/'); - $this->cache->registerMounts($user1, [$mount1]); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user1); - $this->assertCount(1, $cachedMounts); - $this->assertEquals('', $cachedMounts[$this->keyForMount($mount1)]->getMountProvider()); - - $mount1 = new MountPoint($storage1, '/foo/', null, null, null, null, 'dummy'); - $this->cache->registerMounts($user1, [$mount1], ['dummy']); - - $this->clearCache(); - - $cachedMounts = $this->cache->getMountsForUser($user1); - $this->assertCount(1, $cachedMounts); - $this->assertEquals('dummy', $cachedMounts[$this->keyForMount($mount1)]->getMountProvider()); - } - - public function testChangedSameRootId(): void { - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount1 = new MountPoint($storage, '/asd/'); - $mount2 = new MountPoint($storage, '/asd2/'); - - $this->cache->registerMounts($user, [$mount1, $mount2]); - - $mount2 = new MountPoint($storage, '/asd2/', null, null, null, 1); - $this->cache->registerMounts($user, [$mount1, $mount2]); - - $this->cache->flush(); - $cached = $this->cache->getMountsForUser($user); - usort($cached, fn (ICachedMountInfo $a, ICachedMountInfo $b, - ) => $a->getMountPoint() <=> $b->getMountPoint()); - - $mountPoints = array_map(fn (ICachedMountInfo $mountInfo, - ) => $mountInfo->getMountPoint(), $cached); - $this->assertEquals(['/asd/', '/asd2/'], $mountPoints); - - $mountIds = array_map(fn (ICachedMountInfo $mountInfo, - ) => $mountInfo->getMountId(), $cached); - $this->assertEquals([null, 1], $mountIds); - } - - public function testGetMountForPath(): void { - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount1 = new MountPoint($storage, '/asd/'); - $mount2 = new MountPoint($storage, '/asd/foo'); - - $this->cache->registerMounts($user, [$mount1, $mount2]); - $this->cache->flush(); - - $this->assertEquals('/asd/', $this->cache->getMountForPath($user, '/asd/bar/')->getMountPoint()); - $this->assertEquals('/asd/', $this->cache->getMountForPath($user, '/asd/')->getMountPoint()); - $this->assertEquals('/asd/foo/', $this->cache->getMountForPath($user, '/asd/foo/bar/')->getMountPoint()); - $this->assertEquals('/asd/foo/', $this->cache->getMountForPath($user, '/asd/foo/')->getMountPoint()); - } - - public function testGetMountsInPath(): void { - $user = $this->userManager->get('u1'); - - [$storage] = $this->getStorage(10); - $mount1 = new MountPoint($storage, '/asd/'); - $mount2 = new MountPoint($storage, '/asd/foo/'); - $mount3 = new MountPoint($storage, '/asd/foo/bar/'); - - $this->cache->registerMounts($user, [$mount1, $mount2, $mount3]); - $this->cache->flush(); - - $getMountPaths = function (string $path) use ($user) { - $mountPoints = array_values( - array_map( - fn (ICachedMountInfo $mount) => $mount->getMountPoint(), - $this->cache->getMountsInPath($user, $path) - ) - ); - sort($mountPoints); - return $mountPoints; - }; - - $this->assertEquals(['/asd/foo/', '/asd/foo/bar/'], $getMountPaths('/asd/')); - $this->assertEquals([], $getMountPaths('/asd/foo/bar/')); - $this->assertEquals(['/asd/foo/bar/'], $getMountPaths('/asd/foo/')); - $this->assertEquals([], $getMountPaths('/asd/bar/')); - } -} --- a/tests/lib/Files/EtagTest.php +++ /dev/null @@ -1,106 +0,0 @@ -datadir = $config->getSystemValueString('datadirectory'); - $this->tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $config->setSystemValue('datadirectory', $this->tmpDir); - - $this->userBackend = new \Test\Util\User\Dummy(); - Server::get(IUserManager::class)->registerBackend($this->userBackend); - } - - #[\Override] - protected function tearDown(): void { - Server::get(IConfig::class)->setSystemValue('datadirectory', $this->datadir); - - $this->logout(); - parent::tearDown(); - } - - public function testNewUser(): void { - $user1 = $this->getUniqueID('user_'); - $this->userBackend->createUser($user1, ''); - - $this->loginAsUser($user1); - Filesystem::mkdir('/folder'); - Filesystem::mkdir('/folder/subfolder'); - Filesystem::file_put_contents('/foo.txt', 'asd'); - Filesystem::file_put_contents('/folder/bar.txt', 'fgh'); - Filesystem::file_put_contents('/folder/subfolder/qwerty.txt', 'jkl'); - - $files = ['/foo.txt', '/folder/bar.txt', '/folder/subfolder', '/folder/subfolder/qwerty.txt']; - $originalEtags = $this->getEtags($files); - - $scanner = new Scanner( - Server::get(IUserManager::class)->get($user1), - Server::get(IDBConnection::class), - Server::get(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->backgroundScan('/'); - - $newEtags = $this->getEtags($files); - // loop over array and use assertSame over assertEquals to prevent false positives - foreach ($originalEtags as $file => $originalEtag) { - $this->assertSame($originalEtag, $newEtags[$file]); - } - } - - /** - * @param string[] $files - */ - private function getEtags($files) { - $etags = []; - foreach ($files as $file) { - $info = Filesystem::getFileInfo($file); - $etags[$file] = $info['etag']; - } - return $etags; - } -} --- a/tests/lib/Files/FileInfoTest.php +++ /dev/null @@ -1,109 +0,0 @@ -createUser('foo', 'foo'); - $this->config = $this->getMockBuilder(IConfig::class)->getMock(); - } - - private function makeFileInfo(array $data): FileInfo { - $storage = new Temporary(); - return new FileInfo('', $storage, '', $data, new MountPoint($storage, '/foo/files')); - } - - public function testGetSizeEncryptedZeroByte(): void { - $info = $this->makeFileInfo(['encrypted' => true, 'size' => 8192, 'unencrypted_size' => 0]); - // Both paths must report the true plaintext size (0), not the on-disk encrypted size (8192) - $this->assertSame(0, $info->getSize(true)); - $this->assertSame(0, $info->getSize(false)); - } - - public function testGetSizeEncryptedNonZero(): void { - $info = $this->makeFileInfo(['encrypted' => true, 'size' => 16384, 'unencrypted_size' => 5000]); - $this->assertSame(5000, $info->getSize(true)); - $this->assertSame(5000, $info->getSize(false)); - } - - public function testGetSizeEncryptedFolderWithoutPlaintextSize(): void { - // End-to-end encrypted folders carry the `encrypted` flag but never a plaintext size, - // so the propagated `size` has to be reported instead of the unset `unencrypted_size` - $info = $this->makeFileInfo([ - 'encrypted' => true, - 'mimetype' => FileInfo::MIMETYPE_FOLDER, - 'size' => 8192, - 'unencrypted_size' => 0, - ]); - $this->assertSame(8192, $info->getSize(true)); - $this->assertSame(8192, $info->getSize(false)); - } - - public function testGetSizeEncryptedFolderWithPlaintextSize(): void { - $info = $this->makeFileInfo([ - 'encrypted' => true, - 'mimetype' => FileInfo::MIMETYPE_FOLDER, - 'size' => 8192, - 'unencrypted_size' => 5000, - ]); - $this->assertSame(5000, $info->getSize(true)); - $this->assertSame(5000, $info->getSize(false)); - } - - public function testGetSizeNonEncrypted(): void { - $info = $this->makeFileInfo(['encrypted' => false, 'size' => 100]); - $this->assertSame(100, $info->getSize(true)); - $this->assertSame(100, $info->getSize(false)); - } - - public function testIsMountedHomeStorage(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('foo'); - $user->method('getHome') - ->willReturn('foo'); - $storage = new Home(['user' => $user]); - - $fileInfo = new FileInfo( - '', - $storage, - '', - [], - new HomeMountPoint($user, $storage, '/foo/files') - ); - $this->assertFalse($fileInfo->isMounted()); - } - - public function testIsMountedNonHomeStorage(): void { - $storage = new Temporary(); - $fileInfo = new FileInfo( - '', - $storage, - '', - [], - new MountPoint($storage, '/foo/files/bar') - ); - $this->assertTrue($fileInfo->isMounted()); - } -} --- a/tests/lib/Files/FilenameValidatorTest.php +++ /dev/null @@ -1,490 +0,0 @@ -createMock(IL10N::class); - $l10n->method('t') - ->willReturnCallback(fn ($string, $params) => sprintf($string, ...$params)); - $this->l10n = $this->createMock(IFactory::class); - $this->l10n - ->method('get') - ->willReturn($l10n); - - $this->config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->database = $this->createMock(IDBConnection::class); - $this->database->method('supports4ByteText')->willReturn(true); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataValidateFilename')] - public function testValidateFilename( - string $filename, - array $forbiddenNames, - array $forbiddenBasenames, - array $forbiddenExtensions, - array $forbiddenCharacters, - ?string $exception, - ): void { - /** @var FilenameValidator&MockObject */ - $validator = $this->getMockBuilder(FilenameValidator::class) - ->onlyMethods([ - 'getForbiddenBasenames', - 'getForbiddenCharacters', - 'getForbiddenExtensions', - 'getForbiddenFilenames', - ]) - ->setConstructorArgs([$this->l10n, $this->database, $this->config, $this->logger]) - ->getMock(); - - $validator->method('getForbiddenBasenames') - ->willReturn($forbiddenBasenames); - $validator->method('getForbiddenCharacters') - ->willReturn($forbiddenCharacters); - $validator->method('getForbiddenExtensions') - ->willReturn($forbiddenExtensions); - $validator->method('getForbiddenFilenames') - ->willReturn($forbiddenNames); - - if ($exception !== null) { - $this->expectException($exception); - } else { - $this->expectNotToPerformAssertions(); - } - $validator->validateFilename($filename); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataValidateFilename')] - public function testIsFilenameValid( - string $filename, - array $forbiddenNames, - array $forbiddenBasenames, - array $forbiddenExtensions, - array $forbiddenCharacters, - ?string $exception, - ): void { - /** @var FilenameValidator&MockObject */ - $validator = $this->getMockBuilder(FilenameValidator::class) - ->onlyMethods([ - 'getForbiddenBasenames', - 'getForbiddenExtensions', - 'getForbiddenFilenames', - 'getForbiddenCharacters', - ]) - ->setConstructorArgs([$this->l10n, $this->database, $this->config, $this->logger]) - ->getMock(); - - $validator->method('getForbiddenBasenames') - ->willReturn($forbiddenBasenames); - $validator->method('getForbiddenCharacters') - ->willReturn($forbiddenCharacters); - $validator->method('getForbiddenExtensions') - ->willReturn($forbiddenExtensions); - $validator->method('getForbiddenFilenames') - ->willReturn($forbiddenNames); - - $this->assertEquals($exception === null, $validator->isFilenameValid($filename)); - } - - public static function dataValidateFilename(): array { - return [ - 'valid name' => [ - 'a: b.txt', ['.htaccess'], [], [], [], null - ], - 'forbidden name in the middle is ok' => [ - 'a.htaccess.txt', ['.htaccess'], [], [], [], null - ], - 'valid name with some more parameters' => [ - 'a: b.txt', ['.htaccess'], [], ['exe'], ['~'], null - ], - 'valid name checks only the full name' => [ - '.htaccess.sample', ['.htaccess'], [], [], [], null - ], - 'forbidden name' => [ - '.htaccess', ['.htaccess'], [], [], [], ReservedWordException::class - ], - 'forbidden name - name is case insensitive' => [ - 'COM1', ['.htaccess', 'com1'], [], [], [], ReservedWordException::class - ], - 'forbidden basename' => [ - // needed for Windows namespaces - 'com1.suffix', ['.htaccess'], ['com1'], [], [], ReservedWordException::class - ], - 'forbidden basename case insensitive' => [ - // needed for Windows namespaces - 'COM1.suffix', ['.htaccess'], ['com1'], [], [], ReservedWordException::class - ], - 'forbidden basename for hidden files' => [ - // needed for Windows namespaces - '.thumbs.db', ['.htaccess'], ['.thumbs'], [], [], ReservedWordException::class - ], - 'invalid character' => [ - 'a: b.txt', ['.htaccess'], [], [], [':'], InvalidCharacterInPathException::class - ], - 'invalid path' => [ - '../../foo.bar', ['.htaccess'], [], [], ['/', '\\'], InvalidCharacterInPathException::class, - ], - 'invalid extension' => [ - 'a: b.txt', ['.htaccess'], [], ['.txt'], [], InvalidPathException::class - ], - 'invalid extension case insensitive' => [ - 'a: b.TXT', ['.htaccess'], [], ['.txt'], [], InvalidPathException::class - ], - 'empty filename' => [ - '', [], [], [], [], EmptyFileNameException::class - ], - 'reserved unix name "."' => [ - '.', [], [], [], [], InvalidDirectoryException::class - ], - 'reserved unix name ".."' => [ - '..', [], [], [], [], InvalidDirectoryException::class - ], - 'weird but valid tripple dot name' => [ - '...', [], [], [], [], null // is valid - ], - 'too long filename "."' => [ - str_repeat('a', 251), [], [], [], [], FileNameTooLongException::class - ], - // make sure to not split the list entries as they migh contain Unicode sequences - // in this example the "face in clouds" emoji contains the clouds emoji so only having clouds is ok - ['🌫️.txt', ['.htaccess'], [], [], ['😶‍🌫️'], null], - // This is the reverse: clouds are forbidden -> so is also the face in the clouds emoji - ['😶‍🌫️.txt', ['.htaccess'], [], [], ['🌫️'], InvalidCharacterInPathException::class], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('data4ByteUnicode')] - public function testDatabaseDoesNotSupport4ByteText($filename): void { - $database = $this->createMock(IDBConnection::class); - $database->expects($this->once()) - ->method('supports4ByteText') - ->willReturn(false); - $this->expectException(InvalidCharacterInPathException::class); - $validator = new FilenameValidator($this->l10n, $database, $this->config, $this->logger); - $validator->validateFilename($filename); - } - - public static function data4ByteUnicode(): array { - return [ - ['plane 1 𐪅'], - ['emoji 😶‍🌫️'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataInvalidAsciiCharacters')] - public function testInvalidAsciiCharactersAreAlwaysForbidden(string $filename): void { - $this->expectException(InvalidPathException::class); - $validator = new FilenameValidator($this->l10n, $this->database, $this->config, $this->logger); - $validator->validateFilename($filename); - } - - public static function dataInvalidAsciiCharacters(): array { - return [ - [\chr(0)], - [\chr(1)], - [\chr(2)], - [\chr(3)], - [\chr(4)], - [\chr(5)], - [\chr(6)], - [\chr(7)], - [\chr(8)], - [\chr(9)], - [\chr(10)], - [\chr(11)], - [\chr(12)], - [\chr(13)], - [\chr(14)], - [\chr(15)], - [\chr(16)], - [\chr(17)], - [\chr(18)], - [\chr(19)], - [\chr(20)], - [\chr(21)], - [\chr(22)], - [\chr(23)], - [\chr(24)], - [\chr(25)], - [\chr(26)], - [\chr(27)], - [\chr(28)], - [\chr(29)], - [\chr(30)], - [\chr(31)], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsForbidden')] - public function testIsForbidden(string $filename, array $forbiddenNames, bool $expected): void { - /** @var FilenameValidator&MockObject */ - $validator = $this->getMockBuilder(FilenameValidator::class) - ->onlyMethods(['getForbiddenFilenames']) - ->setConstructorArgs([$this->l10n, $this->database, $this->config, $this->logger]) - ->getMock(); - - $validator->method('getForbiddenFilenames') - ->willReturn($forbiddenNames); - - $this->assertEquals($expected, $validator->isForbidden($filename)); - } - - public static function dataIsForbidden(): array { - return [ - 'valid name' => [ - 'a: b.txt', ['.htaccess'], false - ], - 'valid name with some more parameters' => [ - 'a: b.txt', ['.htaccess'], false - ], - 'valid name as only full forbidden should be matched' => [ - '.htaccess.sample', ['.htaccess'], false, - ], - 'forbidden name' => [ - '.htaccess', ['.htaccess'], true - ], - 'forbidden name - name is case insensitive' => [ - 'COM1', ['.htaccess', 'com1'], true, - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetForbiddenExtensions')] - public function testGetForbiddenExtensions(array $configValue, array $expectedValue): void { - $validator = new FilenameValidator($this->l10n, $this->database, $this->config, $this->logger); - $this->config - // only once - then cached - ->expects(self::once()) - ->method('getSystemValue') - ->with('forbidden_filename_extensions', ['.filepart']) - ->willReturn($configValue); - - self::assertEqualsCanonicalizing($expectedValue, $validator->getForbiddenExtensions()); - } - - public static function dataGetForbiddenExtensions(): array { - return [ - // default - [['.filepart'], ['.filepart', '.part']], - // always include .part - [[], ['.part']], - // handle case insensitivity - [['.TXT'], ['.txt', '.part']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetForbiddenFilenames')] - public function testGetForbiddenFilenames(array $configValue, array $legacyValue, array $expectedValue): void { - $validator = new FilenameValidator($this->l10n, $this->database, $this->config, $this->logger); - $this->config - // only once - then cached - ->expects(self::exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['forbidden_filenames', ['.htaccess'], $configValue], - ['blacklisted_files', [], $legacyValue], - ]); - - $this->logger - ->expects(empty($legacyValue) ? self::never() : self::once()) - ->method('warning'); - - self::assertEqualsCanonicalizing($expectedValue, $validator->getForbiddenFilenames()); - } - - public static function dataGetForbiddenFilenames(): array { - return [ - // default - [['.htaccess'], [], ['.htaccess']], - // with legacy values - [['.htaccess'], ['legacy'], ['.htaccess', 'legacy']], - // handle case insensitivity - [['FileName', '.htaccess'], ['LegAcy'], ['.htaccess', 'filename', 'legacy']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetForbiddenBasenames')] - public function testGetForbiddenBasenames(array $configValue, array $expectedValue): void { - $validator = new FilenameValidator($this->l10n, $this->database, $this->config, $this->logger); - $this->config - // only once - then cached - ->expects(self::once()) - ->method('getSystemValue') - ->with('forbidden_filename_basenames', []) - ->willReturn($configValue); - - self::assertEqualsCanonicalizing($expectedValue, $validator->getForbiddenBasenames()); - } - - public static function dataGetForbiddenBasenames(): array { - return [ - // default - [[], []], - // with values - [['aux', 'com0'], ['aux', 'com0']], - // handle case insensitivity - [['AuX', 'COM1'], ['aux', 'com1']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeFilename')] - public function testSanitizeFilename( - string $filename, - array $forbiddenNames, - array $forbiddenBasenames, - array $forbiddenExtensions, - array $forbiddenCharacters, - string $expected, - ): void { - /** @var FilenameValidator&MockObject */ - $validator = $this->getMockBuilder(FilenameValidator::class) - ->onlyMethods([ - 'getForbiddenBasenames', - 'getForbiddenExtensions', - 'getForbiddenFilenames', - 'getForbiddenCharacters', - ]) - ->setConstructorArgs([$this->l10n, $this->database, $this->config, $this->logger]) - ->getMock(); - - $validator->method('getForbiddenBasenames') - ->willReturn($forbiddenBasenames); - $validator->method('getForbiddenCharacters') - ->willReturn($forbiddenCharacters); - $validator->method('getForbiddenExtensions') - ->willReturn($forbiddenExtensions); - $validator->method('getForbiddenFilenames') - ->willReturn($forbiddenNames); - - $this->assertEquals($expected, $validator->sanitizeFilename($filename)); - } - - public static function dataSanitizeFilename(): array { - return [ - 'valid name' => [ - 'a * b.txt', ['.htaccess'], [], [], [], 'a * b.txt' - ], - 'forbidden name in the middle is ok' => [ - 'a.htaccess.txt', ['.htaccess'], [], [], [], 'a.htaccess.txt' - ], - 'forbidden name on the beginning' => [ - '.htaccess.sample', ['.htaccess'], [], [], [], '.htaccess.sample' - ], - 'forbidden name' => [ - '.htaccess', ['.htaccess'], [], [], [], '.htaccess (renamed)' - ], - 'forbidden name - name is case insensitive' => [ - 'COM1', ['.htaccess', 'com1'], [], [], [], 'COM1 (renamed)' - ], - 'forbidden basename' => [ - 'com1.suffix', ['.htaccess'], ['com1'], [], [], 'com1 (renamed).suffix' - ], - 'forbidden basename case insensitive' => [ - // needed for Windows namespaces - 'COM1.suffix', ['.htaccess'], ['com1'], [], [], 'COM1 (renamed).suffix' - ], - 'forbidden basename for hidden files' => [ - // needed for Windows namespaces - '.thumbs.db', ['.htaccess'], ['.thumbs'], [], [], '.thumbs (renamed).db' - ], - 'invalid character' => [ - 'a: b.txt', ['.htaccess'], [], [], [':'], 'a_ b.txt', - ], - 'invalid extension' => [ - 'a: b.txt', ['.htaccess'], [], ['.txt'], [], 'a: b' - ], - 'invalid extension case insensitive' => [ - 'a: b.TXT', ['.htaccess'], [], ['.txt'], [], 'a: b' - ], - 'empty filename' => [ - '', [], [], [], [], 'renamed file' - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSanitizeFilenameCharacterReplacement')] - public function testSanitizeFilenameCharacterReplacement( - string $filename, - array $forbiddenCharacters, - ?string $characterReplacement, - ?string $expected, - ): void { - /** @var FilenameValidator&MockObject */ - $validator = $this->getMockBuilder(FilenameValidator::class) - ->onlyMethods([ - 'getForbiddenBasenames', - 'getForbiddenExtensions', - 'getForbiddenFilenames', - 'getForbiddenCharacters', - ]) - ->setConstructorArgs([$this->l10n, $this->database, $this->config, $this->logger]) - ->getMock(); - - $validator->method('getForbiddenBasenames') - ->willReturn([]); - $validator->method('getForbiddenCharacters') - ->willReturn($forbiddenCharacters); - $validator->method('getForbiddenExtensions') - ->willReturn([]); - $validator->method('getForbiddenFilenames') - ->willReturn([]); - - if ($expected === null) { - $this->expectException(\InvalidArgumentException::class); - $validator->sanitizeFilename($filename, $characterReplacement); - } else { - $this->assertEquals($expected, $validator->sanitizeFilename($filename, $characterReplacement)); - } - } - - public static function dataSanitizeFilenameCharacterReplacement(): array { - return [ - 'default' => [ - 'foo*bar', ['*'], null, 'foo_bar' - ], - 'default - underscore not allowed' => [ - 'foo*bar', ['*', '_'], null, 'foo-bar' - ], - 'default - dash and underscore not allowed' => [ - 'foo*bar', ['*', '-', '_'], null, 'foo bar' - ], - 'default - no replacement' => [ - 'foo*bar', ['*', ' ', '_', '-'], null, null - ], - 'custom replacement' => [ - 'foo*bar', ['*'], 'x', 'fooxbar' - ], - ]; - } -} --- a/tests/lib/Files/FilesystemTest.php +++ /dev/null @@ -1,480 +0,0 @@ -mounts[$user->getUID()]) ? $this->mounts[$user->getUID()] : []; - } -} - -/** - * Class FilesystemTest - * - * - * @package Test\Files - */ -#[\PHPUnit\Framework\Attributes\Group('DB')] -class FilesystemTest extends \Test\TestCase { - public const TEST_FILESYSTEM_USER1 = 'test-filesystem-user1'; - public const TEST_FILESYSTEM_USER2 = 'test-filesystem-user1'; - - /** - * @var array tmpDirs - */ - private $tmpDirs = []; - - /** - * @return array - */ - private function getStorageData() { - $dir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->tmpDirs[] = $dir; - return ['datadir' => $dir]; - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $userBackend = new \Test\Util\User\Dummy(); - $userBackend->createUser(self::TEST_FILESYSTEM_USER1, self::TEST_FILESYSTEM_USER1); - $userBackend->createUser(self::TEST_FILESYSTEM_USER2, self::TEST_FILESYSTEM_USER2); - Server::get(IUserManager::class)->registerBackend($userBackend); - $this->loginAsUser(); - } - - #[\Override] - protected function tearDown(): void { - foreach ($this->tmpDirs as $dir) { - Files::rmdirr($dir); - } - - $this->logout(); - $this->invokePrivate('\OC\Files\Filesystem', 'normalizedPathCache', [null]); - parent::tearDown(); - } - - public function testMount(): void { - Filesystem::mount('\OC\Files\Storage\Local', self::getStorageData(), '/'); - $this->assertEquals('/', Filesystem::getMountPoint('/')); - $this->assertEquals('/', Filesystem::getMountPoint('/some/folder')); - [, $internalPath] = Filesystem::resolvePath('/'); - $this->assertEquals('', $internalPath); - [, $internalPath] = Filesystem::resolvePath('/some/folder'); - $this->assertEquals('some/folder', $internalPath); - - Filesystem::mount('\OC\Files\Storage\Local', self::getStorageData(), '/some'); - $this->assertEquals('/', Filesystem::getMountPoint('/')); - $this->assertEquals('/some/', Filesystem::getMountPoint('/some/folder')); - $this->assertEquals('/some/', Filesystem::getMountPoint('/some/')); - $this->assertEquals('/some/', Filesystem::getMountPoint('/some')); - [, $internalPath] = Filesystem::resolvePath('/some/folder'); - $this->assertEquals('folder', $internalPath); - } - - public static function normalizePathData(): array { - return [ - ['/', ''], - ['/', '/'], - ['/', '//'], - ['/', '/', false], - ['/', '//', false], - - ['/path', '/path/'], - ['/path/', '/path/', false], - ['/path', 'path'], - - ['/foo/bar', '/foo//bar/'], - ['/foo/bar/', '/foo//bar/', false], - ['/foo/bar', '/foo////bar'], - ['/foo/bar', '/foo/////bar'], - ['/foo/bar', '/foo/bar/.'], - ['/foo/bar', '/foo/bar/./'], - ['/foo/bar/', '/foo/bar/./', false], - ['/foo/bar', '/foo/bar/./.'], - ['/foo/bar', '/foo/bar/././'], - ['/foo/bar/', '/foo/bar/././', false], - ['/foo/bar', '/foo/./bar/'], - ['/foo/bar/', '/foo/./bar/', false], - ['/foo/.bar', '/foo/.bar/'], - ['/foo/.bar/', '/foo/.bar/', false], - ['/foo/.bar/tee', '/foo/.bar/tee'], - ['/foo/bar.', '/foo/bar./'], - ['/foo/bar./', '/foo/bar./', false], - ['/foo/bar./tee', '/foo/bar./tee'], - ['/foo/.bar.', '/foo/.bar./'], - ['/foo/.bar./', '/foo/.bar./', false], - ['/foo/.bar./tee', '/foo/.bar./tee'], - - ['/foo/bar', '/.////././//./foo/.///././//./bar/././/./.'], - ['/foo/bar/', '/.////././//./foo/.///././//./bar/./././.', false], - ['/foo/bar', '/.////././//./foo/.///././//./bar/././/././'], - ['/foo/bar/', '/.////././//./foo/.///././//./bar/././/././', false], - ['/foo/.bar', '/.////././//./foo/./././/./.bar/././/././'], - ['/foo/.bar/', '/.////././//./foo/./././/./.bar/././/././', false], - ['/foo/.bar/tee./', '/.////././//./foo/./././/./.bar/tee././/././', false], - ['/foo/bar.', '/.////././//./foo/./././/./bar./././/././'], - ['/foo/bar./', '/.////././//./foo/./././/./bar./././/././', false], - ['/foo/bar./tee./', '/.////././//./foo/./././/./bar./tee././/././', false], - ['/foo/.bar.', '/.////././//./foo/./././/./.bar./././/././'], - ['/foo/.bar./', '/.////././//./foo/./././/./.bar./././././', false], - ['/foo/.bar./tee./', '/.////././//./foo/./././/./.bar./tee././././', false], - - // Windows paths - ['/', ''], - ['/', '\\'], - ['/', '\\', false], - ['/', '\\\\'], - ['/', '\\\\', false], - - ['/path', '\\path'], - ['/path', '\\path', false], - ['/path', '\\path\\'], - ['/path/', '\\path\\', false], - - ['/foo/bar', '\\foo\\\\bar\\'], - ['/foo/bar/', '\\foo\\\\bar\\', false], - ['/foo/bar', '\\foo\\\\\\\\bar'], - ['/foo/bar', '\\foo\\\\\\\\\\bar'], - ['/foo/bar', '\\foo\\bar\\.'], - ['/foo/bar', '\\foo\\bar\\.\\'], - ['/foo/bar/', '\\foo\\bar\\.\\', false], - ['/foo/bar', '\\foo\\bar\\.\\.'], - ['/foo/bar', '\\foo\\bar\\.\\.\\'], - ['/foo/bar/', '\\foo\\bar\\.\\.\\', false], - ['/foo/bar', '\\foo\\.\\bar\\'], - ['/foo/bar/', '\\foo\\.\\bar\\', false], - ['/foo/.bar', '\\foo\\.bar\\'], - ['/foo/.bar/', '\\foo\\.bar\\', false], - ['/foo/.bar/tee', '\\foo\\.bar\\tee'], - - // Absolute windows paths NOT marked as absolute - ['/C:', 'C:\\'], - ['/C:/', 'C:\\', false], - ['/C:/tests', 'C:\\tests'], - ['/C:/tests', 'C:\\tests', false], - ['/C:/tests', 'C:\\tests\\'], - ['/C:/tests/', 'C:\\tests\\', false], - ['/C:/tests/bar', 'C:\\tests\\.\\.\\bar'], - ['/C:/tests/bar/', 'C:\\tests\\.\\.\\bar\\.\\', false], - - // normalize does not resolve '..' (by design) - ['/foo/..', '/foo/../'], - ['/foo/../bar', '/foo/../bar/.'], - ['/foo/..', '\\foo\\..\\'], - ['/foo/../bar', '\\foo\\..\\bar'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('normalizePathData')] - public function testNormalizePath($expected, $path, $stripTrailingSlash = true): void { - $this->assertEquals($expected, Filesystem::normalizePath($path, $stripTrailingSlash)); - } - - public static function normalizePathKeepUnicodeData(): array { - $nfdName = 'ümlaut'; - $nfcName = 'ümlaut'; - return [ - ['/' . $nfcName, $nfcName, true], - ['/' . $nfcName, $nfcName, false], - ['/' . $nfdName, $nfdName, true], - ['/' . $nfcName, $nfdName, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('normalizePathKeepUnicodeData')] - public function testNormalizePathKeepUnicode($expected, $path, $keepUnicode = false): void { - $this->assertEquals($expected, Filesystem::normalizePath($path, true, false, $keepUnicode)); - } - - public function testNormalizePathKeepUnicodeCache(): void { - $nfdName = 'ümlaut'; - $nfcName = 'ümlaut'; - // call in succession due to cache - $this->assertEquals('/' . $nfcName, Filesystem::normalizePath($nfdName, true, false, false)); - $this->assertEquals('/' . $nfdName, Filesystem::normalizePath($nfdName, true, false, true)); - } - - public static function isValidPathData(): array { - return [ - ['/', true], - ['/path', true], - ['/foo/bar', true], - ['/foo//bar/', true], - ['/foo////bar', true], - ['/foo//\///bar', true], - ['/foo/bar/.', true], - ['/foo/bar/./', true], - ['/foo/bar/./.', true], - ['/foo/bar/././', true], - ['/foo/bar/././..bar', true], - ['/foo/bar/././..bar/a', true], - ['/foo/bar/././..', false], - ['/foo/bar/././../', false], - ['/foo/bar/.././', false], - ['/foo/bar/../../', false], - ['/foo/bar/../..\\', false], - ['..', false], - ['../', false], - ['../foo/bar', false], - ['..\foo/bar', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('isValidPathData')] - public function testIsValidPath($path, $expected): void { - $this->assertSame($expected, Filesystem::isValidPath($path)); - } - - public static function isFileBlacklistedData(): array { - return [ - ['/etc/foo/bar/foo.txt', false], - ['\etc\foo/bar\foo.txt', false], - ['.htaccess', true], - ['.htaccess/', true], - ['.htaccess\\', true], - ['/etc/foo\bar/.htaccess\\', true], - ['/etc/foo\bar/.htaccess/', true], - ['/etc/foo\bar/.htaccess/foo', false], - ['//foo//bar/\.htaccess/', true], - ['\foo\bar\.HTAccess', true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('isFileBlacklistedData')] - public function testIsFileBlacklisted($path, $expected): void { - $this->assertSame($expected, Filesystem::isFileBlacklisted($path)); - } - - public function testNormalizePathUTF8(): void { - if (!class_exists('Patchwork\PHP\Shim\Normalizer')) { - $this->markTestSkipped('UTF8 normalizer Patchwork was not found'); - } - - $this->assertEquals("/foo/bar\xC3\xBC", Filesystem::normalizePath("/foo/baru\xCC\x88")); - $this->assertEquals("/foo/bar\xC3\xBC", Filesystem::normalizePath("\\foo\\baru\xCC\x88")); - } - - public function testHooks(): void { - if (Filesystem::getView()) { - $user = \OC_User::getUser(); - } else { - $user = self::TEST_FILESYSTEM_USER1; - $backend = new \Test\Util\User\Dummy(); - Server::get(IUserManager::class)->registerBackend($backend); - $backend->createUser($user, $user); - $userObj = Server::get(IUserManager::class)->get($user); - Server::get(IUserSession::class)->setUser($userObj); - Filesystem::init($user, '/' . $user . '/files'); - } - \OC_Hook::clear('OC_Filesystem'); - \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook'); - - Filesystem::mount('OC\Files\Storage\Temporary', [], '/'); - - $rootView = new View(''); - $rootView->mkdir('/' . $user); - $rootView->mkdir('/' . $user . '/files'); - - // \OC\Files\Filesystem::file_put_contents('/foo', 'foo'); - Filesystem::mkdir('/bar'); - // \OC\Files\Filesystem::file_put_contents('/bar//foo', 'foo'); - - $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); - file_put_contents($tmpFile, 'foo'); - $fh = fopen($tmpFile, 'r'); - // \OC\Files\Filesystem::file_put_contents('/bar//foo', $fh); - } - - /** - * Tests that an exception is thrown when passed user does not exist. - * - */ - public function testLocalMountWhenUserDoesNotExist(): void { - $this->expectException(UserNotFoundException::class); - - $userId = $this->getUniqueID('user_'); - - Filesystem::initMountPoints($userId); - } - - public function testNullUserThrows(): void { - $this->expectException(UserNotFoundException::class); - - Filesystem::initMountPoints(null); - } - - public function testNullUserThrowsTwice(): void { - $thrown = 0; - try { - Filesystem::initMountPoints(null); - } catch (UserNotFoundException $e) { - $thrown++; - } - try { - Filesystem::initMountPoints(null); - } catch (UserNotFoundException $e) { - $thrown++; - } - $this->assertEquals(2, $thrown); - } - - /** - * Tests that an exception is thrown when passed user does not exist. - */ - public function testLocalMountWhenUserDoesNotExistTwice(): void { - $thrown = 0; - $userId = $this->getUniqueID('user_'); - - try { - Filesystem::initMountPoints($userId); - } catch (UserNotFoundException $e) { - $thrown++; - } - - try { - Filesystem::initMountPoints($userId); - } catch (UserNotFoundException $e) { - $thrown++; - } - - $this->assertEquals(2, $thrown); - } - - /** - * Tests that the home storage is used for the user's mount point - */ - public function testHomeMount(): void { - $userId = $this->getUniqueID('user_'); - - Server::get(IUserManager::class)->createUser($userId, $userId); - - Filesystem::initMountPoints($userId); - - $homeMount = Filesystem::getStorage('/' . $userId . '/'); - - $this->assertTrue($homeMount->instanceOfStorage('\OCP\Files\IHomeStorage')); - if ($homeMount->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage')) { - $this->assertEquals('object::user:' . $userId, $homeMount->getId()); - } elseif ($homeMount->instanceOfStorage('\OC\Files\Storage\Home')) { - $this->assertEquals('home::' . $userId, $homeMount->getId()); - } - - $user = Server::get(IUserManager::class)->get($userId); - if ($user !== null) { - $user->delete(); - } - } - - public function dummyHook($arguments) { - $path = $arguments['path']; - $this->assertEquals($path, Filesystem::normalizePath($path)); //the path passed to the hook should already be normalized - } - - /** - * Test that the default cache dir is part of the user's home - */ - public function testMountDefaultCacheDir(): void { - $userId = $this->getUniqueID('user_'); - $config = Server::get(IConfig::class); - $oldCachePath = $config->getSystemValueString('cache_path', ''); - // no cache path configured - $config->setSystemValue('cache_path', ''); - - Server::get(IUserManager::class)->createUser($userId, $userId); - Filesystem::initMountPoints($userId); - - $this->assertEquals( - '/' . $userId . '/', - Filesystem::getMountPoint('/' . $userId . '/cache') - ); - [$storage, $internalPath] = Filesystem::resolvePath('/' . $userId . '/cache'); - $this->assertTrue($storage->instanceOfStorage('\OCP\Files\IHomeStorage')); - $this->assertEquals('cache', $internalPath); - $user = Server::get(IUserManager::class)->get($userId); - if ($user !== null) { - $user->delete(); - } - - $config->setSystemValue('cache_path', $oldCachePath); - } - - /** - * Test that an external cache is mounted into - * the user's home - */ - public function testMountExternalCacheDir(): void { - $userId = $this->getUniqueID('user_'); - - $config = Server::get(IConfig::class); - $oldCachePath = $config->getSystemValueString('cache_path', ''); - // set cache path to temp dir - $cachePath = Server::get(ITempManager::class)->getTemporaryFolder() . '/extcache'; - $config->setSystemValue('cache_path', $cachePath); - - Server::get(IUserManager::class)->createUser($userId, $userId); - Filesystem::initMountPoints($userId); - - $this->assertEquals( - '/' . $userId . '/cache/', - Filesystem::getMountPoint('/' . $userId . '/cache') - ); - [$storage, $internalPath] = Filesystem::resolvePath('/' . $userId . '/cache'); - $this->assertTrue($storage->instanceOfStorage('\OC\Files\Storage\Local')); - $this->assertEquals('', $internalPath); - $user = Server::get(IUserManager::class)->get($userId); - if ($user !== null) { - $user->delete(); - } - - $config->setSystemValue('cache_path', $oldCachePath); - } - - public function testRegisterMountProviderAfterSetup(): void { - Filesystem::initMountPoints(self::TEST_FILESYSTEM_USER2); - $this->assertEquals('/', Filesystem::getMountPoint('/foo/bar')); - $mount = new MountPoint(new Temporary([]), '/foo/bar'); - $mountProvider = new DummyMountProvider([self::TEST_FILESYSTEM_USER2 => [$mount]]); - Server::get(IMountProviderCollection::class)->registerProvider($mountProvider); - $this->assertEquals('/foo/bar/', Filesystem::getMountPoint('/foo/bar')); - } -} --- a/tests/lib/Files/Mount/CacheMountProviderTest.php +++ /dev/null @@ -1,94 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->user = $this->createMock(IUser::class); - $this->storageFactory = new StorageFactory(); - stream_wrapper_register('cachemountprovidertest', CacheMountProviderTestStream::class); - } - - #[\Override] - protected function tearDown(): void { - stream_wrapper_unregister('cachemountprovidertest'); - } - - public function testGetMountsForUser(): void { - $provider = new CacheMountProvider($this->config); - - $this->assertCount(0, $provider->getMountsForUser($this->user, $this->storageFactory)); - } - - public function testGetMountsForUserCacheDir(): void { - $this->config->expects($this->exactly(1)) - ->method('getSystemValueString') - ->willReturnMap([ - ['cache_path', '', 'cachemountprovidertest:////cache_path'], - ]); - $this->user->method('getUID') - ->willReturn('bob'); - - $provider = new CacheMountProvider($this->config); - $mounts = $provider->getMountsForUser($this->user, $this->storageFactory); - - $this->assertCount(2, $mounts); - $this->assertEquals(1, CacheMountProviderTestStream::$statCounter); - $this->assertEquals(2, CacheMountProviderTestStream::$mkdirCounter); - - $cacheMountProvider = $mounts[0]; - $this->assertEquals('/bob/cache/', $cacheMountProvider->getMountPoint()); - - $cacheStorage = $cacheMountProvider->getStorage(); - $this->assertEquals('local::cachemountprovidertest://cache_path/bob/', $cacheStorage->getId()); - - $uploadsMountProvider = $mounts[1]; - $this->assertEquals('/bob/uploads/', $uploadsMountProvider->getMountPoint()); - - $uploadsStorage = $uploadsMountProvider->getStorage(); - $this->assertEquals('local::cachemountprovidertest://cache_path/bob/uploads/', $uploadsStorage->getId()); - - $cacheStorage->mkdir('foobar'); - $this->assertEquals(3, CacheMountProviderTestStream::$mkdirCounter); - - $uploadsStorage->mkdir('foobar'); - $this->assertEquals(4, CacheMountProviderTestStream::$mkdirCounter); - } -} --- a/tests/lib/Files/Mount/ManagerTest.php +++ /dev/null @@ -1,88 +0,0 @@ -manager = new \OC\Files\Mount\Manager($this->createMock(SetupManagerFactory::class)); - } - - public function testFind(): void { - $rootMount = new MountPoint(new Temporary([]), '/'); - $this->manager->addMount($rootMount); - $this->assertEquals($rootMount, $this->manager->find('/')); - $this->assertEquals($rootMount, $this->manager->find('/foo/bar')); - - $storage = new Temporary([]); - $mount1 = new MountPoint($storage, '/foo'); - $this->manager->addMount($mount1); - $this->assertEquals($rootMount, $this->manager->find('/')); - $this->assertEquals($mount1, $this->manager->find('/foo/bar')); - - $this->assertEquals(1, count($this->manager->findIn('/'))); - $mount2 = new MountPoint(new Temporary([]), '/bar'); - $this->manager->addMount($mount2); - $this->assertEquals(2, count($this->manager->findIn('/'))); - - $id = $mount1->getStorageId(); - $this->assertEquals([$mount1], $this->manager->findByStorageId($id)); - - $mount3 = new MountPoint($storage, '/foo/bar'); - $this->manager->addMount($mount3); - $this->assertEquals([$mount1, $mount3], $this->manager->findByStorageId($id)); - } - - public function testBinarySearch(): void { - $sortedArray = [ - 'a' => false, - 'aa' => false, - 'b' => false, - 'bb' => false, - 'bba' => true, - 'bbb' => true, - 'bdc' => false, - ]; - $sortedKey = array_keys($sortedArray); - $result = $this->invokePrivate($this->manager, 'binarySearch', [$sortedArray, $sortedKey, 'bb']); - $this->assertEquals(2, count($result)); - foreach ($result as $entry) { - $this->assertTrue($entry); - } - } - - public function testLong(): void { - $storage = new LongId([]); - $mount = new MountPoint($storage, '/foo'); - $this->manager->addMount($mount); - - $id = $mount->getStorageId(); - $storageId = $storage->getId(); - $this->assertEquals([$mount], $this->manager->findByStorageId($id)); - $this->assertEquals([$mount], $this->manager->findByStorageId($storageId)); - $this->assertEquals([$mount], $this->manager->findByStorageId(md5($storageId))); - } -} --- a/tests/lib/Files/Mount/MountPointTest.php +++ /dev/null @@ -1,77 +0,0 @@ -createMock(IStorage::class); - $storage->expects($this->once()) - ->method('getId') - ->willReturn(123); - - $loader = $this->createMock(StorageFactory::class); - $loader->expects($this->once()) - ->method('wrap') - ->willReturn($storage); - - $mountPoint = new MountPoint( - // just use this because a real class is needed - NullStorage::class, - '/mountpoint', - null, - $loader - ); - - $this->assertEquals($storage, $mountPoint->getStorage()); - $this->assertEquals(123, $mountPoint->getStorageId()); - $this->assertEquals('/mountpoint/', $mountPoint->getMountPoint()); - - $mountPoint->setMountPoint('another'); - $this->assertEquals('/another/', $mountPoint->getMountPoint()); - } - - public function testInvalidStorage(): void { - $loader = $this->createMock(StorageFactory::class); - $loader->expects($this->once()) - ->method('wrap') - ->willThrowException(new \Exception('Test storage init exception')); - - $called = false; - $wrapper = function ($mountPoint, $storage) use ($called): void { - $called = true; - }; - - $mountPoint = new MountPoint( - // just use this because a real class is needed - NullStorage::class, - '/mountpoint', - null, - $loader - ); - - $this->assertNull($mountPoint->getStorage()); - // call it again to make sure the init code only ran once - $this->assertNull($mountPoint->getStorage()); - - $this->assertNull($mountPoint->getStorageId()); - - // wrapping doesn't fail - $mountPoint->wrapStorage($wrapper); - - $this->assertNull($mountPoint->getStorage()); - - // storage wrapper never called - $this->assertFalse($called); - } -} --- a/tests/lib/Files/Mount/MountTest.php +++ /dev/null @@ -1,46 +0,0 @@ -getMockBuilder('\OC\Files\Storage\Temporary') - ->disableOriginalConstructor() - ->getMock(); - $mount = new MountPoint($storage, '/foo'); - $this->assertInstanceOf('\OC\Files\Storage\Temporary', $mount->getStorage()); - } - - public function testFromStorageClassname(): void { - $mount = new MountPoint('\OC\Files\Storage\Temporary', '/foo'); - $this->assertInstanceOf('\OC\Files\Storage\Temporary', $mount->getStorage()); - } - - public function testWrapper(): void { - $test = $this; - $wrapper = function ($mountPoint, $storage) use (&$test) { - $test->assertEquals('/foo/', $mountPoint); - $test->assertInstanceOf('\OC\Files\Storage\Storage', $storage); - return new Wrapper(['storage' => $storage]); - }; - - $loader = new StorageFactory(); - $loader->addStorageWrapper('test_wrapper', $wrapper); - - $storage = $this->getMockBuilder('\OC\Files\Storage\Temporary') - ->disableOriginalConstructor() - ->getMock(); - $mount = new MountPoint($storage, '/foo', [], $loader); - $this->assertInstanceOf('\OC\Files\Storage\Wrapper\Wrapper', $mount->getStorage()); - } -} --- a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php +++ /dev/null @@ -1,284 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->user = $this->createMock(IUser::class); - $this->loader = $this->createMock(IStorageFactory::class); - - $objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->createMock(IAppManager::class)); - $this->provider = new ObjectHomeMountProvider($objectStoreConfig); - } - - public function testSingleBucket(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'objectstore') { - return [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar' - ], - ]; - } else { - return $default; - } - }); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $arguments = $this->invokePrivate($mount, 'arguments'); - - $objectStore = $arguments['objectstore']; - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); - $this->assertEquals(['foo' => 'bar', 'multibucket' => false], $objectStore->getArguments()); - } - - public function testMultiBucket(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'objectstore_multibucket') { - return [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar' - ], - ]; - } else { - return $default; - } - }); - - $this->user->method('getUID') - ->willReturn('uid'); - $this->loader->expects($this->never())->method($this->anything()); - - $this->config->method('getUserValue') - ->willReturn(null); - - $this->config - ->method('setUserValue') - ->with( - $this->equalTo('uid'), - $this->equalTo('homeobjectstore'), - $this->equalTo('bucket'), - $this->equalTo('49'), - $this->equalTo(null) - ); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $arguments = $this->invokePrivate($mount, 'arguments'); - - $objectStore = $arguments['objectstore']; - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); - $this->assertEquals(['foo' => 'bar', 'bucket' => 49, 'multibucket' => true], $objectStore->getArguments()); - } - - public function testMultiBucketWithPrefix(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'objectstore_multibucket') { - return [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar', - 'bucket' => 'myBucketPrefix', - ], - ]; - } else { - return $default; - } - }); - - $this->user->method('getUID') - ->willReturn('uid'); - $this->loader->expects($this->never())->method($this->anything()); - - $this->config - ->method('getUserValue') - ->willReturn(null); - - $this->config->expects($this->once()) - ->method('setUserValue') - ->with( - $this->equalTo('uid'), - $this->equalTo('homeobjectstore'), - $this->equalTo('bucket'), - $this->equalTo('myBucketPrefix49'), - $this->equalTo(null) - ); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $arguments = $this->invokePrivate($mount, 'arguments'); - - $objectStore = $arguments['objectstore']; - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); - $this->assertEquals(['foo' => 'bar', 'bucket' => 'myBucketPrefix49', 'multibucket' => true], $objectStore->getArguments()); - } - - public function testMultiBucketBucketAlreadySet(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'objectstore_multibucket') { - return [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar', - 'bucket' => 'myBucketPrefix', - ], - ]; - } else { - return $default; - } - }); - - $this->user->method('getUID') - ->willReturn('uid'); - $this->loader->expects($this->never())->method($this->anything()); - - $this->config - ->method('getUserValue') - ->willReturnCallback(function ($uid, $app, $key, $default) { - if ($uid === 'uid' && $app === 'homeobjectstore' && $key === 'bucket') { - return 'awesomeBucket1'; - } else { - return $default; - } - }); - - $this->config->expects($this->never()) - ->method('setUserValue'); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $arguments = $this->invokePrivate($mount, 'arguments'); - - $objectStore = $arguments['objectstore']; - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); - $this->assertEquals(['foo' => 'bar', 'bucket' => 'awesomeBucket1', 'multibucket' => true], $objectStore->getArguments()); - } - - public function testMultiBucketConfigFirst(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'objectstore_multibucket') { - return [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar', - 'bucket' => 'myBucketPrefix', - ], - ]; - } else { - return $default; - } - }); - - $this->user->method('getUID') - ->willReturn('uid'); - $this->loader->expects($this->never())->method($this->anything()); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); - } - - public function testMultiBucketConfigFirstFallBackSingle(): void { - $this->config - ->method('getSystemValue')->willReturnMap([ - ['objectstore_multibucket', null, null], - ['objectstore', null, [ - 'class' => 'Test\Files\Mount\FakeObjectStore', - 'arguments' => [ - 'foo' => 'bar', - 'bucket' => 'myBucketPrefix', - ], - ]], - ]); - - $this->user->method('getUID') - ->willReturn('uid'); - $this->loader->expects($this->never())->method($this->anything()); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); - } - - public function testNoObjectStore(): void { - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); - $this->assertNull($mount); - } -} - -class FakeObjectStore implements IObjectStore { - public function __construct( - private array $arguments, - ) { - } - - public function getArguments() { - return $this->arguments; - } - - #[\Override] - public function getStorageId() { - } - - #[\Override] - public function readObject($urn) { - } - - #[\Override] - public function writeObject($urn, $stream, ?string $mimetype = null) { - } - - #[\Override] - public function deleteObject($urn) { - } - - #[\Override] - public function objectExists($urn) { - } - - #[\Override] - public function copyObject($from, $to) { - } - - #[\Override] - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { - return null; - } -} --- a/tests/lib/Files/Mount/RootMountProviderTest.php +++ /dev/null @@ -1,124 +0,0 @@ -loader = new StorageFactory(); - } - - private function getConfig(array $systemConfig): IConfig { - $config = $this->createMock(IConfig::class); - $config->method('getSystemValue') - ->willReturnCallback(function (string $key, $default) use ($systemConfig) { - return $systemConfig[$key] ?? $default; - }); - return $config; - } - - private function getProvider(array $systemConfig): RootMountProvider { - $config = $this->getConfig($systemConfig); - $objectStoreConfig = new PrimaryObjectStoreConfig($config, $this->createMock(IAppManager::class)); - return new RootMountProvider($objectStoreConfig, $config); - } - - public function testLocal(): void { - $provider = $this->getProvider([ - 'datadirectory' => '/data', - ]); - $mounts = $provider->getRootMounts($this->loader); - $this->assertCount(1, $mounts); - $mount = $mounts[0]; - $this->assertEquals('/', $mount->getMountPoint()); - /** @var LocalRootStorage $storage */ - $storage = $mount->getStorage(); - $this->assertInstanceOf(LocalRootStorage::class, $storage); - $this->assertEquals('/data/', $storage->getSourcePath('')); - } - - public function testObjectStore(): void { - $provider = $this->getProvider([ - 'objectstore' => [ - 'class' => "OC\Files\ObjectStore\S3", - 'arguments' => [ - 'bucket' => 'nextcloud', - 'autocreate' => true, - 'key' => 'minio', - 'secret' => 'minio123', - 'hostname' => 'localhost', - 'port' => 9000, - 'use_ssl' => false, - 'use_path_style' => true, - 'uploadPartSize' => 52428800, - ], - ], - ]); - $mounts = $provider->getRootMounts($this->loader); - $this->assertCount(1, $mounts); - $mount = $mounts[0]; - $this->assertEquals('/', $mount->getMountPoint()); - /** @var ObjectStoreStorage $storage */ - $storage = $mount->getStorage(); - $this->assertInstanceOf(ObjectStoreStorage::class, $storage); - - $class = new \ReflectionClass($storage); - $prop = $class->getProperty('objectStore'); - /** @var S3 $objectStore */ - $objectStore = $prop->getValue($storage); - $this->assertEquals('nextcloud', $objectStore->getBucket()); - } - - public function testObjectStoreMultiBucket(): void { - $provider = $this->getProvider([ - 'objectstore_multibucket' => [ - 'class' => "OC\Files\ObjectStore\S3", - 'arguments' => [ - 'bucket' => 'nextcloud', - 'autocreate' => true, - 'key' => 'minio', - 'secret' => 'minio123', - 'hostname' => 'localhost', - 'port' => 9000, - 'use_ssl' => false, - 'use_path_style' => true, - 'uploadPartSize' => 52428800, - ], - ], - ]); - $mounts = $provider->getRootMounts($this->loader); - $this->assertCount(1, $mounts); - $mount = $mounts[0]; - $this->assertEquals('/', $mount->getMountPoint()); - /** @var ObjectStoreStorage $storage */ - $storage = $mount->getStorage(); - $this->assertInstanceOf(ObjectStoreStorage::class, $storage); - - $class = new \ReflectionClass($storage); - $prop = $class->getProperty('objectStore'); - /** @var S3 $objectStore */ - $objectStore = $prop->getValue($storage); - $this->assertEquals('nextcloud0', $objectStore->getBucket()); - } -} --- a/tests/lib/Files/Node/FileTest.php +++ /dev/null @@ -1,318 +0,0 @@ -getFileInfo($data, $internalPath, $storage)); - } else { - return new File($root, $view, $path); - } - } - - #[\Override] - protected function getNodeClass(): string { - return File::class; - } - - #[\Override] - protected function getNonExistingNodeClass(): string { - return NonExistingFile::class; - } - - #[\Override] - protected function getViewDeleteMethod(): string { - return 'unlink'; - } - - public function testGetContent(): void { - /** @var Root|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $hook = function ($file): void { - throw new \Exception('Hooks are not supposed to be called'); - }; - - $root->listen('\OC\Files', 'preWrite', $hook); - $root->listen('\OC\Files', 'postWrite', $hook); - - $this->view->expects($this->once()) - ->method('file_get_contents') - ->with('/bar/foo') - ->willReturn('bar'); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = new File($root, $this->view, '/bar/foo'); - $this->assertEquals('bar', $node->getContent()); - } - - public function testGetContentNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - /** @var Root|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => 0])); - - $node = new File($root, $this->view, '/bar/foo'); - $node->getContent(); - } - - public function testPutContent(): void { - /** @var Root|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $this->view->expects($this->once()) - ->method('file_put_contents') - ->with('/bar/foo', 'bar') - ->willReturn(true); - - $node = new File($root, $this->view, '/bar/foo'); - $node->putContent('bar'); - } - - public function testPutContentNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - /** @var Root|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = new File($root, $this->view, '/bar/foo'); - $node->putContent('bar'); - } - - public function testGetMimeType(): void { - /** @var Root|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['mimetype' => 'text/plain'])); - - $node = new File($root, $this->view, '/bar/foo'); - $this->assertEquals('text/plain', $node->getMimeType()); - } - - public function testFOpenRead(): void { - $stream = fopen('php://memory', 'w+'); - fwrite($stream, 'bar'); - rewind($stream); - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - - $hook = function ($file): void { - throw new \Exception('Hooks are not supposed to be called'); - }; - - $root->listen('\OC\Files', 'preWrite', $hook); - $root->listen('\OC\Files', 'postWrite', $hook); - - $this->view->expects($this->once()) - ->method('fopen') - ->with('/bar/foo', 'r') - ->willReturn($stream); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $node = new File($root, $this->view, '/bar/foo'); - $fh = $node->fopen('r'); - $this->assertEquals($stream, $fh); - $this->assertEquals('bar', fread($fh, 3)); - } - - public function testFOpenWrite(): void { - $stream = fopen('php://memory', 'w+'); - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $hooksCalled = 0; - $hook = function ($file) use (&$hooksCalled): void { - $hooksCalled++; - }; - - $root->listen('\OC\Files', 'preWrite', $hook); - $root->listen('\OC\Files', 'postWrite', $hook); - - $this->view->expects($this->once()) - ->method('fopen') - ->with('/bar/foo', 'w') - ->willReturn($stream); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $node = new File($root, $this->view, '/bar/foo'); - $fh = $node->fopen('w'); - $this->assertEquals($stream, $fh); - fwrite($fh, 'bar'); - rewind($fh); - $this->assertEquals('bar', fread($stream, 3)); - $this->assertEquals(2, $hooksCalled); - } - - public function testFOpenReadNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $hook = function ($file): void { - throw new \Exception('Hooks are not supposed to be called'); - }; - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => 0])); - - $node = new File($root, $this->view, '/bar/foo'); - $node->fopen('r'); - } - - public function testFOpenReadWriteNoReadPermissions(): void { - $this->expectException(NotPermittedException::class); - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $hook = function (): void { - throw new \Exception('Hooks are not supposed to be called'); - }; - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_UPDATE])); - - $node = new File($root, $this->view, '/bar/foo'); - $node->fopen('w'); - } - - public function testFOpenReadWriteNoWritePermissions(): void { - $this->expectException(NotPermittedException::class); - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $hook = function (): void { - throw new \Exception('Hooks are not supposed to be called'); - }; - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = new File($root, $this->view, '/bar/foo'); - $node->fopen('w'); - } -} --- a/tests/lib/Files/Node/FolderTest.php +++ /dev/null @@ -1,1253 +0,0 @@ -createUser('bar', 'bar'); - } - - #[\Override] - protected function createTestNode(IRootFolder $root, View&MockObject $view, string $path, array $data = [], string $internalPath = '', ?IStorage $storage = null): Folder { - $view->expects($this->any()) - ->method('getRoot') - ->willReturn(''); - if ($data || $internalPath || $storage) { - return new Folder($root, $view, $path, $this->getFileInfo($data, $internalPath, $storage)); - } else { - return new Folder($root, $view, $path); - } - } - - #[\Override] - protected function getNodeClass(): string { - return Folder::class; - } - - #[\Override] - protected function getNonExistingNodeClass(): string { - return NonExistingFolder::class; - } - - #[\Override] - protected function getViewDeleteMethod(): string { - return 'rmdir'; - } - - public function testGetDirectoryContent(): void { - $manager = $this->createMock(Manager::class); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->any()) - ->method('getDirectoryContent') - ->with('/bar/foo') - ->willReturn([ - new FileInfo('/bar/foo/asd', null, 'foo/asd', ['fileid' => 2, 'path' => '/bar/foo/asd', 'name' => 'asd', 'size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain'], null), - new FileInfo('/bar/foo/qwerty', null, 'foo/qwerty', ['fileid' => 3, 'path' => '/bar/foo/qwerty', 'name' => 'qwerty', 'size' => 200, 'mtime' => 55, 'mimetype' => 'httpd/unix-directory'], null), - ]); - $this->view->method('getFileInfo') - ->willReturn($this->createMock(FileInfo::class)); - $this->view->method('getRelativePath') - ->willReturn('/bar/foo'); - - $node = new Folder($root, $this->view, '/bar/foo'); - $children = $node->getDirectoryListing(); - $this->assertEquals(2, count($children)); - $this->assertInstanceOf('\OC\Files\Node\File', $children[0]); - $this->assertInstanceOf('\OC\Files\Node\Folder', $children[1]); - $this->assertEquals('asd', $children[0]->getName()); - $this->assertEquals('qwerty', $children[1]->getName()); - $this->assertEquals(2, $children[0]->getId()); - $this->assertEquals(3, $children[1]->getId()); - } - - public function testGet(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $node = new File($root, $view, '/bar/foo/asd'); - $root->method('get') - ->with('/bar/foo/asd') - ->willReturn($node); - - $parentNode = new Folder($root, $view, '/bar/foo'); - self::assertEquals($node, $parentNode->get('asd')); - } - - public function testNodeExists(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $child = new Folder($root, $view, '/bar/foo/asd'); - - $root->method('get') - ->with('/bar/foo/asd') - ->willReturn($child); - - $node = new Folder($root, $view, '/bar/foo'); - $this->assertTrue($node->nodeExists('asd')); - } - - public function testNodeExistsNotExists(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $root->method('get') - ->with('/bar/foo/asd') - ->willThrowException(new NotFoundException()); - - $node = new Folder($root, $view, '/bar/foo'); - $this->assertFalse($node->nodeExists('asd')); - } - - public function testNewFolder(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $view->method('mkdir') - ->with('/bar/foo/asd') - ->willReturn(true); - - $node = new Folder($root, $view, '/bar/foo'); - $child = new Folder($root, $view, '/bar/foo/asd', null, $node); - $result = $node->newFolder('asd'); - $this->assertEquals($child, $result); - } - - public function testNewFolderDeepParent(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->with('/foobar') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $view->method('mkdir') - ->with('/foobar/asd/sdf') - ->willReturn(true); - - $node = new Folder($root, $view, '/foobar'); - $child = new Folder($root, $view, '/foobar/asd/sdf', null, null); - $result = $node->newFolder('asd/sdf'); - $this->assertEquals($child, $result); - } - - public function testNewFolderNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = new Folder($root, $view, '/bar/foo'); - $node->newFolder('asd'); - } - - public function testNewFile(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $view->method('touch') - ->with('/bar/foo/asd') - ->willReturn(true); - - $node = new Folder($root, $view, '/bar/foo'); - $child = new File($root, $view, '/bar/foo/asd', null, $node); - $result = $node->newFile('asd'); - $this->assertEquals($child, $result); - } - - public function testNewFileNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = new Folder($root, $view, '/bar/foo'); - $node->newFile('asd'); - } - - public function testGetFreeSpace(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->method('getUser') - ->willReturn($this->user); - - $view->method('free_space') - ->with('/bar/foo') - ->willReturn(100); - - $node = new Folder($root, $view, '/bar/foo'); - $this->assertEquals(100, $node->getFreeSpace()); - } - - public function testSearch(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->method('getUser') - ->willReturn($this->user); - $storage = $this->createMock(IStorage::class); - $storage->method('getId')->willReturn('test::1'); - $cache = new Cache($storage); - - $storage->method('getCache') - ->willReturn($cache); - - $storage->expects($this->atLeastOnce()) - ->method('getOwner') - ->with('qwerty') - ->willReturn(false); - - $mount = $this->createMock(IMountPoint::class); - $mount->expects($this->atLeastOnce()) - ->method('getStorage') - ->willReturn($storage); - $mount->expects($this->atLeastOnce()) - ->method('getInternalPath') - ->willReturn('foo'); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo', ['size' => 200, 'mtime' => 55, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo/qwerty', ['size' => 200, 'mtime' => 55, 'mimetype' => 'text/plain']); - - $root->method('getMountsIn') - ->with('/bar/foo') - ->willReturn([]); - - $root->method('getMount') - ->with('/bar/foo') - ->willReturn($mount); - - $node = new Folder($root, $view, '/bar/foo'); - $result = $node->search('qw'); - $cache->clear(); - $this->assertEquals(1, count($result)); - $this->assertEquals('/bar/foo/qwerty', $result[0]->getPath()); - } - - public function testSearchInRoot(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - $storage = $this->createMock(IStorage::class); - $storage->method('getId')->willReturn('test::2'); - $cache = new Cache($storage); - - $mount = $this->createMock(IMountPoint::class); - $mount->method('getStorage') - ->willReturn($storage); - $mount->method('getInternalPath') - ->willReturn('files'); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('files', ['size' => 200, 'mtime' => 55, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('files/foo', ['size' => 200, 'mtime' => 55, 'mimetype' => 'text/plain']); - - $root->method('getMountsIn') - ->with('') - ->willReturn([]); - - $root->method('getMount') - ->with('') - ->willReturn($mount); - - $result = $root->search('foo'); - $cache->clear(); - $this->assertEquals(1, count($result)); - $this->assertEquals('/foo', $result[0]->getPath()); - } - - public function testSearchInStorageRoot(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->method('getUser') - ->willReturn($this->user); - $storage = $this->createMock(IStorage::class); - $storage->method('getId')->willReturn('test::1'); - $cache = new Cache($storage); - - $mount = $this->createMock(IMountPoint::class); - $mount->method('getStorage') - ->willReturn($storage); - $mount->method('getInternalPath') - ->willReturn(''); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo', ['size' => 200, 'mtime' => 55, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo/qwerty', ['size' => 200, 'mtime' => 55, 'mimetype' => 'text/plain']); - - $root->method('getMountsIn') - ->with('/bar') - ->willReturn([]); - - $root->method('getMount') - ->with('/bar') - ->willReturn($mount); - - $node = new Folder($root, $view, '/bar'); - $result = $node->search('qw'); - $cache->clear(); - $this->assertEquals(1, count($result)); - $this->assertEquals('/bar/foo/qwerty', $result[0]->getPath()); - } - - public function testSearchSubStorages(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - $storage = $this->createMock(IStorage::class); - $storage->method('getId')->willReturn('test::1'); - $cache = new Cache($storage); - $subStorage = $this->createMock(IStorage::class); - $subStorage->method('getId')->willReturn('test::2'); - $subCache = new Cache($subStorage); - $subMount = $this->getMockBuilder(MountPoint::class)->setConstructorArgs([Temporary::class, ''])->getMock(); - - $mount = $this->createMock(IMountPoint::class); - $mount->method('getStorage') - ->willReturn($storage); - $mount->method('getInternalPath') - ->willReturn('foo'); - - $subMount->method('getStorage') - ->willReturn($subStorage); - - $subMount->method('getMountPoint') - ->willReturn('/bar/foo/bar/'); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $subStorage->method('getCache') - ->willReturn($subCache); - $subStorage->method('getOwner') - ->willReturn('owner'); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo', ['size' => 200, 'mtime' => 55, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo/qwerty', ['size' => 200, 'mtime' => 55, 'mimetype' => 'text/plain']); - - $subCache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $subCache->insert('asd', ['size' => 200, 'mtime' => 55, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $subCache->insert('asd/qwerty', ['size' => 200, 'mtime' => 55, 'mimetype' => 'text/plain']); - - $root->method('getMountsIn') - ->with('/bar/foo') - ->willReturn([$subMount]); - - $root->method('getMount') - ->with('/bar/foo') - ->willReturn($mount); - - $node = new Folder($root, $view, '/bar/foo'); - $result = $node->search('qw'); - $cache->clear(); - $subCache->clear(); - $this->assertEquals(2, count($result)); - } - - public function testIsSubNode(): void { - $rootFolderMock = $this->createMock(IRootFolder::class); - $file = new Node($rootFolderMock, $this->view, '/foo/bar'); - $folder = new Folder($rootFolderMock, $this->view, '/foo'); - $this->assertTrue($folder->isSubNode($file)); - $this->assertFalse($folder->isSubNode($folder)); - - $file = new Node($rootFolderMock, $this->view, '/foobar'); - $this->assertFalse($folder->isSubNode($file)); - } - - public function testGetById(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $storage = $this->createMock(Storage::class); - $mount = new MountPoint($storage, '/bar'); - $storage->method('getId')->willReturn(''); - $cache = $this->getMockBuilder(Cache::class)->setConstructorArgs([$storage])->getMock(); - - $fileInfo = new CacheEntry(['path' => 'foo/qwerty', 'mimetype' => 'text/plain'], null); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $this->userMountCache->expects($this->any()) - ->method('getMountsForFileId') - ->with(1) - ->willReturn([new CachedMountInfo( - $this->user, - 1, - 0, - '/bar/', - 'test', - 1, - '' - )]); - - $cache->method('get') - ->with(1) - ->willReturn($fileInfo); - - $root->method('getMountsIn') - ->with('/bar/foo') - ->willReturn([]); - - $manager->method('getMountFromMountInfo') - ->willReturn($mount); - $manager->method('getMountsByMountProvider') - ->willReturn([$mount]); - - $node = new Folder($root, $view, '/bar/foo'); - $result = $node->getById(1); - $this->assertEquals(1, count($result)); - $this->assertEquals('/bar/foo/qwerty', $result[0]->getPath()); - } - - public function testGetByIdMountRoot(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $storage = $this->createMock(Storage::class); - $mount = new MountPoint($storage, '/bar'); - $storage->method('getId')->willReturn(''); - $cache = $this->getMockBuilder(Cache::class)->setConstructorArgs([$storage])->getMock(); - - $fileInfo = new CacheEntry(['path' => '', 'mimetype' => 'text/plain'], null); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $this->userMountCache->expects($this->any()) - ->method('getMountsForFileId') - ->with(1) - ->willReturn([new CachedMountInfo( - $this->user, - 1, - 0, - '/bar/', - 'test', - 1, - '' - )]); - - $cache->method('get') - ->with(1) - ->willReturn($fileInfo); - - $manager->method('getMountFromMountInfo') - ->willReturn($mount); - $manager->method('getMountsByMountProvider') - ->willReturn([$mount]); - - $node = new Folder($root, $view, '/bar'); - $result = $node->getById(1); - $this->assertEquals(1, count($result)); - $this->assertEquals('/bar', $result[0]->getPath()); - } - - public function testGetByIdOutsideFolder(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $storage = $this->createMock(Storage::class); - $mount = new MountPoint($storage, '/bar'); - $storage->method('getId')->willReturn(''); - $cache = $this->getMockBuilder(Cache::class)->setConstructorArgs([$storage])->getMock(); - - $fileInfo = new CacheEntry(['path' => 'foobar', 'mimetype' => 'text/plain'], null); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $this->userMountCache->expects($this->any()) - ->method('getMountsForFileId') - ->with(1) - ->willReturn([new CachedMountInfo( - $this->user, - 1, - 0, - '/bar/', - 'test', - 1, - '' - )]); - - $cache->method('get') - ->with(1) - ->willReturn($fileInfo); - - $manager->method('getMountFromMountInfo') - ->willReturn($mount); - $manager->method('getMountsByMountProvider') - ->willReturn([$mount]); - - $node = new Folder($root, $view, '/bar/foo'); - $result = $node->getById(1); - $this->assertEquals(0, count($result)); - } - - public function testGetByIdMultipleStorages(): void { - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $storage = $this->createMock(Storage::class); - $mount1 = new MountPoint($storage, '/bar'); - $mount2 = new MountPoint($storage, '/bar/foo/asd'); - $storage->method('getId')->willReturn(''); - $cache = $this->getMockBuilder(Cache::class)->setConstructorArgs([$storage])->getMock(); - - $fileInfo = new CacheEntry(['path' => 'foo/qwerty', 'mimetype' => 'text/plain'], null); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $this->userMountCache->method('getMountsForFileId') - ->with(1) - ->willReturn([ - new CachedMountInfo( - $this->user, - 1, - 0, - '/bar/', - 'test', - 1, - '' - ), - new CachedMountInfo( - $this->user, - 1, - 0, - '/bar/foo/asd/', - 'test', - 1, - '' - ), - ]); - - $cache->method('get') - ->with(1) - ->willReturn($fileInfo); - - $manager->method('getMountFromMountInfo') - ->willReturnCallback(function (ICachedMountInfo $mountInfo) use ($mount1, $mount2) { - if ($mountInfo->getMountPoint() === $mount1->getMountPoint()) { - return $mount1; - } - if ($mountInfo->getMountPoint() === $mount2->getMountPoint()) { - return $mount2; - } - return null; - }); - $manager->method('getMountsByMountProvider') - ->willReturn([$mount1, $mount2]); - - $node = new Folder($root, $view, '/bar/foo'); - $result = $node->getById(1); - $this->assertEquals(2, count($result)); - $this->assertEquals('/bar/foo/qwerty', $result[0]->getPath()); - $this->assertEquals('/bar/foo/asd/foo/qwerty', $result[1]->getPath()); - } - - public static function uniqueNameProvider(): array { - return [ - // input, existing, expected - ['foo', [], 'foo'], - ['foo', ['foo'], 'foo (2)'], - ['foo', ['foo', 'foo (2)'], 'foo (3)'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('uniqueNameProvider')] - public function testGetUniqueName($name, $existingFiles, $expected): void { - $manager = $this->createMock(Manager::class); - $folderPath = '/bar/foo'; - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - - $view->expects($this->any()) - ->method('file_exists') - ->willReturnCallback(function ($path) use ($existingFiles, $folderPath) { - foreach ($existingFiles as $existing) { - if ($folderPath . '/' . $existing === $path) { - return true; - } - } - return false; - }); - - $node = new Folder($root, $view, $folderPath); - $this->assertEquals($expected, $node->getNonExistingName($name)); - } - - public function testRecent(): void { - $manager = $this->createMock(Manager::class); - $folderPath = '/bar/foo'; - $view = $this->getRootViewMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|Root $root */ - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|FileInfo $folderInfo */ - $folderInfo = $this->getMockBuilder(FileInfo::class) - ->disableOriginalConstructor()->getMock(); - - $baseTime = time(); - $storage = new Temporary(); - $mount = new MountPoint($storage, ''); - - $folderInfo->expects($this->any()) - ->method('getMountPoint') - ->willReturn($mount); - $root->method('getMount') - ->willReturn($mount); - $root->method('getMountsIn') - ->willReturn([]); - - $cache = $storage->getCache(); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('bar', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('bar/foo', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('bar/asd', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $id1 = $cache->put('bar/foo/inside.txt', [ - 'storage_mtime' => $baseTime, - 'mtime' => $baseTime, - 'mimetype' => 'text/plain', - 'size' => 3, - 'permissions' => Constants::PERMISSION_ALL, - ]); - $id2 = $cache->put('bar/foo/old.txt', [ - 'storage_mtime' => $baseTime - 100, - 'mtime' => $baseTime - 100, - 'mimetype' => 'text/plain', - 'size' => 3, - 'permissions' => Constants::PERMISSION_READ, - ]); - $cache->put('bar/asd/outside.txt', [ - 'storage_mtime' => $baseTime, - 'mtime' => $baseTime, - 'mimetype' => 'text/plain', - 'size' => 3, - ]); - $id3 = $cache->put('bar/foo/older.txt', [ - 'storage_mtime' => $baseTime - 600, - 'mtime' => $baseTime - 600, - 'mimetype' => 'text/plain', - 'size' => 3, - 'permissions' => Constants::PERMISSION_ALL, - ]); - - $node = new Folder($root, $view, $folderPath, $folderInfo); - - $nodes = $node->getRecent(5); - $ids = array_map(function (Node $node) { - return (int)$node->getId(); - }, $nodes); - $this->assertEquals([$id1, $id2, $id3], $ids); - } - - public function testRecentFolder(): void { - $manager = $this->createMock(Manager::class); - $folderPath = '/bar/foo'; - $view = $this->getRootViewMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|Root $root */ - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|FileInfo $folderInfo */ - $folderInfo = $this->getMockBuilder(FileInfo::class) - ->disableOriginalConstructor()->getMock(); - - $baseTime = time(); - $storage = new Temporary(); - $mount = new MountPoint($storage, ''); - - $folderInfo->expects($this->any()) - ->method('getMountPoint') - ->willReturn($mount); - - $root->method('getMount') - ->willReturn($mount); - $root->method('getMountsIn') - ->willReturn([]); - - $cache = $storage->getCache(); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('bar', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('bar/foo', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $id1 = $cache->put('bar/foo/folder', [ - 'storage_mtime' => $baseTime, - 'mtime' => $baseTime, - 'mimetype' => \OCP\Files\FileInfo::MIMETYPE_FOLDER, - 'size' => 3, - 'permissions' => 0, - ]); - $id2 = $cache->put('bar/foo/folder/bar.txt', [ - 'storage_mtime' => $baseTime, - 'mtime' => $baseTime, - 'mimetype' => 'text/plain', - 'size' => 3, - 'parent' => $id1, - 'permissions' => Constants::PERMISSION_ALL, - ]); - $id3 = $cache->put('bar/foo/folder/asd.txt', [ - 'storage_mtime' => $baseTime - 100, - 'mtime' => $baseTime - 100, - 'mimetype' => 'text/plain', - 'size' => 3, - 'parent' => $id1, - 'permissions' => Constants::PERMISSION_ALL, - ]); - - $node = new Folder($root, $view, $folderPath, $folderInfo); - - $nodes = $node->getRecent(5); - $ids = array_map(function (Node $node) { - return (int)$node->getId(); - }, $nodes); - $this->assertEquals([$id2, $id3], $ids); - $this->assertEquals($baseTime, $nodes[0]->getMTime()); - $this->assertEquals($baseTime - 100, $nodes[1]->getMTime()); - } - - public function testRecentJail(): void { - $manager = $this->createMock(Manager::class); - $folderPath = '/bar/foo'; - $view = $this->getRootViewMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|Root $root */ - $root = $this->getMockBuilder(Root::class) - ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - /** @var \PHPUnit\Framework\MockObject\MockObject|FileInfo $folderInfo */ - $folderInfo = $this->getMockBuilder(FileInfo::class) - ->disableOriginalConstructor()->getMock(); - - $baseTime = time(); - $storage = new Temporary(); - $jail = new Jail([ - 'storage' => $storage, - 'root' => 'folder', - ]); - $mount = new MountPoint($jail, '/bar/foo'); - - $folderInfo->expects($this->any()) - ->method('getMountPoint') - ->willReturn($mount); - $root->method('getMount') - ->willReturn($mount); - $root->method('getMountsIn') - ->willReturn([]); - - $cache = $storage->getCache(); - - $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('folder', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $id1 = $cache->put('folder/inside.txt', [ - 'storage_mtime' => $baseTime, - 'mtime' => $baseTime, - 'mimetype' => 'text/plain', - 'size' => 3, - 'permissions' => Constants::PERMISSION_ALL, - ]); - - $cache->put('outside.txt', [ - 'storage_mtime' => $baseTime - 100, - 'mtime' => $baseTime - 100, - 'mimetype' => 'text/plain', - 'size' => 3, - ]); - - $node = new Folder($root, $view, $folderPath, $folderInfo); - - $nodes = $node->getRecent(5); - $ids = array_map(function (Node $node) { - return (int)$node->getId(); - }, $nodes); - $this->assertEquals([$id1], $ids); - } - - public static function offsetLimitProvider(): array { - return [ - [0, 10, ['/bar/foo/foo1', '/bar/foo/foo2', '/bar/foo/foo3', '/bar/foo/foo4', '/bar/foo/sub1/foo5', '/bar/foo/sub1/foo6', '/bar/foo/sub2/foo7', '/bar/foo/sub2/foo8'], []], - [0, 5, ['/bar/foo/foo1', '/bar/foo/foo2', '/bar/foo/foo3', '/bar/foo/foo4', '/bar/foo/sub1/foo5'], []], - [0, 2, ['/bar/foo/foo1', '/bar/foo/foo2'], []], - [3, 2, ['/bar/foo/foo4', '/bar/foo/sub1/foo5'], []], - [3, 5, ['/bar/foo/foo4', '/bar/foo/sub1/foo5', '/bar/foo/sub1/foo6', '/bar/foo/sub2/foo7', '/bar/foo/sub2/foo8'], []], - [5, 2, ['/bar/foo/sub1/foo6', '/bar/foo/sub2/foo7'], []], - [6, 2, ['/bar/foo/sub2/foo7', '/bar/foo/sub2/foo8'], []], - [7, 2, ['/bar/foo/sub2/foo8'], []], - [10, 2, [], []], - [0, 5, ['/bar/foo/sub2/foo7', '/bar/foo/foo1', '/bar/foo/sub1/foo5', '/bar/foo/foo2', '/bar/foo/foo3'], [new SearchOrder(ISearchOrder::DIRECTION_ASCENDING, 'mtime')]], - [3, 2, ['/bar/foo/foo2', '/bar/foo/foo3'], [new SearchOrder(ISearchOrder::DIRECTION_ASCENDING, 'mtime')]], - [0, 5, ['/bar/foo/sub1/foo5', '/bar/foo/sub1/foo6', '/bar/foo/sub2/foo7', '/bar/foo/foo1', '/bar/foo/foo2'], [ - new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'size'), - new SearchOrder(ISearchOrder::DIRECTION_ASCENDING, 'mtime') - ]], - ]; - } - - /** - * @param int $offset - * @param int $limit - * @param string[] $expectedPaths - * @param ISearchOrder[] $ordering - * @throws NotFoundException - * @throws InvalidPathException - */ - #[\PHPUnit\Framework\Attributes\DataProvider('offsetLimitProvider')] - public function testSearchSubStoragesLimitOffset(int $offset, int $limit, array $expectedPaths, array $ordering): void { - if (!$ordering) { - $ordering = [new SearchOrder(ISearchOrder::DIRECTION_ASCENDING, 'fileid')]; - } - - $manager = $this->createMock(Manager::class); - $view = $this->getRootViewMock(); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - $storage = $this->createMock(IStorage::class); - $storage->method('getId')->willReturn('test::1'); - $cache = new Cache($storage); - $subStorage1 = $this->createMock(IStorage::class); - $subStorage1->method('getId')->willReturn('test::2'); - $subCache1 = new Cache($subStorage1); - $subMount1 = $this->getMockBuilder(MountPoint::class)->setConstructorArgs([Temporary::class, ''])->getMock(); - $subStorage2 = $this->createMock(IStorage::class); - $subStorage2->method('getId')->willReturn('test::3'); - $subCache2 = new Cache($subStorage2); - $subMount2 = $this->getMockBuilder(MountPoint::class)->setConstructorArgs([Temporary::class, ''])->getMock(); - - $mount = $this->createMock(IMountPoint::class); - $mount->method('getStorage') - ->willReturn($storage); - $mount->method('getInternalPath') - ->willReturn('foo'); - - $subMount1->method('getStorage') - ->willReturn($subStorage1); - - $subMount1->method('getMountPoint') - ->willReturn('/bar/foo/sub1/'); - - $storage->method('getCache') - ->willReturn($cache); - $storage->method('getOwner') - ->willReturn('owner'); - - $subStorage1->method('getCache') - ->willReturn($subCache1); - $subStorage1->method('getOwner') - ->willReturn('owner'); - - $subMount2->method('getStorage') - ->willReturn($subStorage2); - - $subMount2->method('getMountPoint') - ->willReturn('/bar/foo/sub2/'); - - $subStorage2->method('getCache') - ->willReturn($subCache2); - $subStorage2->method('getOwner') - ->willReturn('owner'); - - $cache->insert('', ['size' => 0, 'mtime' => 10, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo', ['size' => 0, 'mtime' => 10, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $cache->insert('foo/foo1', ['size' => 200, 'mtime' => 10, 'mimetype' => 'text/plain']); - $cache->insert('foo/foo2', ['size' => 200, 'mtime' => 20, 'mimetype' => 'text/plain']); - $cache->insert('foo/foo3', ['size' => 200, 'mtime' => 30, 'mimetype' => 'text/plain']); - $cache->insert('foo/foo4', ['size' => 200, 'mtime' => 40, 'mimetype' => 'text/plain']); - - $subCache1->insert('', ['size' => 0, 'mtime' => 10, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $subCache1->insert('foo5', ['size' => 300, 'mtime' => 15, 'mimetype' => 'text/plain']); - $subCache1->insert('foo6', ['size' => 300, 'mtime' => 50, 'mimetype' => 'text/plain']); - - $subCache2->insert('', ['size' => 0, 'mtime' => 10, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); - $subCache2->insert('foo7', ['size' => 200, 'mtime' => 5, 'mimetype' => 'text/plain']); - $subCache2->insert('foo8', ['size' => 200, 'mtime' => 60, 'mimetype' => 'text/plain']); - - $root->method('getMountsIn') - ->with('/bar/foo') - ->willReturn([$subMount1, $subMount2]); - - $root->method('getMount') - ->with('/bar/foo') - ->willReturn($mount); - - $node = new Folder($root, $view, '/bar/foo'); - $comparison = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%foo%'); - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - $comparison, - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE)]), - ]); - $query = new SearchQuery($operator, $limit, $offset, $ordering); - $result = $node->search($query); - $cache->clear(); - $subCache1->clear(); - $subCache2->clear(); - $ids = array_map(function (Node $info) { - return $info->getPath(); - }, $result); - $this->assertEquals($expectedPaths, $ids); - } - - public static function dataGetOrCreateFolder(): \Generator { - yield 'Create new folder' => [0]; - yield 'Get existing folder' => [1]; - yield 'Create new folder while a file with the same name already exists' => [2]; - } - - #[DataProvider('dataGetOrCreateFolder')] - public function testGetOrCreateFolder(int $case): void { - $folderName = 'asd'; - - $view = $this->getRootViewMock(); - $manager = $this->createMock(Manager::class); - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - $root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $view->method('getFileInfo') - ->willReturnCallback(function (string $path) use ($folderName) { - if ($path === '/bar/foo' || $path === '/bar/foo/' . $folderName) { - return $this->getFileInfo(['permissions' => Constants::PERMISSION_ALL]); - } - $this->fail('Trying to get ' . $path); - }); - - $view->method('mkdir') - ->willReturn(true); - - $view->method('touch') - ->with('/bar/foo/asd') - ->willReturn(true); - - $node = new Folder($root, $view, '/bar/foo'); - - switch ($case) { - case 0: - $child = new Folder($root, $view, '/bar/foo/' . $folderName, null, $node); - - $root->expects($this->any()) - ->method('get') - ->willReturnCallback(function (string $path) use ($root, $view, $folderName) { - if ($path === '/bar/foo/') { - return new Folder($root, $view, '/bar/foo/'); - } elseif ($path === '/bar/foo/' . $folderName) { - throw new NotFoundException(); - } - $this->fail('Trying to get ' . $path); - }); - - break; // do nothing - case 1: - $child = new Folder($root, $view, '/bar/foo/' . $folderName, null, $node); - - $root->expects($this->any()) - ->method('get') - ->with('/bar/foo/' . $folderName) - ->willReturn($child); - $node->newFolder($folderName); - break; - case 2: - $child = new Folder($root, $view, '/bar/foo/' . $folderName . ' (1)', null, $node); - $root->expects($this->any()) - ->method('get') - ->willReturnCallback(function (string $path) use ($root, $view, $folderName) { - if ($path === '/bar/foo/') { - return new Folder($root, $view, '/bar/foo/'); - } elseif ($path === '/bar/foo/' . $folderName) { - return new File($root, $view, '/bar/foo/asd'); - } elseif ($path === '/bar/foo/' . $folderName . ' (1)') { - throw new NotFoundException(); - } - $this->fail('Trying to get ' . $path); - }); - $node->newFile($folderName); - break; - } - - $result = $node->getOrCreateFolder($folderName); - $this->assertEquals($child, $result); - } - - private function callBuildNotExistingFileNameForView(string $path, string $name, View&MockObject $view): string { - $rootFolder = $this->createMock(IRootFolder::class); - $folder = new Folder($rootFolder, $view, $path); - return $path . (str_ends_with('/', $path) ? '' : '/') . $folder->getNonExistingName($name); - } - - public function testBuildNotExistingFileNameForView(): void { - $viewMock = $this->createMock(View::class); - $this->assertEquals('/filename', $this->callBuildNotExistingFileNameForView('/', 'filename', $viewMock)); - $this->assertEquals('dir/filename.ext', $this->callBuildNotExistingFileNameForView('dir', 'filename.ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - // Conflict on filename.ext - ['dir/filename.ext', true], - ['dir/filename (2).ext', false], - ]); - $this->assertEquals('dir/filename (2).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename.ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(3)) - ->method('file_exists') - ->willReturnMap([ - // Conflict on filename.ext - ['dir/filename.ext', true], - ['dir/filename (2).ext', true], - ['dir/filename (3).ext', false], - ]); - $this->assertEquals('dir/filename (3).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename.ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename (1).ext', true], - ['dir/filename (2).ext', false], - ]); - $this->assertEquals('dir/filename (2).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename (1).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename (2).ext', true], - ['dir/filename (3).ext', false], - ]); - $this->assertEquals('dir/filename (3).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename (2).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(3)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename (2).ext', true], - ['dir/filename (3).ext', true], - ['dir/filename (4).ext', false], - ]); - $this->assertEquals('dir/filename (4).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename (2).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename(1).ext', true], - ['dir/filename(2).ext', false], - ]); - $this->assertEquals('dir/filename(2).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename(1).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename(1) (1).ext', true], - ['dir/filename(1) (2).ext', false], - ]); - $this->assertEquals('dir/filename(1) (2).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename(1) (1).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(3)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename(1) (1).ext', true], - ['dir/filename(1) (2).ext', true], - ['dir/filename(1) (3).ext', false], - ]); - $this->assertEquals('dir/filename(1) (3).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename(1) (1).ext', $viewMock)); - - $viewMock = $this->createMock(View::class); - $viewMock->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['dir/filename(1) (2) (3).ext', true], - ['dir/filename(1) (2) (4).ext', false], - ]); - $this->assertEquals('dir/filename(1) (2) (4).ext', $this->callBuildNotExistingFileNameForView('dir', 'filename(1) (2) (3).ext', $viewMock)); - } -} --- a/tests/lib/Files/Node/HookConnectorTest.php +++ /dev/null @@ -1,331 +0,0 @@ -userId = $this->getUniqueID(); - $this->createUser($this->userId, 'pass'); - // this will setup the FS - $this->loginAsUser($this->userId); - $this->registerMount($this->userId, new Temporary(), '/' . $this->userId . '/files/'); - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createLocal') - ->willReturnCallback(function () { - return new ArrayCache(); - }); - $this->view = new View(); - $this->root = new Root( - Filesystem::getMountManager(), - $this->view, - Server::get(IUserManager::class)->get($this->userId), - Server::get(IUserMountCache::class), - $this->createMock(LoggerInterface::class), - $this->createMock(IUserManager::class), - $this->createMock(IEventDispatcher::class), - $cacheFactory, - $this->createMock(IAppConfig::class), - ); - $this->eventDispatcher = Server::get(IEventDispatcher::class); - $this->logger = Server::get(LoggerInterface::class); - } - - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - \OC_Hook::clear('OC_Filesystem'); - \OC_Util::tearDownFS(); - } - - public static function viewToNodeProvider(): array { - return [ - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - }, 'preWrite', '\OCP\Files::preWrite', BeforeNodeWrittenEvent::class], - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - }, 'postWrite', '\OCP\Files::postWrite', NodeWrittenEvent::class], - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - }, 'preCreate', '\OCP\Files::preCreate', BeforeNodeCreatedEvent::class], - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - }, 'postCreate', '\OCP\Files::postCreate', NodeCreatedEvent::class], - [function (): void { - Filesystem::mkdir('test.txt'); - }, 'preCreate', '\OCP\Files::preCreate', BeforeNodeCreatedEvent::class], - [function (): void { - Filesystem::mkdir('test.txt'); - }, 'postCreate', '\OCP\Files::postCreate', NodeCreatedEvent::class], - [function (): void { - Filesystem::touch('test.txt'); - }, 'preTouch', '\OCP\Files::preTouch', BeforeNodeTouchedEvent::class], - [function (): void { - Filesystem::touch('test.txt'); - }, 'postTouch', '\OCP\Files::postTouch', NodeTouchedEvent::class], - [function (): void { - Filesystem::touch('test.txt'); - }, 'preCreate', '\OCP\Files::preCreate', BeforeNodeCreatedEvent::class], - [function (): void { - Filesystem::touch('test.txt'); - }, 'postCreate', '\OCP\Files::postCreate', NodeCreatedEvent::class], - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - Filesystem::unlink('test.txt'); - }, 'preDelete', '\OCP\Files::preDelete', BeforeNodeDeletedEvent::class], - [function (): void { - Filesystem::file_put_contents('test.txt', 'asd'); - Filesystem::unlink('test.txt'); - }, 'postDelete', '\OCP\Files::postDelete', NodeDeletedEvent::class], - [function (): void { - Filesystem::mkdir('test.txt'); - Filesystem::rmdir('test.txt'); - }, 'preDelete', '\OCP\Files::preDelete', BeforeNodeDeletedEvent::class], - [function (): void { - Filesystem::mkdir('test.txt'); - Filesystem::rmdir('test.txt'); - }, 'postDelete', '\OCP\Files::postDelete', NodeDeletedEvent::class], - ]; - } - - /** - * @param callable $operation - * @param string $expectedHook - */ - #[\PHPUnit\Framework\Attributes\DataProvider('viewToNodeProvider')] - public function testViewToNode(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent): void { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); - $connector->viewToNode(); - $hookCalled = false; - /** @var Node $hookNode */ - $hookNode = null; - - $this->root->listen('\OC\Files', $expectedHook, function ($node) use (&$hookNode, &$hookCalled): void { - $hookCalled = true; - $hookNode = $node; - }); - - $dispatcherCalled = false; - /** @var Node $dispatcherNode */ - $dispatcherNode = null; - $this->eventDispatcher->addListener($expectedLegacyEvent, function ($event) use (&$dispatcherCalled, &$dispatcherNode): void { - /** @var GenericEvent|APIGenericEvent $event */ - $dispatcherCalled = true; - $dispatcherNode = $event->getSubject(); - }); - - $newDispatcherCalled = false; - $newDispatcherNode = null; - $this->eventDispatcher->addListener($expectedEvent, function ($event) use ($expectedEvent, &$newDispatcherCalled, &$newDispatcherNode): void { - if ($event instanceof $expectedEvent) { - /** @var AbstractNodeEvent $event */ - $newDispatcherCalled = true; - $newDispatcherNode = $event->getNode(); - } - }); - - $operation(); - - $this->assertTrue($hookCalled); - $this->assertEquals('/' . $this->userId . '/files/test.txt', $hookNode->getPath()); - - $this->assertTrue($dispatcherCalled); - $this->assertEquals('/' . $this->userId . '/files/test.txt', $dispatcherNode->getPath()); - - $this->assertTrue($newDispatcherCalled); - $this->assertEquals('/' . $this->userId . '/files/test.txt', $newDispatcherNode->getPath()); - } - - public static function viewToNodeProviderCopyRename(): array { - return [ - [function (): void { - Filesystem::file_put_contents('source', 'asd'); - Filesystem::rename('source', 'target'); - }, 'preRename', '\OCP\Files::preRename', BeforeNodeRenamedEvent::class], - [function (): void { - Filesystem::file_put_contents('source', 'asd'); - Filesystem::rename('source', 'target'); - }, 'postRename', '\OCP\Files::postRename', NodeRenamedEvent::class], - [function (): void { - Filesystem::file_put_contents('source', 'asd'); - Filesystem::copy('source', 'target'); - }, 'preCopy', '\OCP\Files::preCopy', BeforeNodeCopiedEvent::class], - [function (): void { - Filesystem::file_put_contents('source', 'asd'); - Filesystem::copy('source', 'target'); - }, 'postCopy', '\OCP\Files::postCopy', NodeCopiedEvent::class], - ]; - } - - /** - * @param callable $operation - * @param string $expectedHook - */ - #[\PHPUnit\Framework\Attributes\DataProvider('viewToNodeProviderCopyRename')] - public function testViewToNodeCopyRename(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent): void { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); - $connector->viewToNode(); - $hookCalled = false; - /** @var Node $hookSourceNode */ - $hookSourceNode = null; - /** @var Node $hookTargetNode */ - $hookTargetNode = null; - - $this->root->listen('\OC\Files', $expectedHook, function ($sourceNode, $targetNode) use (&$hookCalled, &$hookSourceNode, &$hookTargetNode): void { - $hookCalled = true; - $hookSourceNode = $sourceNode; - $hookTargetNode = $targetNode; - }); - - $dispatcherCalled = false; - /** @var Node $dispatcherSourceNode */ - $dispatcherSourceNode = null; - /** @var Node $dispatcherTargetNode */ - $dispatcherTargetNode = null; - $this->eventDispatcher->addListener($expectedLegacyEvent, function ($event) use (&$dispatcherSourceNode, &$dispatcherTargetNode, &$dispatcherCalled): void { - /** @var GenericEvent|APIGenericEvent $event */ - $dispatcherCalled = true; - [$dispatcherSourceNode, $dispatcherTargetNode] = $event->getSubject(); - }); - - $newDispatcherCalled = false; - /** @var Node $dispatcherSourceNode */ - $newDispatcherSourceNode = null; - /** @var Node $dispatcherTargetNode */ - $newDispatcherTargetNode = null; - $this->eventDispatcher->addListener($expectedEvent, function ($event) use ($expectedEvent, &$newDispatcherSourceNode, &$newDispatcherTargetNode, &$newDispatcherCalled): void { - if ($event instanceof $expectedEvent) { - /** @var AbstractNodesEvent$event */ - $newDispatcherCalled = true; - $newDispatcherSourceNode = $event->getSource(); - $newDispatcherTargetNode = $event->getTarget(); - } - }); - - $operation(); - - $this->assertTrue($hookCalled); - $this->assertEquals('/' . $this->userId . '/files/source', $hookSourceNode->getPath()); - $this->assertEquals('/' . $this->userId . '/files/target', $hookTargetNode->getPath()); - - $this->assertTrue($dispatcherCalled); - $this->assertEquals('/' . $this->userId . '/files/source', $dispatcherSourceNode->getPath()); - $this->assertEquals('/' . $this->userId . '/files/target', $dispatcherTargetNode->getPath()); - - $this->assertTrue($newDispatcherCalled); - $this->assertEquals('/' . $this->userId . '/files/source', $newDispatcherSourceNode->getPath()); - $this->assertEquals('/' . $this->userId . '/files/target', $newDispatcherTargetNode->getPath()); - } - - public function testPostDeleteMeta(): void { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); - $connector->viewToNode(); - $hookCalled = false; - /** @var Node $hookNode */ - $hookNode = null; - - $this->root->listen('\OC\Files', 'postDelete', function ($node) use (&$hookNode, &$hookCalled): void { - $hookCalled = true; - $hookNode = $node; - }); - - $dispatcherCalled = false; - /** @var Node $dispatcherNode */ - $dispatcherNode = null; - $this->eventDispatcher->addListener('\OCP\Files::postDelete', function ($event) use (&$dispatcherCalled, &$dispatcherNode): void { - /** @var GenericEvent|APIGenericEvent $event */ - $dispatcherCalled = true; - $dispatcherNode = $event->getSubject(); - }); - - $newDispatcherCalled = false; - /** @var Node $dispatcherNode */ - $newDispatcherNode = null; - $this->eventDispatcher->addListener(NodeDeletedEvent::class, function ($event) use (&$newDispatcherCalled, &$newDispatcherNode): void { - if ($event instanceof NodeDeletedEvent) { - /** @var AbstractNodeEvent $event */ - $newDispatcherCalled = true; - $newDispatcherNode = $event->getNode(); - } - }); - - Filesystem::file_put_contents('test.txt', 'asd'); - $info = Filesystem::getFileInfo('test.txt'); - Filesystem::unlink('test.txt'); - - $this->assertTrue($hookCalled); - $this->assertEquals($hookNode->getId(), $info->getId()); - - $this->assertTrue($dispatcherCalled); - $this->assertEquals($dispatcherNode->getId(), $info->getId()); - - $this->assertTrue($newDispatcherCalled); - $this->assertEquals($newDispatcherNode->getId(), $info->getId()); - } -} --- a/tests/lib/Files/Node/IntegrationTest.php +++ /dev/null @@ -1,155 +0,0 @@ -createUser($this->getUniqueID('user'), ''); - $this->loginAsUser($user->getUID()); - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createLocal') - ->willReturnCallback(function () { - return new ArrayCache(); - }); - - $this->view = new View(); - $this->root = new Root( - $manager, - $this->view, - $user, - Server::get(IUserMountCache::class), - $this->createMock(LoggerInterface::class), - $this->createMock(IUserManager::class), - $this->createMock(IEventDispatcher::class), - $cacheFactory, - $this->createMock(IAppConfig::class), - ); - $storage = new Temporary([]); - $subStorage = new Temporary([]); - $this->storages[] = $storage; - $this->storages[] = $subStorage; - $this->root->mount($storage, '/'); - $this->root->mount($subStorage, '/substorage/'); - $manager->removeMount('/' . $user->getUID()); - } - - #[\Override] - protected function tearDown(): void { - foreach ($this->storages as $storage) { - $storage->getCache()->clear(); - } - - $this->logout(); - parent::tearDown(); - } - - public function testBasicFile(): void { - $file = $this->root->newFile('/foo.txt'); - $this->assertCount(2, $this->root->getDirectoryListing()); - $this->assertTrue($this->root->nodeExists('/foo.txt')); - $id = $file->getId(); - $this->assertInstanceOf('\OC\Files\Node\File', $file); - $file->putContent('qwerty'); - $this->assertEquals('text/plain', $file->getMimeType()); - $this->assertEquals('qwerty', $file->getContent()); - $this->assertFalse($this->root->nodeExists('/bar.txt')); - $target = $file->move('/bar.txt'); - $this->assertEquals($id, $target->getId()); - $this->assertEquals($id, $file->getId()); - $this->assertFalse($this->root->nodeExists('/foo.txt')); - $this->assertTrue($this->root->nodeExists('/bar.txt')); - $this->assertEquals('bar.txt', $file->getName()); - $this->assertEquals('bar.txt', $file->getInternalPath()); - - $file->move('/substorage/bar.txt'); - $this->assertEquals($id, $file->getId()); - $this->assertEquals('qwerty', $file->getContent()); - } - - public function testBasicFolder(): void { - $folder = $this->root->newFolder('/foo'); - $this->assertTrue($this->root->nodeExists('/foo')); - $file = $folder->newFile('/bar'); - $this->assertTrue($this->root->nodeExists('/foo/bar')); - $file->putContent('qwerty'); - - $listing = $folder->getDirectoryListing(); - $this->assertEquals(1, count($listing)); - $this->assertEquals($file->getId(), $listing[0]->getId()); - $this->assertEquals($file->getStorage(), $listing[0]->getStorage()); - - $rootListing = $this->root->getDirectoryListing(); - $this->assertEquals(2, count($rootListing)); - - $folder->move('/asd'); - /** - * @var File $file - */ - $file = $folder->get('/bar'); - $this->assertInstanceOf('\OC\Files\Node\File', $file); - $this->assertFalse($this->root->nodeExists('/foo/bar')); - $this->assertTrue($this->root->nodeExists('/asd/bar')); - $this->assertEquals('qwerty', $file->getContent()); - $folder->move('/substorage/foo'); - /** - * @var File $file - */ - $file = $folder->get('/bar'); - $this->assertInstanceOf('\OC\Files\Node\File', $file); - $this->assertTrue($this->root->nodeExists('/substorage/foo/bar')); - $this->assertEquals('qwerty', $file->getContent()); - } -} --- a/tests/lib/Files/Node/NodeTestCase.php +++ /dev/null @@ -1,771 +0,0 @@ -user = $this->createMock(IUser::class); - $this->manager = $this->getMockBuilder(Manager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->getMock(); - $this->view->expects($this->any()) - ->method('getRoot') - ->willReturn(''); - $this->userMountCache = $this->getMockBuilder('\OCP\Files\Config\IUserMountCache') - ->disableOriginalConstructor() - ->getMock(); - $this->logger = $this->createMock(LoggerInterface::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cacheFactory->method('createLocal') - ->willReturnCallback(function () { - return new ArrayCache(); - }); - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->getMock(); - } - - /** - * @return View|\PHPUnit\Framework\MockObject\MockObject $view - */ - protected function getRootViewMock() { - $view = $this->createMock(View::class); - $view->expects($this->any()) - ->method('getRoot') - ->willReturn(''); - return $view; - } - - abstract protected function createTestNode(IRootFolder $root, View&MockObject $view, string $path, array $data = [], string $internalPath = '', ?IStorage $storage = null): Node; - - /** - * @return class-string - */ - abstract protected function getNodeClass(): string; - - /** - * @return class-string - */ - abstract protected function getNonExistingNodeClass(): string; - - /** - * @return string - */ - abstract protected function getViewDeleteMethod(): string; - - protected function getMockStorage(): IStorage&MockObject { - $storage = $this->getMockBuilder(IStorage::class) - ->disableOriginalConstructor() - ->getMock(); - $storage->expects($this->any()) - ->method('getId') - ->willReturn('home::someuser'); - return $storage; - } - - protected function getFileInfo($data, $internalPath = '', ?IStorage $storage = null) { - $mount = $this->createMock(IMountPoint::class); - $mount->method('getStorage') - ->willReturn($storage); - return new FileInfo('', $this->getMockStorage(), $internalPath, $data, $mount); - } - - public function testDelete(): void { - $this->root->expects($this->exactly(2)) - ->method('emit') - ->willReturn(true); - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $this->view->expects($this->once()) - ->method($this->getViewDeleteMethod()) - ->with('/bar/foo') - ->willReturn(true); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $node->delete(); - } - - public function testDeleteHooks(): void { - $test = $this; - $hooksRun = 0; - /** - * @param File $node - */ - $preListener = function ($node) use (&$test, &$hooksRun): void { - $test->assertInstanceOf($this->getNodeClass(), $node); - $test->assertEquals('foo', $node->getInternalPath()); - $test->assertEquals('/bar/foo', $node->getPath()); - $test->assertEquals(1, $node->getId()); - $hooksRun++; - }; - - /** - * @param File $node - */ - $postListener = function ($node) use (&$test, &$hooksRun): void { - $test->assertInstanceOf($this->getNonExistingNodeClass(), $node); - $test->assertEquals('foo', $node->getInternalPath()); - $test->assertEquals('/bar/foo', $node->getPath()); - $test->assertEquals(1, $node->getId()); - $test->assertEquals('text/plain', $node->getMimeType()); - $hooksRun++; - }; - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->createMock(IAppConfig::class), - ); - - $root->listen('\OC\Files', 'preDelete', $preListener); - $root->listen('\OC\Files', 'postDelete', $postListener); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 1, 'mimetype' => 'text/plain'], 'foo')); - - $this->view->expects($this->once()) - ->method($this->getViewDeleteMethod()) - ->with('/bar/foo') - ->willReturn(true); - - $node = $this->createTestNode($root, $this->view, '/bar/foo'); - $node->delete(); - $this->assertEquals(2, $hooksRun); - } - - public function testDeleteNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $node->delete(); - } - - public function testStat(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $stat = [ - 'fileid' => 1, - 'size' => 100, - 'etag' => 'qwerty', - 'mtime' => 50, - 'permissions' => 0 - ]; - - $this->view->expects($this->once()) - ->method('stat') - ->with('/bar/foo') - ->willReturn($stat); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals($stat, $node->stat()); - } - - public function testGetId(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $stat = $this->getFileInfo([ - 'fileid' => 1, - 'size' => 100, - 'etag' => 'qwerty', - 'mtime' => 50 - ]); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($stat); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals(1, $node->getId()); - } - - public function testGetSize(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $stat = $this->getFileInfo([ - 'fileid' => 1, - 'size' => 100, - 'etag' => 'qwerty', - 'mtime' => 50 - ]); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($stat); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals(100, $node->getSize()); - } - - public function testGetEtag(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $stat = $this->getFileInfo([ - 'fileid' => 1, - 'size' => 100, - 'etag' => 'qwerty', - 'mtime' => 50 - ]); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($stat); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals('qwerty', $node->getEtag()); - } - - public function testGetMTime(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $stat = $this->getFileInfo([ - 'fileid' => 1, - 'size' => 100, - 'etag' => 'qwerty', - 'mtime' => 50 - ]); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($stat); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals(50, $node->getMTime()); - } - - public function testGetStorage(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - /** - * @var Storage|\PHPUnit\Framework\MockObject\MockObject $storage - */ - $storage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor() - ->getMock(); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo', [], 'foo', $storage); - $this->assertEquals($storage, $node->getStorage()); - } - - public function testGetPath(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals('/bar/foo', $node->getPath()); - } - - public function testGetInternalPath(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - /** - * @var Storage|\PHPUnit\Framework\MockObject\MockObject $storage - */ - $storage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor() - ->getMock(); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo([], 'foo')); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals('foo', $node->getInternalPath()); - } - - public function testGetName(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $this->assertEquals('foo', $node->getName()); - } - - public function testTouchSetMTime(): void { - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->once()) - ->method('touch') - ->with('/bar/foo', 100) - ->willReturn(true); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $node->touch(100); - $this->assertEquals(100, $node->getMTime()); - } - - public function testTouchHooks(): void { - $test = $this; - $hooksRun = 0; - /** - * @param File $node - */ - $preListener = function ($node) use (&$test, &$hooksRun): void { - $test->assertEquals('foo', $node->getInternalPath()); - $test->assertEquals('/bar/foo', $node->getPath()); - $hooksRun++; - }; - - /** - * @param File $node - */ - $postListener = function ($node) use (&$test, &$hooksRun): void { - $test->assertEquals('foo', $node->getInternalPath()); - $test->assertEquals('/bar/foo', $node->getPath()); - $hooksRun++; - }; - - $root = new Root( - $this->manager, - $this->view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->createMock(IAppConfig::class), - ); - $root->listen('\OC\Files', 'preTouch', $preListener); - $root->listen('\OC\Files', 'postTouch', $postListener); - - $this->view->expects($this->once()) - ->method('touch') - ->with('/bar/foo', 100) - ->willReturn(true); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL], 'foo')); - - $node = $this->createTestNode($root, $this->view, '/bar/foo'); - $node->touch(100); - $this->assertEquals(2, $hooksRun); - } - - public function testTouchNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $this->root->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $node->touch(100); - } - - public function testInvalidPath(): void { - $this->expectException(InvalidPathException::class); - - $node = $this->createTestNode($this->root, $this->view, '/../foo'); - $node->getFileInfo(); - } - - public function testCopySameStorage(): void { - $this->view->expects($this->any()) - ->method('copy') - ->with('/bar/foo', '/bar/asd') - ->willReturn(true); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 3])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - $newNode = $this->createTestNode($this->root, $this->view, '/bar/asd'); - - $this->root->method('get') - ->willReturnMap([ - ['/bar/asd', $newNode], - ['/bar', $parentNode] - ]); - - $target = $node->copy('/bar/asd'); - $this->assertInstanceOf($this->getNodeClass(), $target); - $this->assertEquals(3, $target->getId()); - } - - public function testCopyNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - /** - * @var Storage|\PHPUnit\Framework\MockObject\MockObject $storage - */ - $storage = $this->createMock('\OC\Files\Storage\Storage'); - - $this->root->expects($this->never()) - ->method('getMount'); - - $storage->expects($this->never()) - ->method('copy'); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ, 'fileid' => 3])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - - $this->root->expects($this->once()) - ->method('get') - ->willReturnMap([ - ['/bar', $parentNode] - ]); - - $node->copy('/bar/asd'); - } - - public function testCopyNoParent(): void { - $this->expectException(NotFoundException::class); - - $this->view->expects($this->never()) - ->method('copy'); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - - $this->root->expects($this->once()) - ->method('get') - ->with('/bar/asd') - ->willThrowException(new NotFoundException()); - - $node->copy('/bar/asd/foo'); - } - - public function testCopyParentIsFile(): void { - $this->expectException(NotPermittedException::class); - - $this->view->expects($this->never()) - ->method('copy'); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new File($this->root, $this->view, '/bar'); - - $this->root->expects($this->once()) - ->method('get') - ->willReturnMap([ - ['/bar', $parentNode] - ]); - - $node->copy('/bar/asd'); - } - - public function testMoveSameStorage(): void { - $this->view->expects($this->any()) - ->method('rename') - ->with('/bar/foo', '/bar/asd') - ->willReturn(true); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 1])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - - $this->root->expects($this->any()) - ->method('get') - ->willReturnMap([['/bar', $parentNode], ['/bar/asd', $node]]); - - $target = $node->move('/bar/asd'); - $this->assertInstanceOf($this->getNodeClass(), $target); - $this->assertEquals(1, $target->getId()); - $this->assertEquals('/bar/asd', $node->getPath()); - } - - public static function moveOrCopyProvider(): array { - return [ - ['move', 'rename', 'preRename', 'postRename'], - ['copy', 'copy', 'preCopy', 'postCopy'], - ]; - } - - /** - * @param string $operationMethod - * @param string $viewMethod - * @param string $preHookName - * @param string $postHookName - */ - #[\PHPUnit\Framework\Attributes\DataProvider('moveOrCopyProvider')] - public function testMoveCopyHooks($operationMethod, $viewMethod, $preHookName, $postHookName): void { - /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject $root */ - $root = $this->getMockBuilder(Root::class) - ->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) - ->onlyMethods(['get']) - ->getMock(); - - $this->view->expects($this->any()) - ->method($viewMethod) - ->with('/bar/foo', '/bar/asd') - ->willReturn(true); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 1])); - - /** - * @var File|\PHPUnit\Framework\MockObject\MockObject $node - */ - $node = $this->createTestNode($root, $this->view, '/bar/foo'); - $parentNode = new Folder($root, $this->view, '/bar'); - $targetTestNode = $this->createTestNode($root, $this->view, '/bar/asd'); - - $root->expects($this->any()) - ->method('get') - ->willReturnMap([['/bar', $parentNode], ['/bar/asd', $targetTestNode]]); - - $hooksRun = 0; - - $preListener = function (Node $sourceNode, Node $targetNode) use (&$hooksRun, $node): void { - $this->assertSame($node, $sourceNode); - $this->assertInstanceOf($this->getNodeClass(), $sourceNode); - $this->assertInstanceOf($this->getNonExistingNodeClass(), $targetNode); - $this->assertEquals('/bar/asd', $targetNode->getPath()); - $hooksRun++; - }; - - $postListener = function (Node $sourceNode, Node $targetNode) use (&$hooksRun, $node, $targetTestNode): void { - $this->assertSame($node, $sourceNode); - $this->assertNotSame($node, $targetNode); - $this->assertSame($targetTestNode, $targetNode); - $this->assertInstanceOf($this->getNodeClass(), $sourceNode); - $this->assertInstanceOf($this->getNodeClass(), $targetNode); - $hooksRun++; - }; - - $preWriteListener = function (Node $targetNode) use (&$hooksRun): void { - $this->assertInstanceOf($this->getNonExistingNodeClass(), $targetNode); - $this->assertEquals('/bar/asd', $targetNode->getPath()); - $hooksRun++; - }; - - $postWriteListener = function (Node $targetNode) use (&$hooksRun, $targetTestNode): void { - $this->assertSame($targetTestNode, $targetNode); - $hooksRun++; - }; - - $root->listen('\OC\Files', $preHookName, $preListener); - $root->listen('\OC\Files', 'preWrite', $preWriteListener); - $root->listen('\OC\Files', $postHookName, $postListener); - $root->listen('\OC\Files', 'postWrite', $postWriteListener); - - $node->$operationMethod('/bar/asd'); - - $this->assertEquals(4, $hooksRun); - } - - public function testMoveNotPermitted(): void { - $this->expectException(NotPermittedException::class); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_READ])); - - $this->view->expects($this->never()) - ->method('rename'); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - - $this->root->expects($this->once()) - ->method('get') - ->with('/bar') - ->willReturn($parentNode); - - $node->move('/bar/asd'); - } - - public function testMoveNoParent(): void { - $this->expectException(NotFoundException::class); - - /** - * @var Storage|\PHPUnit\Framework\MockObject\MockObject $storage - */ - $storage = $this->createMock('\OC\Files\Storage\Storage'); - - $storage->expects($this->never()) - ->method('rename'); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - - $this->root->expects($this->once()) - ->method('get') - ->with('/bar') - ->willThrowException(new NotFoundException()); - - $node->move('/bar/asd'); - } - - public function testMoveParentIsFile(): void { - $this->expectException(NotPermittedException::class); - - $this->view->expects($this->never()) - ->method('rename'); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new File($this->root, $this->view, '/bar'); - - $this->root->expects($this->once()) - ->method('get') - ->with('/bar') - ->willReturn($parentNode); - - $node->move('/bar/asd'); - } - - public function testMoveFailed(): void { - $this->expectException(NotPermittedException::class); - - $this->view->expects($this->any()) - ->method('rename') - ->with('/bar/foo', '/bar/asd') - ->willReturn(false); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 1])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - - $this->root->expects($this->any()) - ->method('get') - ->willReturnMap([['/bar', $parentNode], ['/bar/asd', $node]]); - - $node->move('/bar/asd'); - } - - public function testCopyFailed(): void { - $this->expectException(NotPermittedException::class); - - $this->view->expects($this->any()) - ->method('copy') - ->with('/bar/foo', '/bar/asd') - ->willReturn(false); - - $this->view->expects($this->any()) - ->method('getFileInfo') - ->willReturn($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL, 'fileid' => 1])); - - $node = $this->createTestNode($this->root, $this->view, '/bar/foo'); - $parentNode = new Folder($this->root, $this->view, '/bar'); - - $this->root->expects($this->any()) - ->method('get') - ->willReturnMap([['/bar', $parentNode], ['/bar/asd', $node]]); - - $node->copy('/bar/asd'); - } -} --- a/tests/lib/Files/Node/RootTest.php +++ /dev/null @@ -1,252 +0,0 @@ -user = $this->createMock(IUser::class); - $this->manager = $this->getMockBuilder(Manager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->userMountCache = $this->getMockBuilder('\OCP\Files\Config\IUserMountCache') - ->disableOriginalConstructor() - ->getMock(); - $this->logger = $this->createMock(LoggerInterface::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cacheFactory->method('createLocal') - ->willReturnCallback(function () { - return new ArrayCache(); - }); - $this->appConfig = $this->createMock(IAppConfig::class); - } - - /** - * @return View&MockObject $view - */ - protected function getRootViewMock() { - $view = $this->createMock(View::class); - $view->expects($this->any()) - ->method('getRoot') - ->willReturn(''); - return $view; - } - - protected function getFileInfo($data) { - return new FileInfo('', null, '', $data, null); - } - - public function testGet(): void { - /** - * @var Storage $storage - */ - $storage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor() - ->getMock(); - $view = $this->getRootViewMock(); - $root = new Root( - $this->manager, - $view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - - $view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn($this->getFileInfo(['fileid' => 10, 'path' => 'bar/foo', 'name', 'mimetype' => 'text/plain'])); - - $root->mount($storage, ''); - $node = $root->get('/bar/foo'); - $this->assertEquals(10, $node->getId()); - $this->assertInstanceOf('\OC\Files\Node\File', $node); - } - - public function testGetNotFound(): void { - $this->expectException(NotFoundException::class); - - /** - * @var Storage $storage - */ - $storage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor() - ->getMock(); - $view = $this->getRootViewMock(); - $root = new Root( - $this->manager, - $view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - - $view->expects($this->once()) - ->method('getFileInfo') - ->with('/bar/foo') - ->willReturn(false); - - $root->mount($storage, ''); - $root->get('/bar/foo'); - } - - public function testGetInvalidPath(): void { - $this->expectException(NotPermittedException::class); - - $view = $this->getRootViewMock(); - $root = new Root( - $this->manager, - $view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - - $root->get('/../foo'); - } - - public function testGetNoStorages(): void { - $this->expectException(NotFoundException::class); - - $view = $this->getRootViewMock(); - $root = new Root( - $this->manager, - $view, - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - - $root->get('/bar/foo'); - } - - public function testGetUserFolder(): void { - $root = new Root( - $this->manager, - $this->getRootViewMock(), - $this->user, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUserId'); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('MyUserId') - ->willReturn($user); - /** @var CappedMemoryCache&MockObject $cappedMemoryCache */ - $cappedMemoryCache = $this->createMock(CappedMemoryCache::class); - $cappedMemoryCache - ->expects($this->once()) - ->method('hasKey') - ->willReturn(true); - $folder = $this->createMock(Folder::class); - $cappedMemoryCache - ->expects($this->once()) - ->method('get') - ->with('MyUserId') - ->willReturn($folder); - - $this->invokePrivate($root, 'userFolderCache', [$cappedMemoryCache]); - $this->assertEquals($folder, $root->getUserFolder('MyUserId')); - } - - public function testGetUserFolderWithNoUserObj(): void { - $this->expectException(UserNotFoundException::class); - $this->expectExceptionMessage('Backends provided no user object'); - - $root = new Root( - $this->createMock(Manager::class), - $this->getRootViewMock(), - null, - $this->userMountCache, - $this->logger, - $this->userManager, - $this->eventDispatcher, - $this->cacheFactory, - $this->appConfig, - ); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('NotExistingUser') - ->willReturn(null); - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Backends provided no user object for NotExistingUser', - $this->anything() - ); - - $root->getUserFolder('NotExistingUser'); - } -} --- a/tests/lib/Files/ObjectStore/AzureTest.php +++ /dev/null @@ -1,30 +0,0 @@ -getSystemValue('objectstore'); - if (!is_array($config) || $config['class'] !== 'OC\\Files\\ObjectStore\\Azure') { - $this->markTestSkipped('objectstore not configured for azure'); - } - - return new Azure($config['arguments']); - } - - #[\Override] - public function testFseekSize(): void { - $this->markTestSkipped('azure does not support seeking at the moment'); - } -} --- a/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php +++ /dev/null @@ -1,53 +0,0 @@ -objectStore->getStorageId(); - } - - #[\Override] - public function readObject($urn) { - return $this->objectStore->readObject($urn); - } - - #[\Override] - public function writeObject($urn, $stream, ?string $mimetype = null) { - return $this->objectStore->writeObject($urn, $stream, $mimetype); - } - - #[\Override] - public function deleteObject($urn) { - throw new \Exception(); - } - - #[\Override] - public function objectExists($urn) { - return $this->objectStore->objectExists($urn); - } - - #[\Override] - public function copyObject($from, $to) { - $this->objectStore->copyObject($from, $to); - } - - #[\Override] - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { - return null; - } -} --- a/tests/lib/Files/ObjectStore/FailWriteObjectStore.php +++ /dev/null @@ -1,54 +0,0 @@ -objectStore->getStorageId(); - } - - #[\Override] - public function readObject($urn) { - return $this->objectStore->readObject($urn); - } - - #[\Override] - public function writeObject($urn, $stream, ?string $mimetype = null) { - // emulate a failed write that didn't throw an error - return true; - } - - #[\Override] - public function deleteObject($urn) { - $this->objectStore->deleteObject($urn); - } - - #[\Override] - public function objectExists($urn) { - return $this->objectStore->objectExists($urn); - } - - #[\Override] - public function copyObject($from, $to) { - $this->objectStore->copyObject($from, $to); - } - - #[\Override] - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { - return null; - } -} --- a/tests/lib/Files/ObjectStore/LocalTest.php +++ /dev/null @@ -1,25 +0,0 @@ -user = $this->createMock(IUser::class); - } - - public static function dataGetBucket(): array { - return [ - ['user', 64, 0, '17'], - ['user', 64, 64, '64'], - ['USER', 64, 0, '0'], - ['bc0e8b52-a66c-1035-90c6-d9663bda9e3f', 64, 0, '56'], - ['user', 8, 0, '1'], - ['user', 2, 0, '1'], - ['USER', 2, 0, '0'], - ['user', 128, 64, '81'], - ]; - } - - /** - * @param string $username - * @param int $numBuckets - * @param string $expectedBucket - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetBucket')] - public function testGetBucket($username, $numBuckets, $bucketShift, $expectedBucket): void { - $mapper = new Mapper($this->user, ['arguments' => ['min_bucket' => $bucketShift]]); - $this->user->method('getUID') - ->willReturn($username); - - $result = $mapper->getBucket($numBuckets); - $this->assertEquals($expectedBucket, $result); - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreScannerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -storage = new Temporary([]); - $this->cache = $this->storage->getCache(); - $this->scanner = new ObjectStoreScanner($this->storage); - $this->realScanner = new Scanner($this->storage); - } - - public function testFile(): void { - $data = "dummy file data\n"; - $this->storage->file_put_contents('foo.txt', $data); - - $this->assertNull($this->scanner->scanFile('foo.txt'), - 'Asserting that no error occurred while scanFile()' - ); - } - - private function fillTestFolders() { - $textData = "dummy file data\n"; - $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $this->storage->mkdir('folder'); - $this->storage->file_put_contents('foo.txt', $textData); - $this->storage->file_put_contents('foo.png', $imgData); - $this->storage->file_put_contents('folder/bar.txt', $textData); - } - - public function testFolder(): void { - $this->fillTestFolders(); - - $this->assertNull( - $this->scanner->scan(''), - 'Asserting that no error occurred while scan()' - ); - } - - public function testBackgroundScan(): void { - $this->fillTestFolders(); - $this->storage->mkdir('folder2'); - $this->storage->file_put_contents('folder2/bar.txt', 'foobar'); - - $this->realScanner->scan(''); - - $this->assertEquals(6, $this->cache->get('folder2')->getSize()); - - $this->cache->put('folder2', ['size' => -1]); - - $this->assertEquals(-1, $this->cache->get('folder2')->getSize()); - - $this->scanner->backgroundScan(); - - $this->assertEquals(6, $this->cache->get('folder2')->getSize()); - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreStorageOverwrite.php +++ /dev/null @@ -1,30 +0,0 @@ -objectStore = $objectStore; - } - - #[\Override] - public function getObjectStore(): IObjectStore { - return $this->objectStore; - } - - public function setValidateWrites(bool $validate): void { - $this->validateWrites = $validate; - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreStorageTest.php +++ /dev/null @@ -1,264 +0,0 @@ -objectStorage = new StorageObjectStore($baseStorage); - $config['objectstore'] = $this->objectStorage; - $this->instance = new ObjectStoreStorageOverwrite($config); - } - - #[\Override] - protected function tearDown(): void { - if (is_null($this->instance)) { - return; - } - $this->instance->getCache()->clear(); - - parent::tearDown(); - } - - #[\Override] - public function testStat(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $ctimeStart = time(); - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); - $this->assertTrue($this->instance->isReadable('/lorem.txt')); - $ctimeEnd = time(); - $mTime = $this->instance->filemtime('/lorem.txt'); - - // check that ($ctimeStart - 5) <= $mTime <= ($ctimeEnd + 1) - $this->assertGreaterThanOrEqual(($ctimeStart - 5), $mTime); - $this->assertLessThanOrEqual(($ctimeEnd + 1), $mTime); - $this->assertEquals(filesize($textFile), $this->instance->filesize('/lorem.txt')); - - $stat = $this->instance->stat('/lorem.txt'); - //only size and mtime are required in the result - $this->assertEquals($stat['size'], $this->instance->filesize('/lorem.txt')); - $this->assertEquals($stat['mtime'], $mTime); - - if ($this->instance->touch('/lorem.txt', 100) !== false) { - $mTime = $this->instance->filemtime('/lorem.txt'); - $this->assertEquals($mTime, 100); - } - } - - #[\Override] - public function testCheckUpdate(): void { - $this->markTestSkipped('Detecting external changes is not supported on object storages'); - } - - #[\Override] - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testMove($source, $target): void { - $this->initSourceAndTarget($source); - $sourceId = $this->instance->getCache()->getId(ltrim($source, '/')); - $this->assertNotEquals(-1, $sourceId); - - $this->instance->rename($source, $target); - - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertFalse($this->instance->file_exists($source), $source . ' still exists'); - $this->assertSameAsLorem($target); - - $targetId = $this->instance->getCache()->getId(ltrim($target, '/')); - $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break'); - } - - #[\Override] - public function testRenameDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - $this->instance->file_put_contents('source/test2.txt', 'qwerty'); - $this->instance->mkdir('source/subfolder'); - $this->instance->file_put_contents('source/subfolder/test.txt', 'bar'); - $sourceId = $this->instance->getCache()->getId('source'); - $this->assertNotEquals(-1, $sourceId); - $this->instance->rename('source', 'target'); - - $this->assertFalse($this->instance->file_exists('source')); - $this->assertFalse($this->instance->file_exists('source/test1.txt')); - $this->assertFalse($this->instance->file_exists('source/test2.txt')); - $this->assertFalse($this->instance->file_exists('source/subfolder')); - $this->assertFalse($this->instance->file_exists('source/subfolder/test.txt')); - - $this->assertTrue($this->instance->file_exists('target')); - $this->assertTrue($this->instance->file_exists('target/test1.txt')); - $this->assertTrue($this->instance->file_exists('target/test2.txt')); - $this->assertTrue($this->instance->file_exists('target/subfolder')); - $this->assertTrue($this->instance->file_exists('target/subfolder/test.txt')); - - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - $this->assertEquals('qwerty', $this->instance->file_get_contents('target/test2.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents('target/subfolder/test.txt')); - $targetId = $this->instance->getCache()->getId('target'); - $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break'); - } - - #[\Override] - public function testRenameOverWriteDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - $sourceId = $this->instance->getCache()->getId('source'); - $this->assertNotEquals(-1, $sourceId); - - $this->instance->mkdir('target'); - $this->instance->file_put_contents('target/test1.txt', 'bar'); - $this->instance->file_put_contents('target/test2.txt', 'bar'); - - $this->instance->rename('source', 'target'); - - $this->assertFalse($this->instance->file_exists('source')); - $this->assertFalse($this->instance->file_exists('source/test1.txt')); - $this->assertFalse($this->instance->file_exists('target/test2.txt')); - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - $targetId = $this->instance->getCache()->getId('target'); - $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break'); - } - - #[\Override] - public function testRenameOverWriteDirectoryOverFile(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - $sourceId = $this->instance->getCache()->getId('source'); - $this->assertNotEquals(-1, $sourceId); - - $this->instance->file_put_contents('target', 'bar'); - - $this->instance->rename('source', 'target'); - - $this->assertFalse($this->instance->file_exists('source')); - $this->assertFalse($this->instance->file_exists('source/test1.txt')); - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - $targetId = $this->instance->getCache()->getId('target'); - $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break'); - } - - public function testWriteObjectSilentFailure(): void { - $objectStore = $this->instance->getObjectStore(); - $this->instance->setObjectStore(new FailWriteObjectStore($objectStore)); - - try { - $this->instance->file_put_contents('test.txt', 'foo'); - $this->fail('expected exception'); - } catch (\Exception $e) { - $this->assertStringStartsWith('Object not found after writing', $e->getMessage()); - } - $this->assertFalse($this->instance->file_exists('test.txt')); - } - - public function testWriteObjectSilentFailureNoCheck(): void { - $objectStore = $this->instance->getObjectStore(); - $this->instance->setObjectStore(new FailWriteObjectStore($objectStore)); - $this->instance->setValidateWrites(false); - - $this->instance->file_put_contents('test.txt', 'foo'); - $this->assertTrue($this->instance->file_exists('test.txt')); - } - - public function testDeleteObjectFailureKeepCache(): void { - $objectStore = $this->instance->getObjectStore(); - $this->instance->setObjectStore(new FailDeleteObjectStore($objectStore)); - $cache = $this->instance->getCache(); - - $this->instance->file_put_contents('test.txt', 'foo'); - - $this->assertTrue($cache->inCache('test.txt')); - - $this->assertFalse($this->instance->unlink('test.txt')); - - $this->assertTrue($cache->inCache('test.txt')); - - $this->instance->mkdir('foo'); - $this->instance->file_put_contents('foo/test.txt', 'foo'); - - $this->assertTrue($cache->inCache('foo')); - $this->assertTrue($cache->inCache('foo/test.txt')); - - $this->instance->rmdir('foo'); - - $this->assertTrue($cache->inCache('foo')); - $this->assertTrue($cache->inCache('foo/test.txt')); - } - - public function testCopyBetweenJails(): void { - $this->instance->mkdir('a'); - $this->instance->mkdir('b'); - $jailA = new Jail([ - 'storage' => $this->instance, - 'root' => 'a' - ]); - $jailB = new Jail([ - 'storage' => $this->instance, - 'root' => 'b' - ]); - $jailA->mkdir('sub'); - $jailA->file_put_contents('1.txt', '1'); - $jailA->file_put_contents('sub/2.txt', '2'); - $jailA->file_put_contents('sub/3.txt', '3'); - - $jailB->copyFromStorage($jailA, '', 'target'); - - $this->assertEquals('1', $this->instance->file_get_contents('b/target/1.txt')); - $this->assertEquals('2', $this->instance->file_get_contents('b/target/sub/2.txt')); - $this->assertEquals('3', $this->instance->file_get_contents('b/target/sub/3.txt')); - } - - public function testCopyGrantsPermissions(): void { - $config['objectstore'] = $this->objectStorage; - $instance = new ObjectStoreStorageOverwrite($config); - - $cache = $instance->getCache(); - - $instance->file_put_contents('test.txt', 'foo'); - $this->assertTrue($cache->inCache('test.txt')); - - $cache->update($cache->getId('test.txt'), ['permissions' => Constants::PERMISSION_READ]); - $this->assertEquals(Constants::PERMISSION_READ, $instance->getPermissions('test.txt')); - - $this->assertTrue($instance->copy('test.txt', 'new.txt')); - - $this->assertTrue($cache->inCache('new.txt')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $instance->getPermissions('new.txt')); - } - - public function testCopyFolderSize(): void { - $cache = $this->instance->getCache(); - - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test.txt', 'foo'); - $this->instance->getUpdater()->update('source/test.txt'); - $this->assertEquals(3, $cache->get('source')->getSize()); - - $this->assertTrue($this->instance->copy('source', 'target')); - - $this->assertEquals(3, $cache->get('target')->getSize()); - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreStoragesDifferentBucketTest.php +++ /dev/null @@ -1,44 +0,0 @@ -objectStore1 = new StorageObjectStore($baseStorage1); - $config['objectstore'] = $this->objectStore1; - $this->storage1 = new ObjectStoreStorageOverwrite($config); - - $baseStorage2 = new Temporary(); - $this->objectStore2 = new StorageObjectStore($baseStorage2); - $config['objectstore'] = $this->objectStore2; - $this->storage2 = new ObjectStoreStorageOverwrite($config); - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreStoragesSameBucketTest.php +++ /dev/null @@ -1,36 +0,0 @@ -objectStore = new StorageObjectStore($baseStorage); - $config['objectstore'] = $this->objectStore; - // storage1 and storage2 share the same object store. - $this->storage1 = new ObjectStoreStorageOverwrite($config); - $this->storage2 = new ObjectStoreStorageOverwrite($config); - } -} --- a/tests/lib/Files/ObjectStore/ObjectStoreTestCase.php +++ /dev/null @@ -1,186 +0,0 @@ -cleanup[] = $urn; - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->instance = $this->getInstance(); - } - - #[\Override] - public function tearDown(): void { - if ($this->instance) { - foreach ($this->cleanup as $urn) { - $this->instance->deleteObject($urn); - } - } - - parent::tearDown(); - } - - protected function stringToStream($data) { - $stream = fopen('php://temp', 'w+'); - fwrite($stream, $data); - rewind($stream); - return $stream; - } - - public function testWriteRead(): void { - $stream = $this->stringToStream('foobar'); - - $instance = $this->getInstance(); - - $instance->writeObject('1', $stream); - - $result = $instance->readObject('1'); - $instance->deleteObject('1'); - - $this->assertEquals('foobar', stream_get_contents($result)); - } - - protected function assertOnlyExpectedWarnings(array $warnings): void { - $onlyFopenWarnings = array_reduce( - $warnings, - fn (bool $ok, string $warning) => $ok && str_starts_with($warning, 'fopen('), - true, - ); - $this->assertTrue($onlyFopenWarnings); - } - - public function testDelete(): void { - $stream = $this->stringToStream('foobar'); - - $instance = $this->getInstance(); - - $instance->writeObject('2', $stream); - - $instance->deleteObject('2'); - - $warnings = []; - try { - set_error_handler( - function (int $errno, string $errstr) use (&$warnings): void { - if ($errno === E_DEPRECATED || $errno === E_USER_DEPRECATED) { - return; - } - $warnings[] = $errstr; - }, - ); - // to to read to verify that the object no longer exists - $instance->readObject('2'); - $this->fail(); - } catch (\Exception $e) { - $this->assertOnlyExpectedWarnings($warnings); - } finally { - restore_error_handler(); - } - } - - public function testReadNonExisting(): void { - $instance = $this->getInstance(); - - $warnings = []; - try { - set_error_handler( - function (int $errno, string $errstr) use (&$warnings): void { - if ($errno === E_DEPRECATED || $errno === E_USER_DEPRECATED) { - return; - } - $warnings[] = $errstr; - }, - ); - $instance->readObject('non-existing'); - $this->fail(); - } catch (\Exception $e) { - $this->assertOnlyExpectedWarnings($warnings); - } finally { - restore_error_handler(); - } - } - - public function testDeleteNonExisting(): void { - $instance = $this->getInstance(); - - try { - $instance->deleteObject('non-existing'); - $this->fail(); - } catch (\Exception $e) { - // dummy assert to keep phpunit happy - $this->assertEquals(1, 1); - } - } - - public function testExists(): void { - $stream = $this->stringToStream('foobar'); - - $instance = $this->getInstance(); - $this->assertFalse($instance->objectExists('2')); - - $instance->writeObject('2', $stream); - - $this->assertTrue($instance->objectExists('2')); - - $instance->deleteObject('2'); - - $this->assertFalse($instance->objectExists('2')); - } - - public function testCopy(): void { - $this->cleanupAfter('source'); - $this->cleanupAfter('target'); - - $stream = $this->stringToStream('foobar'); - - $instance = $this->getInstance(); - - $instance->writeObject('source', $stream); - - $this->assertFalse($instance->objectExists('target')); - - $instance->copyObject('source', 'target'); - - $this->assertTrue($instance->objectExists('target')); - - $this->assertEquals('foobar', stream_get_contents($instance->readObject('target'))); - } - - public function testFseekSize(): void { - $instance = $this->getInstance(); - - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $size = filesize($textFile); - $instance->writeObject('source', fopen($textFile, 'r')); - - $fh = $instance->readObject('source'); - - fseek($fh, 0, SEEK_END); - $pos = ftell($fh); - - $this->assertEquals($size, $pos); - } -} --- a/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php +++ /dev/null @@ -1,296 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace lib\Files\ObjectStore; - -use OC\Files\ObjectStore\PrimaryObjectStoreConfig; -use OC\Files\ObjectStore\StorageObjectStore; -use OCP\App\IAppManager; -use OCP\IConfig; -use OCP\IUser; -use PHPUnit\Framework\MockObject\MockObject; -use Test\TestCase; - -class PrimaryObjectStoreConfigTest extends TestCase { - private array $systemConfig = []; - private array $userConfig = []; - private IConfig&MockObject $config; - private IAppManager&MockObject $appManager; - private PrimaryObjectStoreConfig $objectStoreConfig; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->systemConfig = []; - $this->config = $this->createMock(IConfig::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->config->method('getSystemValue') - ->willReturnCallback(function ($key, $default = '') { - if (isset($this->systemConfig[$key])) { - return $this->systemConfig[$key]; - } else { - return $default; - } - }); - $this->config->method('getUserValue') - ->willReturnCallback(function ($userId, $appName, $key, $default = '') { - if (isset($this->userConfig[$userId][$appName][$key])) { - return $this->userConfig[$userId][$appName][$key]; - } else { - return $default; - } - }); - $this->config->method('setUserValue') - ->willReturnCallback(function ($userId, $appName, $key, $value): void { - $this->userConfig[$userId][$appName][$key] = $value; - }); - - $this->objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->appManager); - } - - private function getUser(string $uid): IUser { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn($uid); - return $user; - } - - private function setConfig(string $key, $value) { - $this->systemConfig[$key] = $value; - } - - public function testNewUserGetsDefault() { - $this->setConfig('objectstore', [ - 'default' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test')); - $this->assertEquals('server1', $result['arguments']['host']); - - $this->assertEquals('server1', $this->config->getUserValue('test', 'homeobjectstore', 'objectstore', null)); - } - - public function testExistingUserKeepsStorage() { - // setup user with `server1` as storage - $this->testNewUserGetsDefault(); - - $this->setConfig('objectstore', [ - 'default' => 'server2', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'bucket' => '1', - ], - ], - 'server2' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server2', - 'bucket' => '2', - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test')); - $this->assertEquals('server1', $result['arguments']['host']); - - $this->assertEquals('server1', $this->config->getUserValue('test', 'homeobjectstore', 'objectstore', null)); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('other-user')); - $this->assertEquals('server2', $result['arguments']['host']); - } - - public function testNestedAliases() { - $this->setConfig('objectstore', [ - 'default' => 'a1', - 'a1' => 'a2', - 'a2' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'bucket' => '1', - ], - ], - ]); - $this->assertEquals('server1', $this->objectStoreConfig->resolveAlias('default')); - } - - public function testMultibucketChangedConfig() { - $this->setConfig('objectstore', [ - 'default' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => true, - 'num_buckets' => 8, - 'bucket' => 'bucket1-' - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test')); - $this->assertEquals('server1', $result['arguments']['host']); - $this->assertEquals('bucket1-7', $result['arguments']['bucket']); - - $this->setConfig('objectstore', [ - 'default' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => true, - 'num_buckets' => 64, - 'bucket' => 'bucket1-' - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test')); - $this->assertEquals('server1', $result['arguments']['host']); - $this->assertEquals('bucket1-7', $result['arguments']['bucket']); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test-foo')); - $this->assertEquals('server1', $result['arguments']['host']); - $this->assertEquals('bucket1-40', $result['arguments']['bucket']); - - $this->setConfig('objectstore', [ - 'default' => 'server2', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => true, - 'num_buckets' => 64, - 'bucket' => 'bucket1-' - ], - ], - 'server2' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server2', - 'multibucket' => true, - 'num_buckets' => 16, - 'bucket' => 'bucket2-' - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test')); - $this->assertEquals('server1', $result['arguments']['host']); - $this->assertEquals('bucket1-7', $result['arguments']['bucket']); - - $result = $this->objectStoreConfig->getObjectStoreConfigForUser($this->getUser('test-bar')); - $this->assertEquals('server2', $result['arguments']['host']); - $this->assertEquals('bucket2-4', $result['arguments']['bucket']); - } - - public function testMultibucketOldConfig() { - $this->setConfig('objectstore_multibucket', [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => true, - 'num_buckets' => 8, - 'bucket' => 'bucket-' - ], - ]); - $configs = $this->objectStoreConfig->getObjectStoreConfigs(); - $this->assertEquals([ - 'default' => 'server1', - 'root' => 'server1', - 'preview' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => true, - 'num_buckets' => 8, - 'bucket' => 'bucket-' - ], - ], - ], $configs); - } - - public function testSingleObjectStore() { - $this->setConfig('objectstore', [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - ], - ]); - $configs = $this->objectStoreConfig->getObjectStoreConfigs(); - $this->assertEquals([ - 'default' => 'server1', - 'root' => 'server1', - 'preview' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'multibucket' => false, - ], - ], - ], $configs); - } - - public function testRoot() { - $this->setConfig('objectstore', [ - 'default' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'bucket' => '1', - ], - ], - 'server2' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server2', - 'bucket' => '2', - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForRoot(); - $this->assertEquals('server1', $result['arguments']['host']); - - $this->setConfig('objectstore', [ - 'default' => 'server1', - 'root' => 'server2', - 'preview' => 'server1', - 'server1' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server1', - 'bucket' => '1', - ], - ], - 'server2' => [ - 'class' => StorageObjectStore::class, - 'arguments' => [ - 'host' => 'server2', - 'bucket' => '2', - ], - ], - ]); - - $result = $this->objectStoreConfig->getObjectStoreConfigForRoot(); - $this->assertEquals('server2', $result['arguments']['host']); - } -} --- a/tests/lib/Files/ObjectStore/S3ContentMd5MiddlewareTest.php +++ /dev/null @@ -1,175 +0,0 @@ -getUri()->getQuery() !== 'delete') { - return $request; - } - - if (!$request->hasHeader('Content-MD5')) { - $body = $request->getBody(); - $contentMd5 = base64_encode(Utils::hash($body, 'md5', true)); - return $request->withHeader('Content-MD5', $contentMd5); - } - - return $request; - } - - /** - * Test that Content-MD5 header is added to DeleteObjects requests - */ - public function testContentMd5HeaderAddedToDeleteObjects(): void { - $testBody = 'test-key'; - $request = new Request('POST', 'http://s3.example.com/bucket?delete', [], $testBody); - - // Calculate expected MD5 - $expectedMd5 = base64_encode(md5($testBody, true)); - - // Apply middleware logic - $resultRequest = $this->applyContentMd5Middleware($request); - - // Verify header was added - $this->assertTrue($resultRequest->hasHeader('Content-MD5')); - $this->assertEquals($expectedMd5, $resultRequest->getHeaderLine('Content-MD5')); - } - - /** - * Test that Content-MD5 header is NOT added to non-DeleteObjects requests - */ - public function testContentMd5NotAddedToNonDeleteRequests(): void { - $testCases = [ - 'GET request' => new Request('GET', 'http://s3.example.com/bucket/key'), - 'PUT request' => new Request('PUT', 'http://s3.example.com/bucket/key'), - 'HEAD request' => new Request('HEAD', 'http://s3.example.com/bucket/key'), - 'POST with different query' => new Request('POST', 'http://s3.example.com/bucket?uploads'), - ]; - - foreach ($testCases as $label => $request) { - $resultRequest = $this->applyContentMd5Middleware($request); - - // Verify header was NOT added for non-delete requests - $this->assertFalse($resultRequest->hasHeader('Content-MD5'), "Content-MD5 should not be added for: $label"); - } - } - - /** - * Test that existing Content-MD5 header is preserved - */ - public function testExistingContentMd5HeaderPreserved(): void { - $testBody = 'test data'; - $existingMd5 = 'existing-md5-value'; - $request = new Request( - 'POST', - 'http://s3.example.com/bucket?delete', - ['Content-MD5' => $existingMd5], - $testBody - ); - - // Apply middleware logic - $resultRequest = $this->applyContentMd5Middleware($request); - - // Verify existing header was preserved - $this->assertTrue($resultRequest->hasHeader('Content-MD5')); - $this->assertEquals($existingMd5, $resultRequest->getHeaderLine('Content-MD5')); - } - - /** - * Test MD5 calculation with various body sizes - */ - public function testMd5CalculationWithVariousSizes(): void { - $testBodies = [ - 'small' => 'x', - 'medium' => str_repeat('y', 1000), - 'large' => str_repeat('z', 10000), - 'xml_payload' => 'file1.txtfile2.txt', - ]; - - foreach ($testBodies as $label => $body) { - $request = new Request('POST', 'http://s3.example.com/bucket?delete', [], $body); - $expectedMd5 = base64_encode(md5($body, true)); - - $resultRequest = $this->applyContentMd5Middleware($request); - - $this->assertEquals( - $expectedMd5, - $resultRequest->getHeaderLine('Content-MD5'), - "MD5 mismatch for $label body size" - ); - } - } - - /** - * Test MD5 header format is base64-encoded - */ - public function testMd5HeaderFormatIsBase64(): void { - $testBody = 'test data for base64 validation'; - $request = new Request('POST', 'http://s3.example.com/bucket?delete', [], $testBody); - - $resultRequest = $this->applyContentMd5Middleware($request); - - $md5Header = $resultRequest->getHeaderLine('Content-MD5'); - - // Verify it's a valid base64 string - $this->assertNotEmpty($md5Header); - $this->assertEquals($md5Header, base64_encode(base64_decode($md5Header, true))); - - // Verify MD5 is typically 24 chars when base64-encoded (16 bytes) - $this->assertEquals(24, strlen($md5Header)); - } - - /** - * Test edge case: Empty body in DeleteObjects request - */ - public function testMd5CalculationWithEmptyBody(): void { - $request = new Request('POST', 'http://s3.example.com/bucket?delete', [], ''); - - $resultRequest = $this->applyContentMd5Middleware($request); - - // MD5 of empty string should still produce a valid header - $this->assertTrue($resultRequest->hasHeader('Content-MD5')); - $this->assertNotEmpty($resultRequest->getHeaderLine('Content-MD5')); - } - - /** - * Test that middleware is idempotent (doesn't double-hash) - */ - public function testMiddlewareIsIdempotent(): void { - $testBody = 'test data'; - $request = new Request('POST', 'http://s3.example.com/bucket?delete', [], $testBody); - - // Apply middleware twice - $resultRequest1 = $this->applyContentMd5Middleware($request); - $resultRequest2 = $this->applyContentMd5Middleware($resultRequest1); - - // Headers should be identical - $this->assertEquals( - $resultRequest1->getHeaderLine('Content-MD5'), - $resultRequest2->getHeaderLine('Content-MD5'), - 'Middleware should be idempotent' - ); - } -} --- a/tests/lib/Files/ObjectStore/S3SSEKMSTest.php +++ /dev/null @@ -1,399 +0,0 @@ -getSystemValue('objectstore'); - if (!is_array($config) || $config['class'] !== S3::class) { - self::markTestSkipped('S3 primary storage not configured'); - } - - $arguments = $config['arguments'] ?? []; - if (empty($arguments['sse_kms_enabled'])) { - self::markTestSkipped('SSE-KMS not enabled. Set sse_kms_enabled=true in objectstore config'); - } - } - - #[\Override] - protected function getInstance() { - if (!isset($this->instance)) { - $config = Server::get(IConfig::class)->getSystemValue('objectstore'); - $this->instance = new S3($config['arguments']); - } - return $this->instance; - } - - /** - * Test basic write and read with SSE-KMS - */ - public function testWriteReadWithKMS(): void { - $this->cleanupAfter('kms-test-write-read'); - - $s3 = $this->getInstance(); - $data = 'Test data for SSE-KMS encryption'; - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write with SSE-KMS - $s3->writeObject('kms-test-write-read', $stream); - - // Read back - $result = $s3->readObject('kms-test-write-read'); - $readData = stream_get_contents($result); - fclose($result); - - $this->assertEquals($data, $readData, 'Data should be readable after SSE-KMS encryption'); - } - - /** - * Test copy operation with SSE-KMS - */ - public function testCopyWithKMS(): void { - $this->cleanupAfter('kms-test-copy-source'); - $this->cleanupAfter('kms-test-copy-target'); - - $s3 = $this->getInstance(); - $data = 'Test data for SSE-KMS copy operation'; - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write source file - $s3->writeObject('kms-test-copy-source', $stream); - - // Copy (should re-encrypt with same KMS key) - $s3->copyObject('kms-test-copy-source', 'kms-test-copy-target'); - - // Verify copy - $this->assertTrue($s3->objectExists('kms-test-copy-target'), 'Copied object should exist'); - - $result = $s3->readObject('kms-test-copy-target'); - $readData = stream_get_contents($result); - fclose($result); - - $this->assertEquals($data, $readData, 'Copied data should match original'); - } - - /** - * Test multipart upload with SSE-KMS - */ - public function testMultipartUploadWithKMS(): void { - $this->cleanupAfter('kms-test-multipart'); - - $s3 = $this->getInstance(); - - // Create 6MB data to trigger multipart - $data = str_repeat('A', 6 * 1024 * 1024); - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write with multipart (forces multipart upload) - $s3->writeObject('kms-test-multipart', $stream, 'application/octet-stream'); - - // Verify - $this->assertTrue($s3->objectExists('kms-test-multipart'), 'Multipart object should exist'); - - // Read back first 1000 bytes to verify - $result = $s3->readObject('kms-test-multipart'); - $readData = fread($result, 1000); - fclose($result); - - $this->assertEquals(substr($data, 0, 1000), $readData, 'Multipart data should be readable'); - } - - /** - * Data provider for various file sizes - */ - public static function dataFileSizes(): array { - return [ - '1KB' => [1024], - '1MB' => [1024 * 1024], - '10MB' => [10 * 1024 * 1024], - ]; - } - - /** - * Data provider for large file sizes to test multipart upload threshold behavior - */ - public static function dataLargeFileSizes(): array { - return [ - '50MB' => [50 * 1024 * 1024], - '100MB' => [100 * 1024 * 1024], - '150MB' => [150 * 1024 * 1024], - ]; - } - - /** - * Test various file sizes with SSE-KMS - * - * @dataProvider dataFileSizes - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataFileSizes')] - public function testFileSizesWithKMS(int $size): void { - $urn = 'kms-test-size-' . ($size / 1024) . 'kb'; - $this->cleanupAfter($urn); - - $s3 = $this->getInstance(); - $data = str_repeat('X', $size); - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write - $s3->writeObject($urn, $stream); - - // Verify object exists - $this->assertTrue($s3->objectExists($urn), "Object should exist for size $size"); - - // Read back and verify - $result = $s3->readObject($urn); - $readData = stream_get_contents($result); - fclose($result); - - $this->assertEquals($size, strlen($readData), "Size mismatch for $size byte file"); - $this->assertEquals($data, $readData, "Content mismatch for $size byte file"); - } - - /** - * Test that SSE-KMS metadata is set on objects - */ - public function testKMSMetadataPresent(): void { - $this->cleanupAfter('kms-test-metadata'); - - $s3 = $this->getInstance(); - $data = 'Test KMS metadata'; - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write with SSE-KMS - $s3->writeObject('kms-test-metadata', $stream); - - // Check metadata via headObject - $result = $s3->getConnection()->headObject([ - 'Bucket' => $s3->getBucket(), - 'Key' => 'kms-test-metadata', - ]); - - // Verify SSE is KMS - $this->assertEquals('aws:kms', $result->get('ServerSideEncryption'), - 'Object should have SSE-KMS encryption'); - - // If specific key configured, verify it's used - $config = Server::get(IConfig::class)->getSystemValue('objectstore'); - if (!empty($config['arguments']['sse_kms_key_id'])) { - $this->assertNotNull($result->get('SSEKMSKeyId'), - 'KMS Key ID should be present when specific key configured'); - } - } - - /** - * Test zero-byte file with SSE-KMS - * - * Note: Zero-byte files are a known edge case with S3. - * While they can be written, reading them back may fail due to - * Range header issues with empty objects. - */ - public function testZeroByteFileWithKMS(): void { - $this->cleanupAfter('kms-test-zerobyte'); - - $s3 = $this->getInstance(); - $stream = fopen('php://temp', 'r+'); - // Write nothing (zero bytes) - rewind($stream); - - // Write zero-byte file - $s3->writeObject('kms-test-zerobyte', $stream); - - // Verify exists - $this->assertTrue($s3->objectExists('kms-test-zerobyte'), 'Zero-byte object should exist'); - - // Verify via headObject instead of read (avoids Range header issue) - $metadata = $s3->getConnection()->headObject([ - 'Bucket' => $s3->getBucket(), - 'Key' => 'kms-test-zerobyte', - ]); - - $this->assertEquals(0, $metadata->get('ContentLength'), - 'Zero-byte file should have ContentLength of 0'); - $this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'), - 'Zero-byte file should still have SSE-KMS encryption'); - } - - /** - * Test large file sizes with SSE-KMS to verify multipart threshold behavior - * - * @dataProvider dataLargeFileSizes - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLargeFileSizes')] - #[\PHPUnit\Framework\Attributes\Group('SLOWDB')] - public function testLargeFileSizesWithKMS(int $size): void { - $urn = 'kms-test-large-size-' . ($size / 1024 / 1024) . 'mb'; - $this->cleanupAfter($urn); - - $s3 = $this->getInstance(); - $data = str_repeat('L', $size); - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write (should trigger multipart for files >= 100MB) - $s3->writeObject($urn, $stream); - - // Verify object exists - $this->assertTrue($s3->objectExists($urn), "Object should exist for size $size"); - - // Verify metadata via headObject - $metadata = $s3->getConnection()->headObject([ - 'Bucket' => $s3->getBucket(), - 'Key' => $urn, - ]); - - $this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'), - "Object should have SSE-KMS encryption for size $size"); - $this->assertEquals($size, $metadata->get('ContentLength'), - "Size should match for $size byte file"); - } - - /** - * Test multipart copy operation with large files - */ - #[\PHPUnit\Framework\Attributes\Group('SLOWDB')] - public function testMultipartCopyWithKMS(): void { - $this->cleanupAfter('kms-test-multipart-copy-source'); - $this->cleanupAfter('kms-test-multipart-copy-target'); - - $s3 = $this->getInstance(); - - // Create large file to trigger multipart copy (> copySizeLimit, default 5GB) - // Use 10MB for test efficiency, but in production this would be > 5GB - $size = 10 * 1024 * 1024; - $data = str_repeat('C', $size); - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write source file - $s3->writeObject('kms-test-multipart-copy-source', $stream); - - // Copy (should re-encrypt with same KMS key) - $s3->copyObject('kms-test-multipart-copy-source', 'kms-test-multipart-copy-target'); - - // Verify copy exists - $this->assertTrue($s3->objectExists('kms-test-multipart-copy-target'), 'Copied object should exist'); - - // Verify encryption on target - $metadata = $s3->getConnection()->headObject([ - 'Bucket' => $s3->getBucket(), - 'Key' => 'kms-test-multipart-copy-target', - ]); - - $this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'), - 'Copied object should have SSE-KMS encryption'); - } - - /** - * Test delete operation with KMS-encrypted objects - */ - public function testDeleteWithKMS(): void { - $this->cleanupAfter('kms-test-delete'); - - $s3 = $this->getInstance(); - $data = 'Test data for delete operation'; - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $data); - rewind($stream); - - // Write with SSE-KMS - $s3->writeObject('kms-test-delete', $stream); - - // Verify exists - $this->assertTrue($s3->objectExists('kms-test-delete'), 'Object should exist before delete'); - - // Delete - $s3->deleteObject('kms-test-delete'); - - // Verify deleted - $this->assertFalse($s3->objectExists('kms-test-delete'), 'Object should not exist after delete'); - } - - /** - * Test overwriting existing KMS-encrypted objects - */ - public function testOverwriteWithKMS(): void { - $this->cleanupAfter('kms-test-overwrite'); - - $s3 = $this->getInstance(); - - // Write initial data - $data1 = 'Initial data for overwrite test'; - $stream1 = fopen('php://temp', 'r+'); - fwrite($stream1, $data1); - rewind($stream1); - $s3->writeObject('kms-test-overwrite', $stream1); - - // Verify initial write - $result1 = $s3->readObject('kms-test-overwrite'); - $readData1 = stream_get_contents($result1); - fclose($result1); - $this->assertEquals($data1, $readData1, 'Initial data should match'); - - // Overwrite with new data - $data2 = 'Overwritten data with different content'; - $stream2 = fopen('php://temp', 'r+'); - fwrite($stream2, $data2); - rewind($stream2); - $s3->writeObject('kms-test-overwrite', $stream2); - - // Verify overwrite - $result2 = $s3->readObject('kms-test-overwrite'); - $readData2 = stream_get_contents($result2); - fclose($result2); - $this->assertEquals($data2, $readData2, 'Overwritten data should match'); - - // Verify still encrypted with KMS - $metadata = $s3->getConnection()->headObject([ - 'Bucket' => $s3->getBucket(), - 'Key' => 'kms-test-overwrite', - ]); - - $this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'), - 'Overwritten object should still have SSE-KMS encryption'); - } -} --- a/tests/lib/Files/ObjectStore/S3Test.php +++ /dev/null @@ -1,193 +0,0 @@ -getConnection()->upload($this->bucket, $urn, $stream, 'private', [ - 'mup_threshold' => 1, - ]); - } -} - -class NonSeekableStream extends Wrapper { - public static function wrap($source) { - $context = stream_context_create([ - 'nonseek' => [ - 'source' => $source, - ], - ]); - return Wrapper::wrapSource($source, $context, 'nonseek', self::class); - } - - #[\Override] - public function dir_opendir($path, $options) { - return false; - } - - #[\Override] - public function stream_open($path, $mode, $options, &$opened_path) { - $this->loadContext('nonseek'); - return true; - } - - #[\Override] - public function stream_seek($offset, $whence = SEEK_SET) { - return false; - } -} - -#[\PHPUnit\Framework\Attributes\Group('PRIMARY-s3')] -class S3Test extends ObjectStoreTestCase { - #[\Override] - public function setUp(): void { - parent::setUp(); - $s3 = $this->getInstance(); - $s3->deleteObject('multiparttest'); - } - - #[\Override] - protected function getInstance() { - $config = Server::get(IConfig::class)->getSystemValue('objectstore'); - if (!is_array($config) || $config['class'] !== S3::class) { - $this->markTestSkipped('objectstore not configured for s3'); - } - - return new S3($config['arguments']); - } - - public function testUploadNonSeekable(): void { - $this->cleanupAfter('multiparttest'); - - $s3 = $this->getInstance(); - - $s3->writeObject('multiparttest', NonSeekableStream::wrap(fopen(__FILE__, 'r'))); - - $result = $s3->readObject('multiparttest'); - - $this->assertEquals(file_get_contents(__FILE__), stream_get_contents($result)); - } - - public function testSeek(): void { - $this->cleanupAfter('seek'); - - $data = file_get_contents(__FILE__); - - $instance = $this->getInstance(); - $instance->writeObject('seek', $this->stringToStream($data)); - - $read = $instance->readObject('seek'); - $this->assertEquals(substr($data, 0, 100), fread($read, 100)); - - fseek($read, 10); - $this->assertEquals(substr($data, 10, 100), fread($read, 100)); - - fseek($read, 100, SEEK_CUR); - $this->assertEquals(substr($data, 210, 100), fread($read, 100)); - } - - public function assertNoUpload($objectUrn) { - /** @var S3 */ - $s3 = $this->getInstance(); - $s3client = $s3->getConnection(); - $uploads = $s3client->listMultipartUploads([ - 'Bucket' => $s3->getBucket(), - 'Prefix' => $objectUrn, - ]); - $this->assertArrayNotHasKey('Uploads', $uploads, 'Assert is not uploaded'); - } - - public function testEmptyUpload(): void { - $s3 = $this->getInstance(); - - $emptyStream = fopen('php://memory', 'r'); - fwrite($emptyStream, ''); - - $warnings = []; - set_error_handler( - function (int $errno, string $errstr) use (&$warnings): void { - if ($errno === E_DEPRECATED || $errno === E_USER_DEPRECATED) { - return; - } - $warnings[] = $errstr; - }, - ); - - $s3->writeObject('emptystream', $emptyStream); - - $this->assertNoUpload('emptystream'); - $this->assertTrue($s3->objectExists('emptystream'), 'Object exists on S3'); - - $thrown = false; - try { - self::assertFalse($s3->readObject('emptystream'), 'Reading empty stream object should return false'); - } catch (\Exception $e) { - // An exception is expected here since 0 byte files are wrapped - // to be read from an empty memory stream in the ObjectStoreStorage - $thrown = true; - } - self::assertTrue($thrown, 'readObject with range requests are not expected to work on empty objects'); - - $s3->deleteObject('emptystream'); - $this->assertOnlyExpectedWarnings($warnings); - restore_error_handler(); - } - - /** File size to upload in bytes */ - public static function dataFileSizes(): array { - return [ - [1000000], [2000000], [5242879], [5242880], [5242881], [10000000] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataFileSizes')] - public function testFileSizes($size): void { - $this->cleanupAfter('testfilesizes'); - $s3 = $this->getInstance(); - - $sourceStream = fopen('php://memory', 'wb+'); - $writeChunkSize = 1024; - $chunk = str_repeat('A', $writeChunkSize); - $remainingSize = $size; - - while ($remainingSize > 0) { - $bytesToWrite = min($writeChunkSize, $remainingSize); - fwrite($sourceStream, ($bytesToWrite === $writeChunkSize) ? $chunk : str_repeat('A', $bytesToWrite)); - $remainingSize -= $bytesToWrite; - } - - rewind($sourceStream); - $s3->writeObject('testfilesizes', $sourceStream); - - $this->assertNoUpload('testfilesizes'); - self::assertTrue($s3->objectExists('testfilesizes'), 'Object exists on S3'); - - $result = $s3->readObject('testfilesizes'); - - // compare first 100 bytes - self::assertSame(str_repeat('A', 100), fread($result, 100), 'Compare first 100 bytes'); - - // compare last 100 bytes - self::assertSame(0, fseek($result, $size - 100), 'Seek to last 100 bytes succeeds'); - self::assertSame(str_repeat('A', 100), fread($result, 100), 'Compare last 100 bytes'); - - // end of file reached - self::assertSame(0, fseek($result, $size), 'Seek to EOF succeeds'); - self::assertSame($size, ftell($result), 'Pointer is at the end of file'); - self::assertSame('', fread($result, 1), 'Reading at end of file returns no bytes'); - self::assertTrue(feof($result), 'End of file reached after read attempt'); - - $this->assertNoUpload('testfilesizes'); - } -} --- a/tests/lib/Files/ObjectStore/SwiftTest.php +++ /dev/null @@ -1,35 +0,0 @@ -getSystemValue('objectstore'); - if (!is_array($config) || $config['class'] !== 'OC\\Files\\ObjectStore\\Swift') { - $this->markTestSkipped('objectstore not configured for swift'); - } - - return new Swift($config['arguments']); - } - - #[\Override] - public function testFseekSize(): void { - $this->markTestSkipped('Swift does not support seeking at the moment'); - } -} --- a/tests/lib/Files/PathVerificationTest.php +++ /dev/null @@ -1,128 +0,0 @@ -view = new View(); - } - - public function testPathVerificationFileNameTooLong(): void { - $this->expectException(InvalidPathException::class); - $this->expectExceptionMessage('Filename is too long'); - - $fileName = str_repeat('a', 500); - $this->view->verifyPath('', $fileName); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesEmptyFiles')] - public function testPathVerificationEmptyFileName($fileName): void { - $this->expectException(InvalidPathException::class); - $this->expectExceptionMessage('Empty filename is not allowed'); - - $this->view->verifyPath('', $fileName); - } - - public static function providesEmptyFiles(): array { - return [ - [''], - [' '], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesDotFiles')] - public function testPathVerificationDotFiles($fileName): void { - $this->expectException(InvalidPathException::class); - $this->expectExceptionMessage('Filenames that begin with a dot are not allowed'); - - $this->view->verifyPath('', $fileName); - } - - public static function providesDotFiles(): array { - return [ - ['.'], - ['..'], - [' .'], - [' ..'], - ['. '], - ['.. '], - [' . '], - [' .. '], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesAstralPlane')] - public function testPathVerificationAstralPlane($fileName): void { - $connection = Server::get(IDBConnection::class); - - if (!$connection->supports4ByteText()) { - $this->expectException(InvalidPathException::class); - $this->expectExceptionMessage('File name contains at least one invalid character'); - } else { - $this->addToAssertionCount(1); - } - - $this->view->verifyPath('', $fileName); - } - - public static function providesAstralPlane(): array { - return [ - // this is the monkey emoji - http://en.wikipedia.org/w/index.php?title=%F0%9F%90%B5&redirect=no - ['🐵'], - ['🐵.txt'], - ['txt.💩'], - ['💩🐵.txt'], - ['💩🐵'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesValidPosixPaths')] - public function testPathVerificationValidPaths($fileName): void { - $storage = new Local(['datadir' => '']); - - self::invokePrivate($storage, 'verifyPosixPath', [$fileName]); - // nothing thrown - $this->addToAssertionCount(1); - } - - public static function providesValidPosixPaths(): array { - return [ - ['simple'], - ['simple.txt'], - ['\''], - ['`'], - ['%'], - ['()'], - ['[]'], - ['!'], - ['$'], - ['_'], - ]; - } -} --- a/tests/lib/Files/Search/QueryOptimizer/CombinedTests.php +++ /dev/null @@ -1,232 +0,0 @@ -optimizer = new QueryOptimizer(); - } - - public function testBasicOrOfAnds(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ); - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(storage eq 1 and path in ["foo","bar","asd"])', $operator->__toString()); - } - - public function testComplexSearchPattern1(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '201'), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', '201/%'), - ]), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '301'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 4), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '401'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '302'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 4), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '402'), - ]), - ] - ); - $this->assertEquals('((storage eq 1) or (storage eq 2 and (path eq "201" or path like "201\/%")) or (storage eq 3 and path eq "301") or (storage eq 4 and path eq "401") or (storage eq 3 and path eq "302") or (storage eq 4 and path eq "402"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(storage eq 1 or (storage eq 2 and (path eq "201" or path like "201\/%")) or (storage eq 3 and path in ["301","302"]) or (storage eq 4 and path in ["401","402"]))', $operator->__toString()); - } - - public function testComplexSearchPattern2(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '201'), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', '201/%'), - ]), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '301'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 4), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '401'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '302'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 4), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', '402'), - ]), - ] - ); - $this->assertEquals('(storage eq 1 or (storage eq 2 and (path eq "201" or path like "201\/%")) or (storage eq 3 and path eq "301") or (storage eq 4 and path eq "401") or (storage eq 3 and path eq "302") or (storage eq 4 and path eq "402"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(storage eq 1 or (storage eq 2 and (path eq "201" or path like "201\/%")) or (storage eq 3 and path in ["301","302"]) or (storage eq 4 and path in ["401","402"]))', $operator->__toString()); - } - - public function testApplySearchConstraints1(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/png'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpeg'), - ]), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files'), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', 'files/%'), - ]), - ]), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files/301'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files/302'), - ]), - ]), - ]); - $this->assertEquals('(((mimetype eq "image\/png" or mimetype eq "image\/jpeg")) and ((storage eq 1 and (path eq "files" or path like "files\/%")) or storage eq 2 or (storage eq 3 and path eq "files\/301") or (storage eq 3 and path eq "files\/302")))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(mimetype in ["image\/png","image\/jpeg"] and ((storage eq 1 and (path eq "files" or path like "files\/%")) or storage eq 2 or (storage eq 3 and path in ["files\/301","files\/302"])))', $operator->__toString()); - } - - public function testApplySearchConstraints2(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/png'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpeg'), - ]), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files'), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', 'files/%'), - ]), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files/301'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'files/302'), - ]), - ]), - ]); - $this->assertEquals('(((mimetype eq "image\/png" or mimetype eq "image\/jpeg")) and ((storage eq 1 and (path eq "files" or path like "files\/%")) or (storage eq 2) or (storage eq 3 and path eq "files\/301") or (storage eq 3 and path eq "files\/302")))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(mimetype in ["image\/png","image\/jpeg"] and ((storage eq 1 and (path eq "files" or path like "files\/%")) or storage eq 2 or (storage eq 3 and path in ["files\/301","files\/302"])))', $operator->__toString()); - } - - public function testNegatedGroupSurvivesTheFullPipeline(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('((not storage eq 1) or (not path eq "foo"))', $operator->__toString()); - } - - public function testNegatedLargeInIsPushedDownAfterSplitting(): void { - $ids = range(1, 1500); - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_IN, 'fileid', $ids), - ]); - - $this->optimizer->processOperator($operator); - - // SplitLargeIn turns the single `in` into `or (in, in)`, which needs pushing back down - // through the `not` a second time before it is something SearchBuilder can turn into SQL. - $chunks = array_chunk($ids, IQueryBuilder::MAX_IN_PARAMETERS); - $expected = '(' . implode(' and ', array_map( - static fn (array $chunk): string => '(not fileid in ' . json_encode($chunk) . ')', - $chunks, - )) . ')'; - $this->assertEquals($expected, $operator->__toString()); - } -} --- a/tests/lib/Files/Search/QueryOptimizer/FlattenNestedBoolTest.php +++ /dev/null @@ -1,48 +0,0 @@ -optimizer = new FlattenNestedBool(); - $this->simplifier = new FlattenSingleArgumentBinaryOperation(); - } - - public function testOrs(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ); - $this->assertEquals('(path eq "foo" or (path eq "bar" or path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(path eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString()); - } -} --- a/tests/lib/Files/Search/QueryOptimizer/MergeDistributiveOperationsTest.php +++ /dev/null @@ -1,166 +0,0 @@ -optimizer = new MergeDistributiveOperations(); - $this->simplifier = new FlattenSingleArgumentBinaryOperation(); - } - - public function testBasicOrOfAnds(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ); - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(storage eq 1 and (path eq "foo" or path eq "bar" or path eq "asd"))', $operator->__toString()); - } - - public function testDontTouchIfNotSame(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 3), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ); - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 2 and path eq "bar") or (storage eq 3 and path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 2 and path eq "bar") or (storage eq 3 and path eq "asd"))', $operator->__toString()); - } - - public function testMergePartial(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 2), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ); - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 2 and path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('((storage eq 1 and (path eq "foo" or path eq "bar")) or (storage eq 2 and path eq "asd"))', $operator->__toString()); - } - - public function testOptimizeInside(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_AND, - [ - new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ]) - ] - ), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'text') - ] - ); - $this->assertEquals('(((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd")) and mimetype eq "text")', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('((storage eq 1 and (path eq "foo" or path eq "bar" or path eq "asd")) and mimetype eq "text")', $operator->__toString()); - } - - public function testMoveInnerOperations(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', '100'), - ]) - ] - ); - $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd" and size gt "100"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(storage eq 1 and (path eq "foo" or path eq "bar" or (path eq "asd" and size gt "100")))', $operator->__toString()); - } -} --- a/tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php +++ /dev/null @@ -1,126 +0,0 @@ -optimizer = new OrEqualsToIn(); - $this->simplifier = new FlattenSingleArgumentBinaryOperation(); - } - - public function testOrs(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ] - ); - $this->assertEquals('(path eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('path in ["foo","bar","asd"]', $operator->__toString()); - } - - public function testOrsMultipleFields(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'fileid', 1), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'fileid', 2), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'asd'), - ] - ); - $this->assertEquals('(path eq "foo" or path eq "bar" or fileid eq 1 or fileid eq 2 or mimetype eq "asd")', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(path in ["foo","bar"] or fileid in [1,2] or mimetype eq "asd")', $operator->__toString()); - } - - public function testPreserveHints(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ] - ); - foreach ($operator->getArguments() as $argument) { - $argument->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false); - } - $this->assertEquals('(path eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('path in ["foo","bar","asd"]', $operator->__toString()); - $this->assertEquals(false, $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)); - } - - public function testOrSomeEq(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', 'foo%'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ] - ); - $this->assertEquals('(path eq "foo" or path like "foo%" or path eq "bar")', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(path in ["foo","bar"] or path like "foo%")', $operator->__toString()); - } - - public function testOrsInside(): void { - $operator = new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_AND, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'text'), - new SearchBinaryOperator( - ISearchBinaryOperator::OPERATOR_OR, - [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'asd'), - ] - ) - ] - ); - $this->assertEquals('(mimetype eq "text" and (path eq "foo" or path eq "bar" or path eq "asd"))', $operator->__toString()); - - $this->optimizer->processOperator($operator); - $this->simplifier->processOperator($operator); - - $this->assertEquals('(mimetype eq "text" and path in ["foo","bar","asd"])', $operator->__toString()); - } -} --- a/tests/lib/Files/Search/QueryOptimizer/PushDownNegationTest.php +++ /dev/null @@ -1,143 +0,0 @@ -optimizer = new PushDownNegation(); - } - - public function testNotOfComparisonIsLeftAlone(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(not path eq "foo")', $operator->__toString()); - } - - public function testNotOfAndBecomesOrOfNots(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('((not path eq "foo") or (not path eq "bar"))', $operator->__toString()); - } - - public function testNotOfOrBecomesAndOfNots(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'bar'), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('((not path eq "foo") and (not path eq "bar"))', $operator->__toString()); - } - - public function testDoubleNegationCancelsOut(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('path eq "foo"', $operator->__toString()); - } - - public function testTripleNegationLeavesASingleNot(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - ]), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(not path eq "foo")', $operator->__toString()); - } - - public function testNegatedNestedGroupIsFullyPushedDown(): void { - // not (a and (b or c)) == (not a) or ((not b) and (not c)) - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'a'), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'b'), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'c'), - ]), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals( - '((not path eq "a") or ((not path eq "b") and (not path eq "c")))', - $operator->__toString(), - ); - } - - public static function emptyGroupProvider(): array { - return [ - 'and' => [ISearchBinaryOperator::OPERATOR_AND], - 'or' => [ISearchBinaryOperator::OPERATOR_OR], - 'not' => [ISearchBinaryOperator::OPERATOR_NOT], - ]; - } - - #[DataProvider('emptyGroupProvider')] - public function testNotOfEmptyGroupIsLeftAlone(string $type): void { - $empty = new SearchBinaryOperator($type, []); - $not = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [$empty]); - $operator = $not; - - $this->assertFalse($this->optimizer->processOperator($operator)); - - $this->assertSame($not, $operator); - $this->assertSame([$empty], $operator->getArguments()); - $this->assertSame([], $empty->getArguments()); - } - - public function testNestedNotOfEmptyGroupIsLeftAlone(): void { - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', 'foo'), - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, []), - ]), - ]); - - $this->optimizer->processOperator($operator); - - $this->assertEquals('(path eq "foo" and (not ()))', $operator->__toString()); - } -} --- a/tests/lib/Files/Search/SearchIntegrationTest.php +++ /dev/null @@ -1,93 +0,0 @@ -user = $this->createMock(IUser::class); - $this->user->method('getUID') - ->willReturn('user'); - $this->storage = new Temporary([]); - $this->cache = $this->storage->getCache(); - $this->storage->getScanner()->scan(''); - $this->mountCache = Server::get(IUserMountCache::class); - $this->mountPoint = '/user/files/search_test/'; - $this->mountCache->addMount($this->user, $this->mountPoint, $this->cache->get(''), 'dummy'); - } - - protected function tearDown(): void { - $this->mountCache->removeMount($this->mountPoint); - - parent::tearDown(); - } - - public function testThousandAndOneFilters(): void { - $id = $this->cache->put('file10', ['size' => 1, 'mtime' => 50, 'mimetype' => 'foo/folder']); - - $comparisons = []; - for ($i = 1; $i <= 1001; $i++) { - $comparisons[] = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', "file$i"); - } - $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $comparisons); - $query = new SearchQuery($operator, 10, 0, []); - - $results = $this->cache->searchQuery($query); - - $this->assertCount(1, $results); - $this->assertEquals($id, $results[0]->getId()); - } - - public static function searchMountNameProvider(): array { - return [ - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search%'), ''], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mount_point_name', 'search_test'), ''], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search_test%'), ''], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%files%'), null], - ]; - } - - #[DataProvider('searchMountNameProvider')] - public function testSearchMountName(ISearchOperator $operator, ?string $resultPath): void { - $query = new SearchQuery($operator, 10, 0, [], $this->user); - - $results = $this->cache->searchQuery($query); - - if (is_null($resultPath)) { - $this->assertCount(0, $results); - } else { - $this->assertCount(1, $results); - $this->assertEquals($this->cache->getId($resultPath), $results[0]->getId()); - } - } -} --- a/tests/lib/Files/SetupManagerTest.php +++ /dev/null @@ -1,672 +0,0 @@ -createMock(IEventLogger::class); - $eventLogger->method('start'); - $eventLogger->method('end'); - - $this->userManager = $this->createMock(IUserManager::class); - $this->cache = $this->createMock(ICache::class); - - $this->userId = 'alice'; - $this->path = "/{$this->userId}/files/folder"; - $this->mountPoint = "{$this->path}/"; - - $this->user = $this->createMock(IUser::class); - $this->user->method('getUID')->willReturn($this->userId); - $this->userManager->method('get') - ->with($this->userId) - ->willReturn($this->user); - - // avoid triggering full setup required check - $this->cache->method('get') - ->with($this->userId) - ->willReturn(true); - - $this->mountProviderCollection = $this->createMock(MountProviderCollection::class); - $this->mountManager = $this->createMock(IMountManager::class); - $eventDispatcher = $this->createMock(IEventDispatcher::class); - $eventDispatcher->method('addListener'); - $this->userMountCache = $this->createMock(IUserMountCache::class); - $lockdownManager = $this->createMock(ILockdownManager::class); - $userSession = $this->createMock(IUserSession::class); - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->expects($this->once()) - ->method('createDistributed') - ->with('setupmanager::') - ->willReturn($this->cache); - $logger = $this->createMock(LoggerInterface::class); - $config = $this->createMock(IConfig::class); - $config->method('getSystemValueBool')->willReturn(false); - $shareDisableChecker = $this->createMock(ShareDisableChecker::class); - $appManager = $this->createMock(IAppManager::class); - $this->fileAccess = $this->createMock(FileAccess::class); - - $lockdownManager->method('canAccessFilesystem')->willReturn(true); - - $this->setupManager = new SetupManager( - $eventLogger, - $this->mountProviderCollection, - $this->mountManager, - $this->userManager, - $eventDispatcher, - $this->userMountCache, - $lockdownManager, - $userSession, - $cacheFactory, - $logger, - $config, - $shareDisableChecker, - $appManager, - $this->fileAccess, - $this->createMock(IAppConfig::class), - $this->createMock(IStorageFactory::class), - ); - } - - public function testTearDown(): void { - $this->setupManager->tearDown(); - } - - /** - * Tests that a path is not set up twice for providers implementing - * IPartialMountProvider in setupForPath. - */ - public function testSetupForPathWithPartialProviderSkipsAlreadySetupPath(): void { - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42); - - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($cachedMount); - $this->userMountCache->expects($this->never())->method('registerMounts'); - $this->userMountCache->expects($this->never())->method('getMountsInPath'); - - $this->fileAccess->expects($this->once()) - ->method('getByFileId') - ->with(42) - ->willReturn($this->createMock(CacheEntry::class)); - - $partialMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsFromProviderByPath') - ->with( - SetupManagerTestPartialMountProvider::class, - $this->path, - false, - $this->callback(function (array $args) use ($cachedMount) { - $this->assertCount(1, $args); - $this->assertInstanceOf(MountProviderArgs::class, $args[0]); - $this->assertSame($cachedMount, $args[0]->mountInfo); - return true; - }) - ) - ->willReturn([$partialMount]); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $this->mountProviderCollection->expects($this->never()) - ->method('getUserMountsForProviderClasses'); - - $invokedCount = $this->exactly(2); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $partialMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, $addMountExpectations)); - - // setup called twice, provider should only be called once - $this->setupManager->setupForPath($this->path, false); - $this->setupManager->setupForPath($this->path, false); - } - - /** - * Tests that providers that are not implementing IPartialMountProvider are - * not set up more than once by setupForPath. - */ - public function testSetupForPathWithNonPartialProviderSkipsAlreadySetupProvider(): void { - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42, - IMountProvider::class); - - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($cachedMount); - - $this->userMountCache->expects($this->once())->method('registerMounts'); - $this->userMountCache->expects($this->never())->method('getMountsInPath'); - - $providerMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsForProviderClasses') - ->with($this->user, [IMountProvider::class]) - ->willReturn([$providerMount]); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $invokedCount = $this->exactly(2); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $providerMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, $addMountExpectations)); - - // setup called twice, provider should only be called once - $this->setupManager->setupForPath($this->path, false); - $this->setupManager->setupForPath($this->path, false); - } - - /** - * Tests that setupForPath does not instantiate already set up providers - * when called for the same path first with $withChildren set to true - * and then set to false. - */ - public function testSetupForPathWithChildrenAndNonPartialProviderSkipsAlreadySetupProvider(): void { - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42, IMountProvider::class); - $additionalCachedMount = $this->getCachedMountInfo($this->mountPoint . 'additional/', 43, SetupManagerTestFullMountProvider::class); - - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($cachedMount); - - $this->userMountCache->expects($this->once())->method('registerMounts'); - $this->userMountCache->expects($this->once())->method('getMountsInPath') - ->willReturn([$additionalCachedMount]); - - $mount = $this->createMock(IMountPoint::class); - $additionalMount = $this->createMock(IMountPoint::class); - - $invokedCount = $this->exactly(2); - $this->mountProviderCollection->expects($invokedCount) - ->method('getUserMountsForProviderClasses') - ->willReturnCallback(function (IUser $userArg, array $providersArg) use ( - $additionalMount, - $mount, - $invokedCount) { - if ($invokedCount->numberOfInvocations() === 1) { - $providers = [IMountProvider::class]; - $returnMounts = [$mount]; - } else { - $providers = [SetupManagerTestFullMountProvider::class]; - $returnMounts = [$additionalMount]; - } - - $this->assertSame($this->user, $userArg); - $this->assertSame($providersArg, $providers); - - return $returnMounts; - }); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $invokedCount = $this->exactly(3); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $mount, - 3 => $additionalMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, $addMountExpectations)); - - // setup called twice, provider should only be called once - $this->setupManager->setupForPath($this->path, true); - $this->setupManager->setupForPath($this->path, false); - } - - /** - * Tests that setupForPath does not set up child mounts again if a parent - * was set up with $withChildren set to true. - */ - public function testSetupForPathWithChildrenAndPartialProviderSkipsIfParentAlreadySetup(): void { - $childPath = "{$this->path}/child"; - $childMountPoint = "{$childPath}/"; - - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42); - $cachedChildMount = $this->getCachedMountInfo($childMountPoint, 43); - - $invokedCount = $this->exactly(3); - $this->userMountCache->expects($invokedCount) - ->method('getMountForPath') - ->willReturnCallback(function (IUser $userArg, string $pathArg) use ( - $cachedChildMount, - $cachedMount, - $childPath, - $invokedCount) { - if ($invokedCount->numberOfInvocations() === 1) { - $expectedPath = $this->path; - $returnMount = $cachedMount; - } else { - $expectedPath = $childPath; - $returnMount = $cachedChildMount; - } - - $this->assertSame($this->user, $userArg); - $this->assertSame($expectedPath, $pathArg); - - return $returnMount; - }); - - $this->userMountCache->expects($this->never())->method('registerMounts'); - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountsInPath') - ->willReturn([$cachedChildMount]); - - $this->fileAccess->expects($this->once()) - ->method('getByFileId') - ->with(42) - ->willReturn($this->createMock(CacheEntry::class)); - - $this->fileAccess->expects($this->once()) - ->method('getByFileIds') - ->with([43]) - ->willReturn([43 => $this->createMock(CacheEntry::class)]); - - $partialMount = $this->createMock(IMountPoint::class); - $partialChildMount = $this->createMock(IMountPoint::class); - - $invokedCount = $this->exactly(2); - $this->mountProviderCollection->expects($invokedCount) - ->method('getUserMountsFromProviderByPath') - ->willReturnCallback(function ( - string $providerClass, - string $pathArg, - bool $forChildren, - array $mountProviderArgs, - ) use ( - $cachedChildMount, - $partialMount, - $partialChildMount, - $cachedMount, - $invokedCount - ) { - $expectedPath = $this->path; - if ($invokedCount->numberOfInvocations() === 1) { - // call for the parent - $expectedCachedMount = $cachedMount; - $mountPoints = [$partialMount]; - $expectedForChildren = false; - } else { - // call for the children - $expectedCachedMount = $cachedChildMount; - $mountPoints = [$partialChildMount]; - $expectedForChildren = true; - } - - $this->assertSame(SetupManagerTestPartialMountProvider::class, $providerClass); - $this->assertSame($expectedPath, $pathArg); - $this->assertSame($expectedForChildren, $forChildren); - $this->assertCount(1, $mountProviderArgs); - $this->assertInstanceOf(MountProviderArgs::class, $mountProviderArgs[0]); - $this->assertSame($expectedCachedMount, $mountProviderArgs[0]->mountInfo); - - return $mountPoints; - }); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $this->mountProviderCollection->expects($this->never()) - ->method('getUserMountsForProviderClasses'); - - $invokedCount = $this->exactly(3); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $partialMount, - 3 => $partialChildMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, $addMountExpectations)); - - // once the setup for a path has been done with children, setup for sub - // paths should not create the same new mounts again - $this->setupManager->setupForPath($this->path, true); - $this->setupManager->setupForPath($childPath, false); - $this->setupManager->setupForPath($childPath, true); - } - - /** - * Tests that when called twice setupForPath does not set up mounts from - * providers implementing IPartialMountProviders or IMountProvider. - */ - public function testSetupForPathHandlesPartialAndFullProvidersWithChildren(): void { - $parentPartialCachedMount = $this->getCachedMountInfo($this->mountPoint, 42); - $childCachedPartialMount = $this->getCachedMountInfo("{$this->mountPoint}partial/", 43); - $childCachedFullMount = $this->getCachedMountInfo("{$this->mountPoint}full/", 44, SetupManagerTestFullMountProvider::class); - - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($parentPartialCachedMount); - $this->userMountCache->expects($this->exactly(2)) - ->method('getMountsInPath') - ->with($this->user, $this->path) - ->willReturn([$childCachedPartialMount, $childCachedFullMount]); - - $homeMount = $this->createMock(IMountPoint::class); - $parentPartialMount = $this->createMock(IMountPoint::class); - $childPartialMount = $this->createMock(IMountPoint::class); - $childFullProviderMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $this->userMountCache->expects($this->once()) - ->method('registerMounts') - ->with( - $this->user, [$childFullProviderMount], - [SetupManagerTestFullMountProvider::class], - ); - - $this->fileAccess->expects($this->once()) - ->method('getByFileId') - ->with(42) - ->willReturn($this->createMock(CacheEntry::class)); - $childMetadata = $this->createMock(CacheEntry::class); - $this->fileAccess->expects($this->once()) - ->method('getByFileIds') - ->with([43]) - ->willReturn([43 => $childMetadata]); - - $invokedCount = $this->exactly(2); - $this->mountProviderCollection->expects($invokedCount) - ->method('getUserMountsFromProviderByPath') - ->willReturnCallback(function (string $providerClass, string $pathArg, bool $forChildren, array $mountProviderArgs) use ( - $childCachedPartialMount, - $childPartialMount, - $parentPartialMount, - $parentPartialCachedMount, - $invokedCount) { - $expectedPath = $this->path; - if ($invokedCount->numberOfInvocations() === 1) { - // call for the parent - $expectedCachedMount = $parentPartialCachedMount; - $mountPoints = [$parentPartialMount]; - $expectedForChildren = false; - } else { - // call for the children - $expectedCachedMount = $childCachedPartialMount; - $mountPoints = [$childPartialMount]; - $expectedForChildren = true; - } - - $this->assertSame(SetupManagerTestPartialMountProvider::class, $providerClass); - $this->assertSame($expectedPath, $pathArg); - $this->assertSame($expectedForChildren, $forChildren); - $this->assertCount(1, $mountProviderArgs); - $this->assertInstanceOf(MountProviderArgs::class, $mountProviderArgs[0]); - $this->assertSame($expectedCachedMount, $mountProviderArgs[0]->mountInfo); - - return $mountPoints; - }); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsForProviderClasses') - ->with($this->user, [SetupManagerTestFullMountProvider::class]) - ->willReturn([$childFullProviderMount]); - - $invokedCount = $this->exactly(4); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $childFullProviderMount, - 3 => $parentPartialMount, - 4 => $childPartialMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, $addMountExpectations)); - - // call twice to test that providers and mounts are only called once - $this->setupManager->setupForPath($this->path, true); - $this->setupManager->setupForPath($this->path, true); - } - - public function testSetupForUserResetsUserPaths(): void { - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42); - - $this->userMountCache->expects($this->once()) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($cachedMount); - $this->userMountCache->expects($this->never()) - ->method('getMountsInPath'); - - $this->fileAccess->expects($this->once()) - ->method('getByFileId') - ->with(42) - ->willReturn($this->createMock(CacheEntry::class)); - - $partialMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsFromProviderByPath') - ->with( - SetupManagerTestPartialMountProvider::class, - $this->path, - false, - $this->callback(function (array $args) use ($cachedMount) { - $this->assertCount(1, $args); - $this->assertInstanceOf(MountProviderArgs::class, - $args[0]); - $this->assertSame($cachedMount, $args[0]->mountInfo); - return true; - }) - ) - ->willReturn([$partialMount]); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - $this->mountProviderCollection->expects($this->never()) - ->method('getUserMountsForProviderClasses'); - - $invokedCount = $this->exactly(2); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $partialMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, - $addMountExpectations)); - - // setting up for $path but then for user should remove the setup path - $this->setupManager->setupForPath($this->path, false); - - // note that only the mount known by SetupManrger is removed not the - // home mount, because MountManager is mocked - $this->mountManager->expects($this->once()) - ->method('removeMount') - ->with($this->mountPoint); - - $this->setupManager->setupForUser($this->user); - } - - /** - * Tests that after a path is setup by a - */ - public function testSetupForProviderResetsUserProviderPaths(): void { - $cachedMount = $this->getCachedMountInfo($this->mountPoint, 42); - - $this->userMountCache->expects($this->once()) - ->method('getMountForPath') - ->with($this->user, $this->path) - ->willReturn($cachedMount); - $this->userMountCache->expects($this->never()) - ->method('getMountsInPath'); - - $this->fileAccess->expects($this->once()) - ->method('getByFileId') - ->with(42) - ->willReturn($this->createMock(CacheEntry::class)); - - $partialMount = $this->createMock(IMountPoint::class); - $partialMount->expects($this->once())->method('getMountProvider') - ->willReturn(SetupManagerTestFullMountProvider::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsFromProviderByPath') - ->with( - SetupManagerTestPartialMountProvider::class, - $this->path, - false, - $this->callback(function (array $args) use ($cachedMount) { - $this->assertCount(1, $args); - $this->assertInstanceOf(MountProviderArgs::class, - $args[0]); - $this->assertSame($cachedMount, $args[0]->mountInfo); - return true; - }) - ) - ->willReturn([$partialMount]); - - $homeMount = $this->createMock(IMountPoint::class); - - $this->mountProviderCollection->expects($this->once()) - ->method('getHomeMountForUser') - ->willReturn($homeMount); - - $invokedCount = $this->exactly(2); - $addMountExpectations = [ - 1 => $homeMount, - 2 => $partialMount, - ]; - $this->mountManager->expects($invokedCount) - ->method('addMount') - ->willReturnCallback($this->getAddMountCheckCallback($invokedCount, - $addMountExpectations)); - $this->mountManager->method('getAll') - ->willReturn([$this->mountPoint => $partialMount]); - - // setting up for $path but then for user should remove the setup path - $this->setupManager->setupForPath($this->path, false); - - // note that only the mount known by SetupManrger is removed not the - // home mount, because MountManager is mocked - $this->mountManager->expects($this->once()) - ->method('removeMount') - ->with($this->mountPoint); - - $this->mountProviderCollection->expects($this->once()) - ->method('getUserMountsForProviderClasses') - ->with($this->user, [SetupManagerTestFullMountProvider::class]); - - $this->setupManager->setupForProvider($this->path, - [SetupManagerTestFullMountProvider::class]); - } - - private function getAddMountCheckCallback(InvokedCount $invokedCount, $expectations): \Closure { - return function (IMountPoint $actualMount) use ($invokedCount, $expectations): void { - $expectedMount = $expectations[$invokedCount->numberOfInvocations()] ?? null; - $this->assertSame($expectedMount, $actualMount); - }; - } - - public function getCachedMountInfo(string $mountPoint, int $rootId, string $providerClass = SetupManagerTestPartialMountProvider::class): ICachedMountInfo&MockObject { - $cachedMount = $this->createMock(ICachedMountInfo::class); - $cachedMount->method('getMountProvider')->willReturn($providerClass); - $cachedMount->method('getMountPoint')->willReturn($mountPoint); - $cachedMount->method('getRootId')->willReturn($rootId); - - return $cachedMount; - } -} - -class SetupManagerTestPartialMountProvider implements IPartialMountProvider { - #[\Override] - public function getMountsForUser(IUser $user, IStorageFactory $loader): array { - return []; - } - - #[\Override] - public function getMountsForPath(string $path, bool $forChildren, array $mountProviderArgs, IStorageFactory $loader): array { - return []; - } -} - -class SetupManagerTestFullMountProvider implements IMountProvider { - #[\Override] - public function getMountsForUser(IUser $user, IStorageFactory $loader): array { - return []; - } -} --- a/tests/lib/Files/SimpleFS/InMemoryFileTest.php +++ /dev/null @@ -1,120 +0,0 @@ -testPdf = new InMemoryFile('test.pdf', $fileContents); - } - - /** - * Asserts that putContent replaces the file contents. - * - * @return void - */ - public function testPutContent(): void { - $this->testPdf->putContent('test'); - self::assertEquals('test', $this->testPdf->getContent()); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testDelete(): void { - $this->testPdf->delete(); - } - - /** - * Asserts that getName returns the name passed on file creation. - * - * @return void - */ - public function testGetName(): void { - self::assertEquals('test.pdf', $this->testPdf->getName()); - } - - /** - * Asserts that the file size is the size of the test file. - * - * @return void - */ - public function testGetSize(): void { - self::assertEquals(7083, $this->testPdf->getSize()); - } - - /** - * Asserts the file contents are the same than the original file contents. - * - * @return void - */ - public function testGetContent(): void { - self::assertEquals( - file_get_contents(__DIR__ . '/../../../data/test.pdf'), - $this->testPdf->getContent() - ); - } - - /** - * Asserts the test file modification time is an integer. - * - * @return void - */ - public function testGetMTime(): void { - self::assertTrue(is_int($this->testPdf->getMTime())); - } - - /** - * Asserts the test file mime type is "application/json". - * - * @return void - */ - public function testGetMimeType(): void { - self::assertEquals('application/pdf', $this->testPdf->getMimeType()); - } - - /** - * Ensure that read() returns a stream with the same contents than the original file. - */ - public function testRead(): void { - self::assertEquals( - file_get_contents(__DIR__ . '/../../../data/test.pdf'), - stream_get_contents($this->testPdf->read()), - ); - } - - /** - * Asserts that write() raises an NotPermittedException. - * - * @return void - */ - public function testWrite(): void { - self::expectException(NotPermittedException::class); - $this->testPdf->write(); - } -} --- a/tests/lib/Files/SimpleFS/SimpleFileTest.php +++ /dev/null @@ -1,127 +0,0 @@ -file = $this->createMock(File::class); - $this->simpleFile = new SimpleFile($this->file); - } - - public function testGetName(): void { - $this->file->expects($this->once()) - ->method('getName') - ->willReturn('myname'); - - $this->assertEquals('myname', $this->simpleFile->getName()); - } - - public function testGetSize(): void { - $this->file->expects($this->once()) - ->method('getSize') - ->willReturn(42); - - $this->assertEquals(42, $this->simpleFile->getSize()); - } - - public function testGetETag(): void { - $this->file->expects($this->once()) - ->method('getETag') - ->willReturn('etag'); - - $this->assertEquals('etag', $this->simpleFile->getETag()); - } - - public function testGetMTime(): void { - $this->file->expects($this->once()) - ->method('getMTime') - ->willReturn(101); - - $this->assertEquals(101, $this->simpleFile->getMTime()); - } - - public function testGetContent(): void { - $this->file->expects($this->once()) - ->method('getContent') - ->willReturn('foo'); - - $this->assertEquals('foo', $this->simpleFile->getContent()); - } - - public function testPutContent(): void { - $this->file->expects($this->once()) - ->method('putContent') - ->with($this->equalTo('bar')); - - $this->simpleFile->putContent('bar'); - } - - public function testDelete(): void { - $this->file->expects($this->once()) - ->method('delete'); - - $this->simpleFile->delete(); - } - - public function testGetMimeType(): void { - $this->file->expects($this->once()) - ->method('getMimeType') - ->willReturn('app/awesome'); - - $this->assertEquals('app/awesome', $this->simpleFile->getMimeType()); - } - - public function testGetContentInvalidAppData(): void { - $this->file->method('getContent') - ->willReturn(false); - $this->file->method('stat')->willReturn(false); - - $parent = $this->createMock(Folder::class); - $parent->method('stat')->willReturn(false); - - $root = $this->createMock(Folder::class); - $root->method('stat')->willReturn([]); - - $this->file->method('getParent')->willReturn($parent); - $parent->method('getParent')->willReturn($root); - - $this->expectException(NotFoundException::class); - - $this->simpleFile->getContent(); - } - - public function testRead(): void { - $this->file->expects($this->once()) - ->method('fopen') - ->with('r'); - - $this->simpleFile->read(); - } - - public function testWrite(): void { - $this->file->expects($this->once()) - ->method('fopen') - ->with('w'); - - $this->simpleFile->write(); - } -} --- a/tests/lib/Files/SimpleFS/SimpleFolderTest.php +++ /dev/null @@ -1,114 +0,0 @@ -storage = new Temporary([]); - $this->createUser('simple', 'simple'); - $this->registerMount('simple', $this->storage, '/simple/files'); - $this->loginAsUser('simple'); - - $this->parentFolder = \OC::$server->getUserFolder('simple'); - - $this->folder = $this->parentFolder->newFolder('test'); - $this->simpleFolder = new SimpleFolder($this->folder); - } - - public function testGetName(): void { - $this->assertEquals('test', $this->simpleFolder->getName()); - } - - public function testDelete(): void { - $this->assertTrue($this->parentFolder->nodeExists('test')); - $this->simpleFolder->delete(); - $this->assertFalse($this->parentFolder->nodeExists('test')); - } - - public function testFileExists(): void { - $this->folder->newFile('exists'); - - $this->assertFalse($this->simpleFolder->fileExists('not-exists')); - $this->assertTrue($this->simpleFolder->fileExists('exists')); - } - - public function testGetFile(): void { - $this->folder->newFile('exists'); - - $result = $this->simpleFolder->getFile('exists'); - $this->assertInstanceOf(ISimpleFile::class, $result); - - $this->expectException(NotFoundException::class); - $this->simpleFolder->getFile('not-exists'); - } - - public function testNewFile(): void { - $result = $this->simpleFolder->newFile('file'); - $this->assertInstanceOf(ISimpleFile::class, $result); - $this->assertFalse($this->folder->nodeExists('file')); - $result->putContent('bar'); - - $this->assertTrue($this->folder->nodeExists('file')); - $this->assertEquals('bar', $result->getContent()); - } - - public function testGetDirectoryListing(): void { - $this->folder->newFile('file1'); - $this->folder->newFile('file2'); - - $result = $this->simpleFolder->getDirectoryListing(); - $this->assertCount(2, $result); - $this->assertInstanceOf(ISimpleFile::class, $result[0]); - $this->assertInstanceOf(ISimpleFile::class, $result[1]); - } - - public function testGetFolder(): void { - $this->folder->newFolder('exists'); - - $result = $this->simpleFolder->getFolder('exists'); - $this->assertInstanceOf(ISimpleFolder::class, $result); - - $this->expectException(NotFoundException::class); - $this->simpleFolder->getFolder('not-exists'); - } - - public function testNewFolder(): void { - $result = $this->simpleFolder->newFolder('folder'); - $this->assertInstanceOf(ISimpleFolder::class, $result); - $result->newFile('file'); - - $this->assertTrue($this->folder->nodeExists('folder')); - } -} --- a/tests/lib/Files/Storage/BearerAuthAwareSabreClientTest.php +++ /dev/null @@ -1,74 +0,0 @@ -getProperty('curlSettings'); - $property->setAccessible(true); - $settings = $property->getValue($client); - return $settings[$key] ?? null; - } - - public function testBearerTokenIsUsedForAuthentication(): void { - $client = new BearerAuthAwareSabreClient([ - 'baseUri' => 'https://example.com/', - // The user name holds the long-lived share secret, never the bearer. - 'userName' => 'refresh-secret', - 'password' => '', - 'authType' => BearerAuthAwareSabreClient::AUTH_BEARER, - 'bearerToken' => 'the-access-jwt', - ]); - - $this->assertSame('the-access-jwt', $this->getCurlSetting($client, CURLOPT_XOAUTH2_BEARER)); - } - - public function testShareSecretIsNotUsedAsBearer(): void { - $client = new BearerAuthAwareSabreClient([ - 'baseUri' => 'https://example.com/', - 'userName' => 'refresh-secret', - 'password' => '', - 'authType' => BearerAuthAwareSabreClient::AUTH_BEARER, - 'bearerToken' => 'the-access-jwt', - ]); - - $this->assertNotSame('refresh-secret', $this->getCurlSetting($client, CURLOPT_XOAUTH2_BEARER)); - } - - public function testNoBearerAppliedWithoutBearerAuthType(): void { - $client = new BearerAuthAwareSabreClient([ - 'baseUri' => 'https://example.com/', - 'userName' => 'user', - 'password' => 'pass', - 'authType' => \Sabre\DAV\Client::AUTH_BASIC, - 'bearerToken' => 'the-access-jwt', - ]); - - $this->assertNull($this->getCurlSetting($client, CURLOPT_XOAUTH2_BEARER)); - } - - public function testNoBearerAppliedWithoutBearerToken(): void { - $client = new BearerAuthAwareSabreClient([ - 'baseUri' => 'https://example.com/', - 'userName' => 'refresh-secret', - 'password' => '', - 'authType' => BearerAuthAwareSabreClient::AUTH_BEARER, - ]); - - $this->assertNull($this->getCurlSetting($client, CURLOPT_XOAUTH2_BEARER)); - } -} --- a/tests/lib/Files/Storage/CommonTest.php +++ /dev/null @@ -1,141 +0,0 @@ -filenameValidator = $this->createMock(IFilenameValidator::class); - $this->overwriteService(IFilenameValidator::class, $this->filenameValidator); - $this->tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->instance = new \OC\Files\Storage\CommonTest(['datadir' => $this->tmpDir]); - } - - #[\Override] - protected function tearDown(): void { - Files::rmdirr($this->tmpDir); - $this->restoreService(IFilenameValidator::class); - parent::tearDown(); - } - - public function testVerifyPath(): void { - $this->filenameValidator - ->expects($this->once()) - ->method('validateFilename') - ->with('invalid:char.txt') - ->willThrowException(new InvalidCharacterInPathException()); - $this->expectException(InvalidPathException::class); - - $this->instance->verifyPath('/', 'invalid:char.txt'); - } - - public function testVerifyPathSucceed(): void { - $this->filenameValidator - ->expects($this->once()) - ->method('validateFilename') - ->with('valid-char.txt'); - - $this->instance->verifyPath('/', 'valid-char.txt'); - } - - public function testMoveFromStorageWrapped(): void { - /** @var \OC\Files\Storage\CommonTest|MockObject $instance */ - $instance = $this->getMockBuilder(\OC\Files\Storage\CommonTest::class) - ->onlyMethods(['copyFromStorage', 'rmdir', 'unlink']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $instance->method('copyFromStorage') - ->willThrowException(new \Exception('copy')); - - $source = new Wrapper([ - 'storage' => $instance, - ]); - - $instance->file_put_contents('foo.txt', 'bar'); - $instance->moveFromStorage($source, 'foo.txt', 'bar.txt'); - $this->assertTrue($instance->file_exists('bar.txt')); - } - - public function testMoveFromStorageJailed(): void { - /** @var \OC\Files\Storage\CommonTest|MockObject $instance */ - $instance = $this->getMockBuilder(\OC\Files\Storage\CommonTest::class) - ->onlyMethods(['copyFromStorage', 'rmdir', 'unlink']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $instance->method('copyFromStorage') - ->willThrowException(new \Exception('copy')); - - $source = new Jail([ - 'storage' => $instance, - 'root' => 'foo' - ]); - $source = new Wrapper([ - 'storage' => $source - ]); - - $instance->mkdir('foo'); - $instance->file_put_contents('foo/foo.txt', 'bar'); - $instance->moveFromStorage($source, 'foo.txt', 'bar.txt'); - $this->assertTrue($instance->file_exists('bar.txt')); - } - - public function testMoveFromStorageNestedJail(): void { - /** @var \OC\Files\Storage\CommonTest|MockObject $instance */ - $instance = $this->getMockBuilder(\OC\Files\Storage\CommonTest::class) - ->onlyMethods(['copyFromStorage', 'rmdir', 'unlink']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $instance->method('copyFromStorage') - ->willThrowException(new \Exception('copy')); - - $source = new Jail([ - 'storage' => $instance, - 'root' => 'foo' - ]); - $source = new Wrapper([ - 'storage' => $source - ]); - $source = new Jail([ - 'storage' => $source, - 'root' => 'bar' - ]); - $source = new Wrapper([ - 'storage' => $source - ]); - - $instance->mkdir('foo'); - $instance->mkdir('foo/bar'); - $instance->file_put_contents('foo/bar/foo.txt', 'bar'); - $instance->moveFromStorage($source, 'foo.txt', 'bar.txt'); - $this->assertTrue($instance->file_exists('bar.txt')); - } -} --- a/tests/lib/Files/Storage/CopyDirectoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ -is_dir($source)) { - return false; - } - return copy($this->getSourcePath($source), $this->getSourcePath($target)); - } -} - -class CopyDirectoryStorage extends StorageNoRecursiveCopy { - use CopyDirectory; -} - -/** - * Class CopyDirectoryTest - * - * - * @package Test\Files\Storage - */ -#[\PHPUnit\Framework\Attributes\Group('DB')] -class CopyDirectoryTest extends Storage { - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->instance = new CopyDirectoryStorage([]); - } -} --- a/tests/lib/Files/Storage/HomeTest.php +++ /dev/null @@ -1,88 +0,0 @@ -home; - } - - #[\Override] - public function getUID(): string { - return $this->uid; - } -} - -/** - * Class Home - * - * - * @package Test\Files\Storage - */ -#[\PHPUnit\Framework\Attributes\Group('DB')] -class HomeTest extends Storage { - /** - * @var string tmpDir - */ - private $tmpDir; - - private $userId; - - /** - * @var User $user - */ - private $user; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->userId = $this->getUniqueID('user_'); - $this->user = new DummyUser($this->userId, $this->tmpDir); - $this->instance = new Home(['user' => $this->user]); - } - - #[\Override] - protected function tearDown(): void { - Files::rmdirr($this->tmpDir); - parent::tearDown(); - } - - /** - * Tests that the home id is in the format home::user1 - */ - public function testId(): void { - $this->assertEquals('home::' . $this->userId, $this->instance->getId()); - } - - /** - * Tests that getCache() returns an instance of HomeCache - */ - public function testGetCacheReturnsHomeCache(): void { - $this->assertInstanceOf('\OC\Files\Cache\HomeCache', $this->instance->getCache()); - } - - public function testGetOwner(): void { - $this->assertEquals($this->userId, $this->instance->getOwner('')); - } -} --- a/tests/lib/Files/Storage/LocalTest.php +++ /dev/null @@ -1,283 +0,0 @@ -tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->instance = new Local(['datadir' => $this->tmpDir]); - } - - #[\Override] - protected function tearDown(): void { - Files::rmdirr($this->tmpDir); - parent::tearDown(); - } - - public function testStableEtag(): void { - $this->instance->file_put_contents('test.txt', 'foobar'); - $etag1 = $this->instance->getETag('test.txt'); - $etag2 = $this->instance->getETag('test.txt'); - $this->assertEquals($etag1, $etag2); - } - - public function testEtagChange(): void { - $this->instance->file_put_contents('test.txt', 'foo'); - $this->instance->touch('test.txt', time() - 2); - $etag1 = $this->instance->getETag('test.txt'); - $this->instance->file_put_contents('test.txt', 'bar'); - $etag2 = $this->instance->getETag('test.txt'); - $this->assertNotEquals($etag1, $etag2); - } - - public function testInvalidArgumentsEmptyArray(): void { - $this->expectException(\InvalidArgumentException::class); - - new Local([]); - } - - public function testInvalidArgumentsNoArray(): void { - $this->expectException(\InvalidArgumentException::class); - - new Local([]); - } - - public function testDisallowSymlinksOutsideDatadir(): void { - $this->expectException(ForbiddenException::class); - - $subDir1 = $this->tmpDir . 'sub1'; - $subDir2 = $this->tmpDir . 'sub2'; - $sym = $this->tmpDir . 'sub1/sym'; - mkdir($subDir1); - mkdir($subDir2); - - symlink($subDir2, $sym); - - $storage = new Local(['datadir' => $subDir1]); - - $storage->file_put_contents('sym/foo', 'bar'); - } - - public function testDisallowSymlinksInsideDatadir(): void { - $subDir1 = $this->tmpDir . 'sub1'; - $subDir2 = $this->tmpDir . 'sub1/sub2'; - $sym = $this->tmpDir . 'sub1/sym'; - mkdir($subDir1); - mkdir($subDir2); - - symlink($subDir2, $sym); - - $storage = new Local(['datadir' => $subDir1]); - - $storage->file_put_contents('sym/foo', 'bar'); - $this->addToAssertionCount(1); - } - - public function testWriteUmaskFilePutContents(): void { - $oldMask = umask(0333); - $this->instance->file_put_contents('test.txt', 'sad'); - umask($oldMask); - $this->assertTrue($this->instance->isUpdatable('test.txt')); - } - - public function testWriteUmaskMkdir(): void { - $oldMask = umask(0333); - $this->instance->mkdir('test.txt'); - umask($oldMask); - $this->assertTrue($this->instance->isUpdatable('test.txt')); - } - - public function testWriteUmaskFopen(): void { - $oldMask = umask(0333); - $handle = $this->instance->fopen('test.txt', 'w'); - fwrite($handle, 'foo'); - fclose($handle); - umask($oldMask); - $this->assertTrue($this->instance->isUpdatable('test.txt')); - } - - public function testWriteUmaskCopy(): void { - $this->instance->file_put_contents('source.txt', 'sad'); - $oldMask = umask(0333); - $this->instance->copy('source.txt', 'test.txt'); - umask($oldMask); - $this->assertTrue($this->instance->isUpdatable('test.txt')); - } - - public function testUnavailableExternal(): void { - $this->expectException(StorageNotAvailableException::class); - $this->instance = new Local(['datadir' => $this->tmpDir . '/unexist', 'isExternal' => true]); - } - - public function testUnavailableNonExternal(): void { - $this->instance = new Local(['datadir' => $this->tmpDir . '/unexist']); - // no exception thrown - $this->assertNotNull($this->instance); - } - - public function testMoveNestedJail(): void { - $this->instance->mkdir('foo'); - $this->instance->mkdir('foo/bar'); - $this->instance->mkdir('target'); - $this->instance->file_put_contents('foo/bar/file.txt', 'foo'); - $jail1 = new Jail([ - 'storage' => $this->instance, - 'root' => 'foo' - ]); - $jail2 = new Jail([ - 'storage' => $jail1, - 'root' => 'bar' - ]); - $jail3 = new Jail([ - 'storage' => $this->instance, - 'root' => 'target' - ]); - $jail3->moveFromStorage($jail2, 'file.txt', 'file.txt'); - $this->assertTrue($this->instance->file_exists('target/file.txt')); - } - - public function testFopenRestoresUmaskWhenTheWritePathThrows(): void { - $storage = new class(['datadir' => $this->tmpDir]) extends Local { - #[\Override] - public function __construct(array $parameters) { - parent::__construct($parameters); - $this->unlinkOnTruncate = true; - } - - #[\Override] - public function unlink(string $path): bool { - throw new \RuntimeException('unlink failed'); - } - }; - - $ambient = umask(0077); - $thrown = false; - try { - $storage->fopen('target.txt', 'w'); - } catch (\RuntimeException) { - $thrown = true; - } - $leaked = umask($ambient); - - $this->assertTrue($thrown, 'the exception has to propagate'); - $this->assertSame(0077, $leaked, 'umask has to be restored when the write path throws'); - } - - public function testFopenRecoveryLeavesEntriesOutsideTheDataDirectoryAlone(): void { - if (!function_exists('exec')) { - $this->markTestSkipped('exec() is required to change the path type out of process'); - } - - $dataDir = rtrim($this->tmpDir, '/'); - $stalePath = $this->tmpDir . 'folder'; - $file = $stalePath . '/a.txt'; - - // opened only to get the "not a directory" entry into the realpath cache - $this->instance->file_put_contents('folder', 'its a file'); - $handle = $this->instance->fopen('folder', 'r'); - $this->assertIsResource($handle); - fclose($handle); - - if ((realpath_cache_get()[$stalePath]['is_dir'] ?? null) !== false) { - $this->markTestSkipped('the realpath cache of this setup does not hold the directory flag'); - } - - realpath(__DIR__); - realpath($dataDir); - $this->assertArrayHasKey(__DIR__, realpath_cache_get()); - $this->assertArrayHasKey($dataDir, realpath_cache_get()); - - exec( - 'rm -f ' . escapeshellarg($stalePath) - . ' && mkdir -p ' . escapeshellarg($stalePath) - . ' && printf abc > ' . escapeshellarg($file), - $output, - $status - ); - $this->assertSame(0, $status, 'failed to replace the file with a directory'); - - $handle = $this->instance->fopen('folder/a.txt', 'r'); - $this->assertIsResource($handle); - fclose($handle); - - $cache = realpath_cache_get(); - $this->assertArrayHasKey(__DIR__, $cache, 'entries outside the data directory have to survive'); - $this->assertArrayHasKey($dataDir, $cache, 'the walk has to stop at the data directory'); - } - - public static function dataStaleRealpathCache(): array { - return [ - // invalidating only the direct parent passes the first and fails the second - 'stale direct parent' => ['staleName' => 'folder', 'filePath' => 'folder/a.txt'], - 'stale grandparent' => ['staleName' => 'nickname', 'filePath' => 'nickname/folder/a.txt'], - ]; - } - - /** - * The type change has to happen out of process: PHP drops its own realpath cache - * entry when it is the one calling unlink() and mkdir(), so doing it here would - * leave nothing stale and the test would pass either way. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataStaleRealpathCache')] - public function testFopenRecoversFromStaleRealpathCache(string $staleName, string $filePath): void { - if (!function_exists('exec')) { - $this->markTestSkipped('exec() is required to change the path type out of process'); - } - - $stalePath = $this->tmpDir . $staleName; - $file = $this->tmpDir . $filePath; - - // opened only to get the "not a directory" entry into the realpath cache - $this->instance->file_put_contents($staleName, 'its a file'); - $handle = $this->instance->fopen($staleName, 'r'); - $this->assertIsResource($handle); - fclose($handle); - - if ((realpath_cache_get()[$stalePath]['is_dir'] ?? null) !== false) { - $this->markTestSkipped('the realpath cache of this setup does not hold the directory flag'); - } - - exec( - 'rm -f ' . escapeshellarg($stalePath) - . ' && mkdir -p ' . escapeshellarg(dirname($file)) - . ' && printf abc > ' . escapeshellarg($file), - $output, - $status - ); - $this->assertSame(0, $status, 'failed to replace the file with a directory'); - - $handle = $this->instance->fopen($filePath, 'r'); - $this->assertIsResource($handle, 'fopen() has to recover from the stale realpath cache entry'); - $this->assertSame('abc', stream_get_contents($handle)); - fclose($handle); - } -} --- a/tests/lib/Files/Storage/Storage.php +++ /dev/null @@ -1,663 +0,0 @@ -waitDelay > 0) { - sleep($this->waitDelay); - } - } - - /** - * the root folder of the storage should always exist, be readable and be recognized as a directory - */ - public function testRoot(): void { - $this->assertTrue($this->instance->file_exists('/'), 'Root folder does not exist'); - $this->assertTrue($this->instance->isReadable('/'), 'Root folder is not readable'); - $this->assertTrue($this->instance->is_dir('/'), 'Root folder is not a directory'); - $this->assertFalse($this->instance->is_file('/'), 'Root folder is a file'); - $this->assertEquals('dir', $this->instance->filetype('/')); - - //without this, any further testing would be useless, not an actual requirement for filestorage though - $this->assertTrue($this->instance->isUpdatable('/'), 'Root folder is not writable'); - } - - /** - * Check that the test() function works - */ - public function testTestFunction(): void { - $this->assertTrue($this->instance->test()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('directoryProvider')] - public function testDirectories($directory): void { - $this->assertFalse($this->instance->file_exists('/' . $directory)); - - $this->assertTrue($this->instance->mkdir('/' . $directory)); - - $this->assertTrue($this->instance->file_exists('/' . $directory)); - $this->assertTrue($this->instance->is_dir('/' . $directory)); - $this->assertFalse($this->instance->is_file('/' . $directory)); - $this->assertEquals('dir', $this->instance->filetype('/' . $directory)); - $this->assertEquals(0, $this->instance->filesize('/' . $directory)); - $this->assertTrue($this->instance->isReadable('/' . $directory)); - $this->assertTrue($this->instance->isUpdatable('/' . $directory)); - - $dh = $this->instance->opendir('/'); - $content = []; - while (($file = readdir($dh)) !== false) { - if ($file !== '.' && $file !== '..') { - $content[] = $file; - } - } - $this->assertEquals([$directory], $content); - - $content = iterator_to_array($this->instance->getDirectoryContent('/')); - - $this->assertCount(1, $content); - $dirEntry = $content[0]; - unset($dirEntry['scan_permissions']); - unset($dirEntry['etag']); - $this->assertLessThanOrEqual(1, abs($dirEntry['mtime'] - $this->instance->filemtime($directory))); - unset($dirEntry['mtime']); - unset($dirEntry['storage_mtime']); - $this->assertEquals([ - 'name' => $directory, - 'mimetype' => $this->instance->getMimeType($directory), - 'size' => -1, - 'permissions' => $this->instance->getPermissions($directory), - ], $dirEntry); - - $this->assertFalse($this->instance->mkdir('/' . $directory)); //can't create existing folders - $this->assertTrue($this->instance->rmdir('/' . $directory)); - - $this->wait(); - $this->assertFalse($this->instance->file_exists('/' . $directory)); - - $this->assertFalse($this->instance->rmdir('/' . $directory)); //can't remove non existing folders - - $dh = $this->instance->opendir('/'); - $content = []; - while (($file = readdir($dh)) !== false) { - if ($file !== '.' && $file !== '..') { - $content[] = $file; - } - } - $this->assertEquals([], $content); - } - - public static function fileNameProvider(): array { - return [ - ['file.txt'], - [' file.txt'], - ['folder .txt'], - ['file with space.txt'], - ['spéciäl fäile'], - ['test single\'quote.txt'], - /*['0'],*/ // disabled until upstream aws-sdk is patched - ['#'], - ['%'], - ['%20'], - ]; - } - - public static function directoryProvider(): array { - return [ - ['folder'], - [' folder'], - ['folder '], - ['folder with space'], - ['spéciäl földer'], - ['test single\'quote'], - /*['0'],*/ // disabled until upstream aws-sdk is patched - ['#'], - ['%'], - ['%20'], - ]; - } - - public static function loremFileProvider(): array { - $root = \OC::$SERVERROOT . '/tests/data/'; - return [ - // small file - [$root . 'lorem.txt'], - // bigger file (> 8 KB which is the standard PHP block size) - [$root . 'lorem-big.txt'] - ]; - } - - /** - * test the various uses of file_get_contents and file_put_contents - */ - #[\PHPUnit\Framework\Attributes\DataProvider('loremFileProvider')] - public function testGetPutContents($sourceFile): void { - $sourceText = file_get_contents($sourceFile); - - //fill a file with string data - $this->instance->file_put_contents('/lorem.txt', $sourceText); - $this->assertFalse($this->instance->is_dir('/lorem.txt')); - $this->assertEquals($sourceText, $this->instance->file_get_contents('/lorem.txt'), 'data returned from file_get_contents is not equal to the source data'); - - //empty the file - $this->instance->file_put_contents('/lorem.txt', ''); - $this->assertEquals('', $this->instance->file_get_contents('/lorem.txt'), 'file not emptied'); - } - - /** - * test various known mimetypes - */ - public function testMimeType(): void { - $this->assertEquals('httpd/unix-directory', $this->instance->getMimeType('/')); - $this->assertEquals(false, $this->instance->getMimeType('/non/existing/file')); - - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile, 'r')); - $this->assertEquals('text/plain', $this->instance->getMimeType('/lorem.txt')); - - $pngFile = \OC::$SERVERROOT . '/tests/data/desktopapp.png'; - $this->instance->file_put_contents('/desktopapp.png', file_get_contents($pngFile, 'r')); - $this->assertEquals('image/png', $this->instance->getMimeType('/desktopapp.png')); - - $svgFile = \OC::$SERVERROOT . '/tests/data/desktopapp.svg'; - $this->instance->file_put_contents('/desktopapp.svg', file_get_contents($svgFile, 'r')); - $this->assertEquals('image/svg+xml', $this->instance->getMimeType('/desktopapp.svg')); - } - - public static function copyAndMoveProvider(): array { - return [ - ['/source.txt', '/target.txt'], - ['/source.txt', '/target with space.txt'], - ['/source with space.txt', '/target.txt'], - ['/source with space.txt', '/target with space.txt'], - ['/source.txt', '/tärgét.txt'], - ['/sòurcē.txt', '/target.txt'], - ['/sòurcē.txt', '/tärgét.txt'], - ['/single \' quote.txt', '/tar\'get.txt'], - ]; - } - - public function initSourceAndTarget($source, $target = null) { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->instance->file_put_contents($source, file_get_contents($textFile)); - if ($target) { - $testContents = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $this->instance->file_put_contents($target, $testContents); - } - } - - public function assertSameAsLorem($file) { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->assertEquals( - file_get_contents($textFile), - $this->instance->file_get_contents($file), - 'Expected ' . $file . ' to be a copy of ' . $textFile - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testCopy($source, $target): void { - $this->initSourceAndTarget($source); - - $this->instance->copy($source, $target); - - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertSameAsLorem($target); - $this->assertTrue($this->instance->file_exists($source), $source . ' was deleted'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testMove($source, $target): void { - $this->initSourceAndTarget($source); - - $this->instance->rename($source, $target); - - $this->wait(); - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertFalse($this->instance->file_exists($source), $source . ' still exists'); - $this->assertSameAsLorem($target); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testCopyOverwrite($source, $target): void { - $this->initSourceAndTarget($source, $target); - - $this->instance->copy($source, $target); - - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertTrue($this->instance->file_exists($source), $source . ' was deleted'); - $this->assertSameAsLorem($target); - $this->assertSameAsLorem($source); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testMoveOverwrite($source, $target): void { - $this->initSourceAndTarget($source, $target); - - $this->instance->rename($source, $target); - - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertFalse($this->instance->file_exists($source), $source . ' still exists'); - $this->assertSameAsLorem($target); - } - - public function testLocal(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); - $localFile = $this->instance->getLocalFile('/lorem.txt'); - $this->assertTrue(file_exists($localFile)); - $this->assertEquals(file_get_contents($textFile), file_get_contents($localFile)); - - $this->instance->mkdir('/folder'); - $this->instance->file_put_contents('/folder/lorem.txt', file_get_contents($textFile)); - $this->instance->file_put_contents('/folder/bar.txt', 'asd'); - $this->instance->mkdir('/folder/recursive'); - $this->instance->file_put_contents('/folder/recursive/file.txt', 'foo'); - - // test below require to use instance->getLocalFile because the physical storage might be different - $localFile = $this->instance->getLocalFile('/folder/lorem.txt'); - $this->assertTrue(file_exists($localFile)); - $this->assertEquals(file_get_contents($localFile), file_get_contents($textFile)); - - $localFile = $this->instance->getLocalFile('/folder/bar.txt'); - $this->assertTrue(file_exists($localFile)); - $this->assertEquals(file_get_contents($localFile), 'asd'); - - $localFile = $this->instance->getLocalFile('/folder/recursive/file.txt'); - $this->assertTrue(file_exists($localFile)); - $this->assertEquals(file_get_contents($localFile), 'foo'); - } - - public function testStat(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $ctimeStart = time(); - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); - $this->assertTrue($this->instance->isReadable('/lorem.txt')); - $ctimeEnd = time(); - $mTime = $this->instance->filemtime('/lorem.txt'); - $this->assertTrue($this->instance->hasUpdated('/lorem.txt', $ctimeStart - 5)); - $this->assertTrue($this->instance->hasUpdated('/', $ctimeStart - 5)); - - // check that ($ctimeStart - 5) <= $mTime <= ($ctimeEnd + 1) - $this->assertGreaterThanOrEqual(($ctimeStart - 5), $mTime); - $this->assertLessThanOrEqual(($ctimeEnd + 1), $mTime); - $this->assertEquals(filesize($textFile), $this->instance->filesize('/lorem.txt')); - - $stat = $this->instance->stat('/lorem.txt'); - //only size and mtime are required in the result - $this->assertEquals($stat['size'], $this->instance->filesize('/lorem.txt')); - $this->assertEquals($stat['mtime'], $mTime); - - if ($this->instance->touch('/lorem.txt', 100) !== false) { - $mTime = $this->instance->filemtime('/lorem.txt'); - $this->assertEquals($mTime, 100); - } - - $mtimeStart = time(); - - $this->instance->unlink('/lorem.txt'); - $this->assertTrue($this->instance->hasUpdated('/', $mtimeStart - 5)); - - $this->assertFalse($this->instance->filesize('/non-existing-file.txt')); - } - - /** - * Test whether checkUpdate properly returns false when there was - * no change. - */ - public function testCheckUpdate(): void { - if ($this->instance instanceof Wrapper) { - $this->markTestSkipped('Cannot test update check on wrappers'); - } - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $watcher = $this->instance->getWatcher(); - $watcher->setPolicy(Watcher::CHECK_ALWAYS); - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); - $this->assertTrue($watcher->checkUpdate('/lorem.txt'), 'Update detected'); - $this->assertFalse($watcher->checkUpdate('/lorem.txt'), 'No update'); - } - - public function testUnlink(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile)); - - $this->assertTrue($this->instance->file_exists('/lorem.txt')); - - $this->assertTrue($this->instance->unlink('/lorem.txt')); - $this->wait(); - - $this->assertFalse($this->instance->file_exists('/lorem.txt')); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('fileNameProvider')] - public function testFOpen($fileName): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - - $fh = @$this->instance->fopen($fileName, 'r'); - if ($fh) { - fclose($fh); - } - $this->assertFalse($fh); - $this->assertFalse($this->instance->file_exists($fileName)); - - $fh = $this->instance->fopen($fileName, 'w'); - fwrite($fh, file_get_contents($textFile)); - fclose($fh); - $this->assertTrue($this->instance->file_exists($fileName)); - - $fh = $this->instance->fopen($fileName, 'r'); - $this->assertTrue(is_resource($fh)); - $content = stream_get_contents($fh); - $this->assertEquals(file_get_contents($textFile), $content); - } - - public function testTouchCreateFile(): void { - $this->assertFalse($this->instance->file_exists('touch')); - // returns true on success - $this->assertTrue($this->instance->touch('touch')); - $this->assertTrue($this->instance->file_exists('touch')); - } - - public function testRecursiveRmdir(): void { - $this->instance->mkdir('folder'); - $this->instance->mkdir('folder/bar'); - $this->wait(); - $this->instance->file_put_contents('folder/asd.txt', 'foobar'); - $this->instance->file_put_contents('folder/bar/foo.txt', 'asd'); - $this->assertTrue($this->instance->rmdir('folder')); - $this->wait(); - $this->assertFalse($this->instance->file_exists('folder/asd.txt')); - $this->assertFalse($this->instance->file_exists('folder/bar/foo.txt')); - $this->assertFalse($this->instance->file_exists('folder/bar')); - $this->assertFalse($this->instance->file_exists('folder')); - } - - public function testRmdirEmptyFolder(): void { - $this->assertTrue($this->instance->mkdir('empty')); - $this->wait(); - $this->assertTrue($this->instance->rmdir('empty')); - $this->assertFalse($this->instance->file_exists('empty')); - } - - public function testRecursiveUnlink(): void { - $this->instance->mkdir('folder'); - $this->instance->mkdir('folder/bar'); - $this->instance->file_put_contents('folder/asd.txt', 'foobar'); - $this->instance->file_put_contents('folder/bar/foo.txt', 'asd'); - $this->assertTrue($this->instance->unlink('folder')); - $this->wait(); - $this->assertFalse($this->instance->file_exists('folder/asd.txt')); - $this->assertFalse($this->instance->file_exists('folder/bar/foo.txt')); - $this->assertFalse($this->instance->file_exists('folder/bar')); - $this->assertFalse($this->instance->file_exists('folder')); - } - - public static function hashProvider(): array { - return [ - ['Foobar', 'md5'], - ['Foobar', 'sha1'], - ['Foobar', 'sha256'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('hashProvider')] - public function testHash($data, $type): void { - $this->instance->file_put_contents('hash.txt', $data); - $this->assertEquals(hash($type, $data), $this->instance->hash($type, 'hash.txt')); - $this->assertEquals(hash($type, $data, true), $this->instance->hash($type, 'hash.txt', true)); - } - - public function testHashInFileName(): void { - $this->instance->file_put_contents('#test.txt', 'data'); - $this->assertEquals('data', $this->instance->file_get_contents('#test.txt')); - - $this->instance->mkdir('#foo'); - $this->instance->file_put_contents('#foo/test.txt', 'data'); - $this->assertEquals('data', $this->instance->file_get_contents('#foo/test.txt')); - - $dh = $this->instance->opendir('#foo'); - $content = []; - while ($file = readdir($dh)) { - if ($file !== '.' && $file !== '..') { - $content[] = $file; - } - } - - $this->assertEquals(['test.txt'], $content); - } - - public function testCopyOverWriteFile(): void { - $this->instance->file_put_contents('target.txt', 'foo'); - $this->instance->file_put_contents('source.txt', 'bar'); - $this->instance->copy('source.txt', 'target.txt'); - $this->assertEquals('bar', $this->instance->file_get_contents('target.txt')); - } - - public function testRenameOverWriteFile(): void { - $this->instance->file_put_contents('target.txt', 'foo'); - $this->instance->file_put_contents('source.txt', 'bar'); - $this->instance->rename('source.txt', 'target.txt'); - $this->assertEquals('bar', $this->instance->file_get_contents('target.txt')); - $this->assertFalse($this->instance->file_exists('source.txt')); - } - - public function testRenameDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - $this->instance->file_put_contents('source/test2.txt', 'qwerty'); - $this->instance->mkdir('source/subfolder'); - $this->instance->file_put_contents('source/subfolder/test.txt', 'bar'); - $this->instance->rename('source', 'target'); - - $this->assertFalse($this->instance->file_exists('source')); - $this->assertFalse($this->instance->file_exists('source/test1.txt')); - $this->assertFalse($this->instance->file_exists('source/test2.txt')); - $this->assertFalse($this->instance->file_exists('source/subfolder')); - $this->assertFalse($this->instance->file_exists('source/subfolder/test.txt')); - - $this->assertTrue($this->instance->file_exists('target')); - $this->assertTrue($this->instance->file_exists('target/test1.txt')); - $this->assertTrue($this->instance->file_exists('target/test2.txt')); - $this->assertTrue($this->instance->file_exists('target/subfolder')); - $this->assertTrue($this->instance->file_exists('target/subfolder/test.txt')); - - $contents = iterator_to_array($this->instance->getDirectoryContent('')); - $this->assertCount(1, $contents); - - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - $this->assertEquals('qwerty', $this->instance->file_get_contents('target/test2.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents('target/subfolder/test.txt')); - } - - public function testRenameOverWriteDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - - $this->instance->mkdir('target'); - $this->instance->file_put_contents('target/test1.txt', 'bar'); - $this->instance->file_put_contents('target/test2.txt', 'bar'); - - $this->assertTrue($this->instance->rename('source', 'target'), 'rename must return true on success'); - - $this->assertFalse($this->instance->file_exists('source'), 'source has not been removed'); - $this->assertFalse($this->instance->file_exists('source/test1.txt'), 'source/test1.txt has not been removed'); - $this->assertFalse($this->instance->file_exists('target/test2.txt'), 'target/test2.txt has not been removed'); - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'), 'target/test1.txt has not been overwritten'); - } - - public function testRenameOverWriteDirectoryOverFile(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - - $this->instance->file_put_contents('target', 'bar'); - - $this->assertTrue($this->instance->rename('source', 'target'), 'rename must return true on success'); - - $this->assertFalse($this->instance->file_exists('source')); - $this->assertFalse($this->instance->file_exists('source/test1.txt')); - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - } - - public function testCopyDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - $this->instance->file_put_contents('source/test2.txt', 'qwerty'); - $this->instance->mkdir('source/subfolder'); - $this->instance->file_put_contents('source/subfolder/test.txt', 'bar'); - $this->instance->copy('source', 'target'); - - $this->assertTrue($this->instance->file_exists('source')); - $this->assertTrue($this->instance->file_exists('source/test1.txt')); - $this->assertTrue($this->instance->file_exists('source/test2.txt')); - $this->assertTrue($this->instance->file_exists('source/subfolder')); - $this->assertTrue($this->instance->file_exists('source/subfolder/test.txt')); - - $this->assertTrue($this->instance->file_exists('target')); - $this->assertTrue($this->instance->file_exists('target/test1.txt')); - $this->assertTrue($this->instance->file_exists('target/test2.txt')); - $this->assertTrue($this->instance->file_exists('target/subfolder')); - $this->assertTrue($this->instance->file_exists('target/subfolder/test.txt')); - - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - $this->assertEquals('qwerty', $this->instance->file_get_contents('target/test2.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents('target/subfolder/test.txt')); - } - - public function testCopyOverWriteDirectory(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - - $this->instance->mkdir('target'); - $this->instance->file_put_contents('target/test1.txt', 'bar'); - $this->instance->file_put_contents('target/test2.txt', 'bar'); - - $this->instance->copy('source', 'target'); - - $this->assertFalse($this->instance->file_exists('target/test2.txt'), 'File target/test2.txt should no longer exist, but does'); - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - } - - public function testCopyOverWriteDirectoryOverFile(): void { - $this->instance->mkdir('source'); - $this->instance->file_put_contents('source/test1.txt', 'foo'); - - $this->instance->file_put_contents('target', 'bar'); - - $this->instance->copy('source', 'target'); - - $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt')); - } - - public function testInstanceOfStorage(): void { - $this->assertTrue($this->instance->instanceOfStorage(IStorage::class)); - $this->assertTrue($this->instance->instanceOfStorage(get_class($this->instance))); - $this->assertFalse($this->instance->instanceOfStorage('\OC')); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('copyAndMoveProvider')] - public function testCopyFromSameStorage($source, $target): void { - $this->initSourceAndTarget($source); - - $this->instance->copyFromStorage($this->instance, $source, $target); - - $this->assertTrue($this->instance->file_exists($target), $target . ' was not created'); - $this->assertSameAsLorem($target); - $this->assertTrue($this->instance->file_exists($source), $source . ' was deleted'); - } - - public function testIsCreatable(): void { - $this->instance->mkdir('source'); - $this->assertTrue($this->instance->isCreatable('source')); - } - - public function testIsReadable(): void { - $this->instance->mkdir('source'); - $this->assertTrue($this->instance->isReadable('source')); - } - - public function testIsUpdatable(): void { - $this->instance->mkdir('source'); - $this->assertTrue($this->instance->isUpdatable('source')); - } - - public function testIsDeletable(): void { - $this->instance->mkdir('source'); - $this->assertTrue($this->instance->isDeletable('source')); - } - - public function testIsShareable(): void { - $this->instance->mkdir('source'); - $this->assertTrue($this->instance->isSharable('source')); - } - - public function testStatAfterWrite(): void { - $this->instance->file_put_contents('foo.txt', 'bar'); - $stat = $this->instance->stat('foo.txt'); - $this->assertEquals(3, $stat['size']); - - $fh = $this->instance->fopen('foo.txt', 'w'); - fwrite($fh, 'qwerty'); - fclose($fh); - - $stat = $this->instance->stat('foo.txt'); - $this->assertEquals(6, $stat['size']); - } - - public function testPartFile(): void { - $this->instance->file_put_contents('bar.txt.part', 'bar'); - $this->instance->rename('bar.txt.part', 'bar.txt'); - $this->assertEquals('bar', $this->instance->file_get_contents('bar.txt')); - } - - public function testWriteStream(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - - if (!$this->instance->instanceOfStorage(IWriteStreamStorage::class)) { - $this->markTestSkipped('Not a WriteSteamStorage'); - } - /** @var IWriteStreamStorage $storage */ - $storage = $this->instance; - - $source = fopen($textFile, 'r'); - - $storage->writeStream('test.txt', $source); - $this->assertTrue($storage->file_exists('test.txt')); - $this->assertStringEqualsFile($textFile, $storage->file_get_contents('test.txt')); - $this->assertEquals('resource (closed)', gettype($source)); - } - - public function testFseekSize(): void { - $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; - $this->instance->file_put_contents('bar.txt', file_get_contents($textFile)); - - $size = $this->instance->filesize('bar.txt'); - $this->assertEquals(filesize($textFile), $size); - $fh = $this->instance->fopen('bar.txt', 'r'); - - fseek($fh, 0, SEEK_END); - $pos = ftell($fh); - - $this->assertEquals($size, $pos); - } -} --- a/tests/lib/Files/Storage/StorageFactoryTest.php +++ /dev/null @@ -1,73 +0,0 @@ -data = $parameters['data']; - } - } -} - -class StorageFactoryTest extends TestCase { - public function testSimpleWrapper(): void { - $instance = new StorageFactory(); - $mount = new MountPoint('\OC\Files\Storage\Temporary', '/foo', [[]], $instance); - $instance->addStorageWrapper('dummy', function ($mountPoint, IStorage $storage, IMountPoint $mount) { - $this->assertInstanceOf('\OC\Files\Storage\Temporary', $storage); - $this->assertEquals('/foo/', $mount->getMountPoint()); - $this->assertEquals('/foo/', $mountPoint); - return new DummyWrapper(['storage' => $storage]); - }); - $wrapped = $mount->getStorage(); - $this->assertInstanceOf('\Test\Files\Storage\DummyWrapper', $wrapped); - } - - public function testRemoveWrapper(): void { - $instance = new StorageFactory(); - $mount = new MountPoint('\OC\Files\Storage\Temporary', '/foo', [[]], $instance); - $instance->addStorageWrapper('dummy', function ($mountPoint, IStorage $storage) { - return new DummyWrapper(['storage' => $storage]); - }); - $instance->removeStorageWrapper('dummy'); - $wrapped = $mount->getStorage(); - $this->assertInstanceOf('\OC\Files\Storage\Temporary', $wrapped); - } - - public function testWrapperPriority(): void { - $instance = new StorageFactory(); - $mount = new MountPoint('\OC\Files\Storage\Temporary', '/foo', [[]], $instance); - $instance->addStorageWrapper('dummy1', function ($mountPoint, IStorage $storage) { - return new DummyWrapper(['storage' => $storage, 'data' => 1]); - }, 1); - $instance->addStorageWrapper('dummy2', function ($mountPoint, IStorage $storage) { - return new DummyWrapper(['storage' => $storage, 'data' => 100]); - }, 100); - $instance->addStorageWrapper('dummy3', function ($mountPoint, IStorage $storage) { - return new DummyWrapper(['storage' => $storage, 'data' => 50]); - }, 50); - /** @var \Test\Files\Storage\DummyWrapper $wrapped */ - $wrapped = $mount->getStorage(); - $this->assertInstanceOf('\Test\Files\Storage\DummyWrapper', $wrapped); - $this->assertEquals(1, $wrapped->data);// lowest priority is applied last, called first - $this->assertEquals(50, $wrapped->getWrapperStorage()->data); - $this->assertEquals(100, $wrapped->getWrapperStorage()->getWrapperStorage()->data); - } -} --- a/tests/lib/Files/Storage/StoragesTestCase.php +++ /dev/null @@ -1,129 +0,0 @@ -storage1) && is_null($this->storage2)) { - return; - } - $this->storage1->getCache()->clear(); - $this->storage2->getCache()->clear(); - - parent::tearDown(); - } - - public function testMoveFileFromStorage() { - $source = 'source.txt'; - $target = 'target.txt'; - $this->storage2->file_put_contents($source, 'foo'); - - $this->storage1->moveFromStorage($this->storage2, $source, $target); - - $this->assertTrue($this->storage1->file_exists($target), $target . ' was not created'); - $this->assertFalse($this->storage2->file_exists($source), $source . ' still exists'); - $this->assertEquals('foo', $this->storage1->file_get_contents($target)); - } - - public function testMoveFileFromStorageWithExistingTarget() { - $source = 'source.txt'; - $target = 'target.txt'; - $this->storage1->file_put_contents($target, 'bar'); - $this->storage2->file_put_contents($source, 'foo'); - - $targetURN = $this->storage1->getURN($this->storage1->getCache()->get($target)->getID()); - $sourceURN = $this->storage2->getURN($this->storage2->getCache()->get($source)->getID()); - - $this->storage1->moveFromStorage($this->storage2, $source, $target); - - $this->assertTrue($this->storage1->file_exists($target), $target . ' was not created in DB'); - $this->assertFalse($this->storage2->file_exists($source), $source . ' still exists in DB'); - $this->assertTrue($this->storage1->getObjectStore()->objectExists($sourceURN), $sourceURN . ' was not created in bucket'); - $this->assertFalse($this->storage1->getObjectStore()->objectExists($targetURN), $targetURN . ' still exists in bucket'); - $this->assertEquals('foo', $this->storage1->file_get_contents($target)); - } - - public function testMoveDirectoryFromStorage() { - $this->storage2->mkdir('source'); - $this->storage2->file_put_contents('source/test1.txt', 'foo'); - $this->storage2->file_put_contents('source/test2.txt', 'qwerty'); - $this->storage2->mkdir('source/subfolder'); - $this->storage2->file_put_contents('source/subfolder/test.txt', 'bar'); - - $this->storage1->moveFromStorage($this->storage2, 'source', 'target'); - - $this->assertTrue($this->storage1->file_exists('target')); - $this->assertTrue($this->storage1->file_exists('target/test1.txt')); - $this->assertTrue($this->storage1->file_exists('target/test2.txt')); - $this->assertTrue($this->storage1->file_exists('target/subfolder')); - $this->assertTrue($this->storage1->file_exists('target/subfolder/test.txt')); - - $this->assertFalse($this->storage2->file_exists('source')); - $this->assertFalse($this->storage2->file_exists('source/test1.txt')); - $this->assertFalse($this->storage2->file_exists('source/test2.txt')); - $this->assertFalse($this->storage2->file_exists('source/subfolder')); - $this->assertFalse($this->storage2->file_exists('source/subfolder/test.txt')); - - $this->assertEquals('foo', $this->storage1->file_get_contents('target/test1.txt')); - $this->assertEquals('qwerty', $this->storage1->file_get_contents('target/test2.txt')); - $this->assertEquals('bar', $this->storage1->file_get_contents('target/subfolder/test.txt')); - } - - public function testCopyFileFromStorage() { - $source = 'source.txt'; - $target = 'target.txt'; - $this->storage2->file_put_contents($source, 'foo'); - - $this->storage1->copyFromStorage($this->storage2, $source, $target); - - $this->assertTrue($this->storage1->file_exists($target), $target . ' was not created'); - $this->assertTrue($this->storage2->file_exists($source), $source . ' was deleted'); - $this->assertEquals('foo', $this->storage1->file_get_contents($target)); - } - - public function testCopyDirectoryFromStorage() { - $this->storage2->mkdir('source'); - $this->storage2->file_put_contents('source/test1.txt', 'foo'); - $this->storage2->file_put_contents('source/test2.txt', 'qwerty'); - $this->storage2->mkdir('source/subfolder'); - $this->storage2->file_put_contents('source/subfolder/test.txt', 'bar'); - - $this->storage1->copyFromStorage($this->storage2, 'source', 'target'); - - $this->assertTrue($this->storage1->file_exists('target')); - $this->assertTrue($this->storage1->file_exists('target/test1.txt')); - $this->assertTrue($this->storage1->file_exists('target/test2.txt')); - $this->assertTrue($this->storage1->file_exists('target/subfolder')); - $this->assertTrue($this->storage1->file_exists('target/subfolder/test.txt')); - - $this->assertTrue($this->storage2->file_exists('source')); - $this->assertTrue($this->storage2->file_exists('source/test1.txt')); - $this->assertTrue($this->storage2->file_exists('source/test2.txt')); - $this->assertTrue($this->storage2->file_exists('source/subfolder')); - $this->assertTrue($this->storage2->file_exists('source/subfolder/test.txt')); - - $this->assertEquals('foo', $this->storage1->file_get_contents('target/test1.txt')); - $this->assertEquals('qwerty', $this->storage1->file_get_contents('target/test2.txt')); - $this->assertEquals('bar', $this->storage1->file_get_contents('target/subfolder/test.txt')); - } -} --- a/tests/lib/Files/Storage/Wrapper/AvailabilityTest.php +++ /dev/null @@ -1,185 +0,0 @@ -storageCache = $this->createMock(StorageCache::class); - - $this->storage = $this->createMock(Temporary::class); - $this->storage->expects($this->any()) - ->method('getStorageCache') - ->willReturn($this->storageCache); - - $this->wrapper = new Availability(['storage' => $this->storage]); - } - - /** - * Storage is available - */ - public function testAvailable(): void { - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => true, 'last_checked' => 0]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage->expects($this->once()) - ->method('mkdir'); - - $this->wrapper->mkdir('foobar'); - } - - /** - * Storage marked unavailable, TTL not expired - * - */ - public function testUnavailable(): void { - $this->expectException(StorageNotAvailableException::class); - - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => false, 'last_checked' => time()]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage->expects($this->never()) - ->method('mkdir'); - - $this->wrapper->mkdir('foobar'); - } - - /** - * Storage marked unavailable, TTL expired - */ - public function testUnavailableRecheck(): void { - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => false, 'last_checked' => 0]); - $this->storage->expects($this->once()) - ->method('test') - ->willReturn(true); - $calls = [ - false, // prevents concurrent rechecks - true, // sets correct availability - ]; - $this->storage->expects($this->exactly(2)) - ->method('setAvailability') - ->willReturnCallback(function ($value) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $value); - }); - $this->storage->expects($this->once()) - ->method('mkdir'); - - $this->wrapper->mkdir('foobar'); - } - - /** - * Storage marked available, but throws StorageNotAvailableException - * - */ - public function testAvailableThrowStorageNotAvailable(): void { - $this->expectException(StorageNotAvailableException::class); - - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => true, 'last_checked' => 0]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage->expects($this->once()) - ->method('mkdir') - ->willThrowException(new StorageNotAvailableException()); - $this->storageCache->expects($this->once()) - ->method('setAvailability') - ->with($this->equalTo(false)); - - $this->wrapper->mkdir('foobar'); - } - - /** - * Storage available, but call fails - * Method failure does not indicate storage unavailability - */ - public function testAvailableFailure(): void { - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => true, 'last_checked' => 0]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage->expects($this->once()) - ->method('mkdir') - ->willReturn(false); - $this->storage->expects($this->never()) - ->method('setAvailability'); - - $this->wrapper->mkdir('foobar'); - } - - /** - * Storage available, but throws exception - * Standard exception does not indicate storage unavailability - * - */ - public function testAvailableThrow(): void { - $this->expectException(\Exception::class); - - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => true, 'last_checked' => 0]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage->expects($this->once()) - ->method('mkdir') - ->willThrowException(new \Exception()); - $this->storage->expects($this->never()) - ->method('setAvailability'); - - $this->wrapper->mkdir('foobar'); - } - - public function testUnavailableMultiple(): void { - $this->storage->expects($this->once()) - ->method('getAvailability') - ->willReturn(['available' => true, 'last_checked' => 0]); - $this->storage->expects($this->never()) - ->method('test'); - $this->storage - ->expects($this->once()) // load-bearing `once` - ->method('mkdir') - ->willThrowException(new StorageNotAvailableException()); - - try { - $this->wrapper->mkdir('foobar'); - $this->fail(); - } catch (StorageNotAvailableException) { - } - - $this->storage->expects($this->never())->method('file_exists'); - - try { - $this->wrapper->mkdir('foobar'); - $this->fail(); - } catch (StorageNotAvailableException) { - } - } -} --- a/tests/lib/Files/Storage/Wrapper/EncodingTest.php +++ /dev/null @@ -1,244 +0,0 @@ -sourceStorage = new Temporary([]); - $this->instance = new Encoding([ - 'storage' => $this->sourceStorage - ]); - } - - #[\Override] - protected function tearDown(): void { - $this->sourceStorage->cleanUp(); - parent::tearDown(); - } - - #[\Override] - public static function directoryProvider(): array { - $a = parent::directoryProvider(); - $a[] = [self::NFC_NAME]; - return $a; - } - - #[\Override] - public static function fileNameProvider(): array { - $a = parent::fileNameProvider(); - $a[] = [self::NFD_NAME . '.txt']; - return $a; - } - - #[\Override] - public static function copyAndMoveProvider(): array { - $a = parent::copyAndMoveProvider(); - $a[] = [self::NFD_NAME . '.txt', self::NFC_NAME . '-renamed.txt']; - return $a; - } - - public static function accessNameProvider(): array { - return [ - [self::NFD_NAME], - [self::NFC_NAME], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] - public function testFputEncoding($accessName): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); - $this->assertEquals('bar', $this->instance->file_get_contents($accessName)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] - public function testFopenReadEncoding($accessName): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); - $fh = $this->instance->fopen($accessName, 'r'); - $data = fgets($fh); - fclose($fh); - $this->assertEquals('bar', $data); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] - public function testFopenOverwriteEncoding($accessName): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); - $fh = $this->instance->fopen($accessName, 'w'); - $data = fputs($fh, 'test'); - fclose($fh); - $data = $this->sourceStorage->file_get_contents(self::NFD_NAME); - $this->assertEquals('test', $data); - $this->assertFalse($this->sourceStorage->file_exists(self::NFC_NAME)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] - public function testFileExistsEncoding($accessName): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); - $this->assertTrue($this->instance->file_exists($accessName)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] - public function testUnlinkEncoding($accessName): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); - $this->assertTrue($this->instance->unlink($accessName)); - $this->assertFalse($this->sourceStorage->file_exists(self::NFC_NAME)); - $this->assertFalse($this->sourceStorage->file_exists(self::NFD_NAME)); - } - - public function testNfcHigherPriority(): void { - $this->sourceStorage->file_put_contents(self::NFC_NAME, 'nfc'); - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'nfd'); - $this->assertEquals('nfc', $this->instance->file_get_contents(self::NFC_NAME)); - } - - public static function encodedDirectoriesProvider(): array { - return [ - [self::NFD_NAME, self::NFC_NAME], - [self::NFD_NAME . '/' . self::NFD_NAME, self::NFC_NAME . '/' . self::NFC_NAME], - [self::NFD_NAME . '/' . self::NFC_NAME . '/' . self::NFD_NAME, self::NFC_NAME . '/' . self::NFC_NAME . '/' . self::NFC_NAME], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('encodedDirectoriesProvider')] - public function testOperationInsideDirectory($sourceDir, $accessDir): void { - $this->sourceStorage->mkdir($sourceDir); - $this->instance->file_put_contents($accessDir . '/test.txt', 'bar'); - $this->assertTrue($this->instance->file_exists($accessDir . '/test.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents($accessDir . '/test.txt')); - - $this->sourceStorage->file_put_contents($sourceDir . '/' . self::NFD_NAME, 'foo'); - $this->assertTrue($this->instance->file_exists($accessDir . '/' . self::NFC_NAME)); - $this->assertEquals('foo', $this->instance->file_get_contents($accessDir . '/' . self::NFC_NAME)); - - // try again to make it use cached path - $this->assertEquals('bar', $this->instance->file_get_contents($accessDir . '/test.txt')); - $this->assertTrue($this->instance->file_exists($accessDir . '/test.txt')); - $this->assertEquals('foo', $this->instance->file_get_contents($accessDir . '/' . self::NFC_NAME)); - $this->assertTrue($this->instance->file_exists($accessDir . '/' . self::NFC_NAME)); - } - - public function testCacheExtraSlash(): void { - $this->sourceStorage->file_put_contents(self::NFD_NAME, 'foo'); - $this->assertEquals(3, $this->instance->file_put_contents(self::NFC_NAME, 'bar')); - $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME)); - clearstatcache(); - $this->assertEquals(5, $this->instance->file_put_contents('/' . self::NFC_NAME, 'baric')); - $this->assertEquals('baric', $this->instance->file_get_contents(self::NFC_NAME)); - clearstatcache(); - $this->assertEquals(8, $this->instance->file_put_contents('/' . self::NFC_NAME, 'barbaric')); - $this->assertEquals('barbaric', $this->instance->file_get_contents('//' . self::NFC_NAME)); - } - - public static function sourceAndTargetDirectoryProvider(): array { - return [ - [self::NFC_NAME . '1', self::NFC_NAME . '2'], - [self::NFD_NAME . '1', self::NFC_NAME . '2'], - [self::NFC_NAME . '1', self::NFD_NAME . '2'], - [self::NFD_NAME . '1', self::NFD_NAME . '2'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('sourceAndTargetDirectoryProvider')] - public function testCopyAndMoveEncodedFolder($sourceDir, $targetDir): void { - $this->sourceStorage->mkdir($sourceDir); - $this->sourceStorage->mkdir($targetDir); - $this->sourceStorage->file_put_contents($sourceDir . '/test.txt', 'bar'); - $this->assertTrue($this->instance->copy(self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test.txt')); - - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test.txt')); - - $this->assertTrue($this->instance->rename(self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test2.txt')); - $this->assertFalse($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test2.txt')); - - $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test2.txt')); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('sourceAndTargetDirectoryProvider')] - public function testCopyAndMoveFromStorageEncodedFolder($sourceDir, $targetDir): void { - $this->sourceStorage->mkdir($sourceDir); - $this->sourceStorage->mkdir($targetDir); - $this->sourceStorage->file_put_contents($sourceDir . '/test.txt', 'bar'); - $this->assertTrue($this->instance->copyFromStorage($this->instance, self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test.txt')); - - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test.txt')); - $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test.txt')); - - $this->assertTrue($this->instance->moveFromStorage($this->instance, self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test2.txt')); - $this->assertFalse($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); - $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test2.txt')); - - $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test2.txt')); - } - - public function testNormalizedDirectoryEntriesOpenDir(): void { - $this->sourceStorage->mkdir('/test'); - $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); - - $this->assertTrue($this->instance->file_exists('/test/' . self::NFC_NAME)); - $this->assertTrue($this->instance->file_exists('/test/' . self::NFD_NAME)); - - $dh = $this->instance->opendir('/test'); - $content = []; - while (($file = readdir($dh)) !== false) { - if ($file !== '.' && $file !== '..') { - $content[] = $file; - } - } - - $this->assertCount(1, $content); - $this->assertEquals(self::NFC_NAME, $content[0]); - } - - public function testNormalizedDirectoryEntriesGetDirectoryContent(): void { - $this->sourceStorage->mkdir('/test'); - $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); - - $this->assertTrue($this->instance->file_exists('/test/' . self::NFC_NAME)); - $this->assertTrue($this->instance->file_exists('/test/' . self::NFD_NAME)); - - $content = iterator_to_array($this->instance->getDirectoryContent('/test')); - $this->assertCount(1, $content); - $this->assertEquals(self::NFC_NAME, $content[0]['name']); - } - - public function testNormalizedGetMetaData(): void { - $this->sourceStorage->mkdir('/test'); - $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); - - $entry = $this->instance->getMetaData('/test/' . self::NFC_NAME); - $this->assertEquals(self::NFC_NAME, $entry['name']); - - $entry = $this->instance->getMetaData('/test/' . self::NFD_NAME); - $this->assertEquals(self::NFC_NAME, $entry['name']); - } - - /** - * Regression test of https://github.com/nextcloud/server/issues/50431 - */ - public function testNoMetadata() { - $this->assertNull($this->instance->getMetaData('/test/null')); - } - -} --- a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php +++ /dev/null @@ -1,1099 +0,0 @@ -buildMockModule(); - $this->encryptionManager = $this->getMockBuilder(\OC\Encryption\Manager::class) - ->disableOriginalConstructor() - ->onlyMethods(['getEncryptionModule', 'isEnabled']) - ->getMock(); - $this->encryptionManager->expects($this->any()) - ->method('getEncryptionModule') - ->willReturn($mockModule); - - $this->arrayCache = $this->createMock(ArrayCache::class); - $this->config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->groupManager = $this->getMockBuilder('\OC\Group\Manager') - ->disableOriginalConstructor() - ->getMock(); - - $this->util = $this->getMockBuilder(Util::class) - ->onlyMethods(['getUidAndFilename', 'isFile', 'isExcluded', 'stripPartialFileExtension']) - ->setConstructorArgs([new View(), new Manager( - $this->config, - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->createMock(LoggerInterface::class), - ), $this->groupManager, $this->config, $this->arrayCache]) - ->getMock(); - $this->util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturnCallback(function ($path) { - return ['user1', $path]; - }); - $this->util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnCallback(function ($path) { - return $path; - }); - - $this->file = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->onlyMethods(['getAccessList']) - ->getMock(); - $this->file->expects($this->any())->method('getAccessList')->willReturn([]); - - $this->logger = $this->createMock(LoggerInterface::class); - - $this->sourceStorage = new Temporary([]); - - $this->keyStore = $this->createMock(\OC\Encryption\Keys\Storage::class); - - $this->mount = $this->getMockBuilder(MountPoint::class) - ->disableOriginalConstructor() - ->onlyMethods(['getOption']) - ->getMock(); - $this->mount->expects($this->any())->method('getOption')->willReturnCallback(function ($option, $default) { - if ($option === 'encrypt' && $default === true) { - global $mockedMountPointEncryptionEnabled; - if ($mockedMountPointEncryptionEnabled !== null) { - return $mockedMountPointEncryptionEnabled; - } - } - return true; - }); - - $this->cache = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - $this->cache->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($path) { - return ['encrypted' => false, 'path' => $path]; - }); - - $this->mountManager = $this->createMock(\OC\Files\Mount\Manager::class); - $this->mountManager->method('findByStorageId') - ->willReturn([]); - - $this->instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $this->sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache - ] - ) - ->onlyMethods(['getMetaData', 'getCache', 'getEncryptionModule']) - ->getMock(); - - $this->instance->expects($this->any()) - ->method('getMetaData') - ->willReturnCallback(function ($path) { - return ['encrypted' => true, 'size' => $this->dummySize, 'path' => $path]; - }); - - $this->instance->expects($this->any()) - ->method('getCache') - ->willReturn($this->cache); - - $this->instance->expects($this->any()) - ->method('getEncryptionModule') - ->willReturn($mockModule); - } - - protected function buildMockModule(): IEncryptionModule&MockObject { - $this->encryptionModule = $this->getMockBuilder('\OCP\Encryption\IEncryptionModule') - ->disableOriginalConstructor() - ->onlyMethods(['getId', 'getDisplayName', 'begin', 'end', 'encrypt', 'decrypt', 'update', 'shouldEncrypt', 'getUnencryptedBlockSize', 'isReadable', 'encryptAll', 'prepareDecryptAll', 'isReadyForUser', 'needDetailedAccessList']) - ->getMock(); - - $this->encryptionModule->expects($this->any())->method('getId')->willReturn('UNIT_TEST_MODULE'); - $this->encryptionModule->expects($this->any())->method('getDisplayName')->willReturn('Unit test module'); - $this->encryptionModule->expects($this->any())->method('begin')->willReturn([]); - $this->encryptionModule->expects($this->any())->method('end')->willReturn(''); - $this->encryptionModule->expects($this->any())->method('encrypt')->willReturnArgument(0); - $this->encryptionModule->expects($this->any())->method('decrypt')->willReturnArgument(0); - $this->encryptionModule->expects($this->any())->method('update')->willReturn(true); - $this->encryptionModule->expects($this->any())->method('shouldEncrypt')->willReturn(true); - $this->encryptionModule->expects($this->any())->method('getUnencryptedBlockSize')->willReturn(8192); - $this->encryptionModule->expects($this->any())->method('isReadable')->willReturn(true); - $this->encryptionModule->expects($this->any())->method('needDetailedAccessList')->willReturn(false); - return $this->encryptionModule; - } - - /** - * - * @param string $path - * @param array $metaData - * @param bool $encrypted - * @param bool $unencryptedSizeSet - * @param int $storedUnencryptedSize - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetMetaData')] - public function testGetMetaData($path, $metaData, $encrypted, $unencryptedSizeSet, $storedUnencryptedSize, $expected): void { - $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor()->getMock(); - - $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - $cache->expects($this->any()) - ->method('get') - ->willReturnCallback( - function ($path) use ($encrypted) { - return new CacheEntry(['encrypted' => $encrypted, 'path' => $path, 'size' => 0, 'fileid' => 1]); - } - ); - - $this->instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ] - ) - ->onlyMethods(['getCache', 'verifyUnencryptedSize']) - ->getMock(); - - if ($unencryptedSizeSet) { - $this->invokePrivate($this->instance, 'unencryptedSize', [[$path => $storedUnencryptedSize]]); - } - - $fileEntry = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - $sourceStorage->expects($this->once())->method('getMetaData')->with($path) - ->willReturn($metaData); - $sourceStorage->expects($this->any()) - ->method('getCache') - ->with($path) - ->willReturn($fileEntry); - if ($metaData !== null) { - $fileEntry->expects($this->any()) - ->method('get') - ->with($metaData['fileid']); - } - - $this->instance->expects($this->any())->method('getCache')->willReturn($cache); - if ($expected !== null) { - $this->instance->expects($this->any())->method('verifyUnencryptedSize') - ->with($path, 0)->willReturn($expected['size']); - } - - $result = $this->instance->getMetaData($path); - if (isset($expected['encrypted'])) { - $this->assertSame($expected['encrypted'], (bool)$result['encrypted']); - - if (isset($expected['encryptedVersion'])) { - $this->assertSame($expected['encryptedVersion'], $result['encryptedVersion']); - } - } - - if ($expected !== null) { - $this->assertSame($expected['size'], $result['size']); - } else { - $this->assertSame(null, $result); - } - } - - public static function dataTestGetMetaData(): array { - return [ - ['/test.txt', ['size' => 42, 'encrypted' => 2, 'encryptedVersion' => 2, 'fileid' => 1], true, true, 12, ['size' => 12, 'encrypted' => true, 'encryptedVersion' => 2]], - ['/test.txt', null, true, true, 12, null], - ['/test.txt', ['size' => 42, 'encrypted' => 0, 'fileid' => 1], false, false, 12, ['size' => 42, 'encrypted' => false]], - ['/test.txt', ['size' => 42, 'encrypted' => false, 'fileid' => 1], true, false, 12, ['size' => 12, 'encrypted' => true]] - ]; - } - - public function testFilesize(): void { - $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - $cache->expects($this->any()) - ->method('get') - ->willReturn(new CacheEntry(['encrypted' => true, 'path' => '/test.txt', 'size' => 0, 'fileid' => 1])); - - $this->instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $this->sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ] - ) - ->onlyMethods(['getCache', 'verifyUnencryptedSize']) - ->getMock(); - - $this->instance->expects($this->any())->method('getCache')->willReturn($cache); - $this->instance->expects($this->any())->method('verifyUnencryptedSize') - ->willReturn(42); - - $this->assertSame(42, - $this->instance->filesize('/test.txt') - ); - } - - /** - * - * @param int $encryptedSize - * @param int $unencryptedSize - * @param bool $failure - * @param int $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestVerifyUnencryptedSize')] - public function testVerifyUnencryptedSize($encryptedSize, $unencryptedSize, $failure, $expected): void { - $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor()->getMock(); - - $this->instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ] - ) - ->onlyMethods(['fixUnencryptedSize']) - ->getMock(); - - $sourceStorage->expects($this->once())->method('filesize')->willReturn($encryptedSize); - - $this->instance->expects($this->any())->method('fixUnencryptedSize') - ->with('/test.txt', $encryptedSize, $unencryptedSize) - ->willReturnCallback( - function () use ($failure, $expected) { - if ($failure) { - throw new Exception(); - } else { - return $expected; - } - } - ); - - $this->assertSame( - $expected, - $this->invokePrivate($this->instance, 'verifyUnencryptedSize', ['/test.txt', $unencryptedSize]) - ); - } - - public static function dataTestVerifyUnencryptedSize(): array { - return [ - [120, 80, false, 80], - [120, 120, false, 80], - [120, -1, false, 80], - [120, -1, true, -1], - // Zero-byte encrypted file: on-disk size equals header only (8192) — should NOT recalculate - [8192, 0, false, 0], - // Encrypted file with content beyond header but unencrypted_size=0 — SHOULD recalculate - [16384, 0, false, 80], - ]; - } - - /** - * - * @param string $source - * @param string $target - * @param $encryptionEnabled - * @param boolean $renameKeysReturn - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyAndRename')] - public function testRename($source, - $target, - $encryptionEnabled, - $renameKeysReturn): void { - if ($encryptionEnabled) { - $this->keyStore - ->expects($this->once()) - ->method('renameKeys') - ->willReturn($renameKeysReturn); - } else { - $this->keyStore - ->expects($this->never())->method('renameKeys'); - } - $this->util->expects($this->any()) - ->method('isFile')->willReturn(true); - $this->encryptionManager->expects($this->once()) - ->method('isEnabled')->willReturn($encryptionEnabled); - - $this->instance->mkdir($source); - $this->instance->mkdir(dirname($target)); - $this->instance->rename($source, $target); - } - - public function testCopyEncryption(): void { - $this->instance->file_put_contents('source.txt', 'bar'); - $this->instance->copy('source.txt', 'target.txt'); - $this->assertSame('bar', $this->instance->file_get_contents('target.txt')); - $targetMeta = $this->instance->getMetaData('target.txt'); - $sourceMeta = $this->instance->getMetaData('source.txt'); - $this->assertSame($sourceMeta['encrypted'], $targetMeta['encrypted']); - $this->assertSame($sourceMeta['size'], $targetMeta['size']); - } - - /** - * data provider for testCopyTesting() and dataTestCopyAndRename() - * - * @return array - */ - public static function dataTestCopyAndRename(): array { - return [ - ['source', 'target', true, false, false], - ['source', 'target', true, true, false], - ['source', '/subFolder/target', true, false, false], - ['source', '/subFolder/target', true, true, true], - ['source', '/subFolder/target', false, true, false], - ]; - } - - public function testIsLocal(): void { - $this->encryptionManager->expects($this->once()) - ->method('isEnabled')->willReturn(true); - $this->assertFalse($this->instance->isLocal()); - } - - /** - * - * @param string $path - * @param boolean $rmdirResult - * @param boolean $isExcluded - * @param boolean $encryptionEnabled - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRmdir')] - public function testRmdir($path, $rmdirResult, $isExcluded, $encryptionEnabled): void { - $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor()->getMock(); - - $util = $this->getMockBuilder('\OC\Encryption\Util')->disableOriginalConstructor()->getMock(); - - $sourceStorage->expects($this->once())->method('rmdir')->willReturn($rmdirResult); - $util->expects($this->any())->method('isExcluded')->willReturn($isExcluded); - $this->encryptionManager->expects($this->any())->method('isEnabled')->willReturn($encryptionEnabled); - - $encryptionStorage = new Encryption( - [ - 'storage' => $sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/mountPoint', - 'mount' => $this->mount - ], - $this->encryptionManager, - $util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ); - - if ($rmdirResult === true && $isExcluded === false && $encryptionEnabled === true) { - $this->keyStore->expects($this->once())->method('deleteAllFileKeys')->with('/mountPoint' . $path); - } else { - $this->keyStore->expects($this->never())->method('deleteAllFileKeys'); - } - - $encryptionStorage->rmdir($path); - } - - public static function dataTestRmdir(): array { - return [ - ['/file.txt', true, true, true], - ['/file.txt', false, true, true], - ['/file.txt', true, false, true], - ['/file.txt', false, false, true], - ['/file.txt', true, true, false], - ['/file.txt', false, true, false], - ['/file.txt', true, false, false], - ['/file.txt', false, false, false], - ]; - } - - /** - * - * @param boolean $excluded - * @param boolean $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyKeys')] - public function testCopyKeys($excluded, $expected): void { - $this->util->expects($this->once()) - ->method('isExcluded') - ->willReturn($excluded); - - if ($excluded) { - $this->keyStore->expects($this->never())->method('copyKeys'); - } else { - $this->keyStore->expects($this->once())->method('copyKeys')->willReturn(true); - } - - $this->assertSame($expected, - self::invokePrivate($this->instance, 'copyKeys', ['/source', '/target']) - ); - } - - public static function dataTestCopyKeys(): array { - return [ - [true, false], - [false, true], - ]; - } - - /** - * - * @param string $path - * @param bool $strippedPathExists - * @param string $strippedPath - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetHeader')] - public function testGetHeader($path, $strippedPathExists, $strippedPath): void { - $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor()->getMock(); - - $util = $this->getMockBuilder('\OC\Encryption\Util') - ->setConstructorArgs( - [ - new View(), - new Manager( - $this->config, - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->createMock(LoggerInterface::class), - ), - $this->groupManager, - $this->config, - $this->arrayCache - ] - )->getMock(); - - $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - $cache->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($path) { - return ['encrypted' => true, 'path' => $path]; - }); - - $instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ] - ) - ->onlyMethods(['getCache', 'readFirstBlock']) - ->getMock(); - - $instance->method('getCache')->willReturn($cache); - - $util->method('parseRawHeader') - ->willReturn([Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']); - - if ($strippedPathExists) { - $instance->method('readFirstBlock') - ->with($strippedPath)->willReturn(''); - } else { - $instance->method('readFirstBlock') - ->with($path)->willReturn(''); - } - - $util->expects($this->once())->method('stripPartialFileExtension') - ->with($path)->willReturn($strippedPath); - $sourceStorage->expects($this->once()) - ->method('is_file') - ->with($strippedPath) - ->willReturn($strippedPathExists); - - $this->invokePrivate($instance, 'getHeader', [$path]); - } - - public static function dataTestGetHeader(): array { - return [ - ['/foo/bar.txt', false, '/foo/bar.txt'], - ['/foo/bar.txt.part', false, '/foo/bar.txt'], - ['/foo/bar.txt.ocTransferId7437493.part', false, '/foo/bar.txt'], - ['/foo/bar.txt.part', true, '/foo/bar.txt'], - ['/foo/bar.txt.ocTransferId7437493.part', true, '/foo/bar.txt'], - ]; - } - - /** - * test if getHeader adds the default module correctly to the header for - * legacy files - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetHeaderAddLegacyModule')] - public function testGetHeaderAddLegacyModule($header, $isEncrypted, $strippedPathExists, $expected): void { - $sourceStorage = $this->getMockBuilder(\OC\Files\Storage\Storage::class) - ->disableOriginalConstructor()->getMock(); - - $sourceStorage->expects($this->once()) - ->method('is_file') - ->with('test.txt') - ->willReturn($strippedPathExists); - - $util = $this->getMockBuilder(Util::class) - ->onlyMethods(['stripPartialFileExtension', 'parseRawHeader']) - ->setConstructorArgs([new View(), new Manager( - $this->config, - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->createMock(LoggerInterface::class), - ), $this->groupManager, $this->config, $this->arrayCache]) - ->getMock(); - $util->expects($this->any()) - ->method('stripPartialFileExtension') - ->willReturnCallback(function ($path) { - return $path; - }); - - $cache = $this->createMock(Cache::class); - $cache->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($path) use ($isEncrypted) { - return ['encrypted' => $isEncrypted, 'path' => $path]; - }); - - $instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache, - ] - ) - ->onlyMethods(['readFirstBlock', 'getCache']) - ->getMock(); - - $instance->method('readFirstBlock')->willReturn(''); - - $util->method(('parseRawHeader'))->willReturn($header); - $instance->method('getCache')->willReturn($cache); - - $result = $this->invokePrivate($instance, 'getHeader', ['test.txt']); - $this->assertSameSize($expected, $result); - foreach ($result as $key => $value) { - $this->assertArrayHasKey($key, $expected); - $this->assertSame($expected[$key], $value); - } - } - - public static function dataTestGetHeaderAddLegacyModule(): array { - return [ - [['cipher' => 'AES-128'], true, true, ['cipher' => 'AES-128', Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']], - [[], true, false, []], - [[], true, true, [Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']], - [[], false, true, []], - ]; - } - - public static function dataCopyBetweenStorage(): array { - return [ - [true, true, true], - [true, false, false], - [false, true, false], - [false, false, false], - ]; - } - - public static function dataUpdateEncryptedVersion(): array { - return [ - // the target is written through the encryption stream, which signs its blocks - // with the version that follows the version of the file they replace - 'copy onto an existing file' => [['encryptedVersion' => 4], ['encryptedVersion' => 3], false, 3], - 'copy onto a new file' => [['encryptedVersion' => 4], false, false, 1], - 'copy onto a file that is not encrypted yet' => [['encryptedVersion' => 4], ['encryptedVersion' => 0], false, 1], - // a 1:1 copy reuses the keys and the ciphertext of the source - '1:1 copy' => [['encryptedVersion' => 5], false, true, 5], - '1:1 copy of a file that is not encrypted yet' => [['encryptedVersion' => 0], false, true, 1], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataUpdateEncryptedVersion')] - public function testUpdateEncryptedVersion( - array|false $sourceCacheEntry, - array|false $targetCacheEntry, - bool $keepEncryptionVersion, - int $expectedVersion, - ): void { - $sourceCache = $this->createMock(ICache::class); - $sourceCache->method('get') - ->with('source.txt') - ->willReturn($sourceCacheEntry); - $sourceStorage = $this->createMock(\OC\Files\Storage\Storage::class); - $sourceStorage->method('getCache') - ->willReturn($sourceCache); - - $targetCache = $this->createMock(ICache::class); - $targetCache->method('get') - ->with('target.txt') - ->willReturn($targetCacheEntry); - $targetCache->expects($this->once()) - ->method('put') - ->with('target.txt', ['encrypted' => true, 'encryptedVersion' => $expectedVersion]); - - $instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $this->sourceStorage, - 'root' => 'foo', - 'mountPoint' => '/', - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache - ] - ) - ->onlyMethods(['getCache', 'getEncryptionModule']) - ->getMock(); - $instance->method('getCache')->willReturn($targetCache); - $instance->method('getEncryptionModule')->willReturn($this->encryptionModule); - - $this->encryptionManager->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - global $mockedMountPointEncryptionEnabled; - $mockedMountPointEncryptionEnabled = true; - - $this->invokePrivate($instance, 'updateEncryptedVersion', [$sourceStorage, 'source.txt', 'target.txt', false, $keepEncryptionVersion]); - } - - public function testCopyBetweenStorageMinimumEncryptedVersion(): void { - $storage2 = $this->createMock(\OC\Files\Storage\Storage::class); - - $sourceInternalPath = $targetInternalPath = 'file.txt'; - $preserveMtime = $isRename = false; - - $storage2->expects($this->any()) - ->method('fopen') - ->willReturnCallback(function ($path, $mode) { - $temp = Server::get(ITempManager::class); - return fopen($temp->getTemporaryFile(), $mode); - }); - $storage2->method('getId') - ->willReturn('stroage2'); - $cache = $this->createMock(ICache::class); - $cache->expects($this->once()) - ->method('get') - ->with($sourceInternalPath) - ->willReturn(['encryptedVersion' => 0]); - $storage2->expects($this->once()) - ->method('getCache') - ->willReturn($cache); - $this->encryptionManager->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - global $mockedMountPointEncryptionEnabled; - $mockedMountPointEncryptionEnabled = true; - - $expectedCachePut = [ - 'encrypted' => true, - ]; - $expectedCachePut['encryptedVersion'] = 1; - - $this->cache->expects($this->once()) - ->method('put') - ->with($sourceInternalPath, $expectedCachePut); - - $this->invokePrivate($this->instance, 'copyBetweenStorage', [$storage2, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename]); - - $this->assertFalse(false); - } - - /** - * - * @param bool $encryptionEnabled - * @param bool $mountPointEncryptionEnabled - * @param bool $expectedEncrypted - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataCopyBetweenStorage')] - public function testCopyBetweenStorage($encryptionEnabled, $mountPointEncryptionEnabled, $expectedEncrypted): void { - $storage2 = $this->createMock(\OC\Files\Storage\Storage::class); - - $sourceInternalPath = $targetInternalPath = 'file.txt'; - $preserveMtime = $isRename = false; - - $storage2->expects($this->any()) - ->method('fopen') - ->willReturnCallback(function ($path, $mode) { - $temp = Server::get(ITempManager::class); - return fopen($temp->getTemporaryFile(), $mode); - }); - $storage2->method('getId') - ->willReturn('stroage2'); - if ($expectedEncrypted) { - $cache = $this->createMock(ICache::class); - $cache->expects($this->once()) - ->method('get') - ->with($sourceInternalPath) - ->willReturn(['encryptedVersion' => 12345]); - $storage2->expects($this->once()) - ->method('getCache') - ->willReturn($cache); - } - $this->encryptionManager->expects($this->any()) - ->method('isEnabled') - ->willReturn($encryptionEnabled); - // FIXME can not overwrite the return after definition - // $this->mount->expects($this->at(0)) - // ->method('getOption') - // ->with('encrypt', true) - // ->willReturn($mountPointEncryptionEnabled); - global $mockedMountPointEncryptionEnabled; - $mockedMountPointEncryptionEnabled = $mountPointEncryptionEnabled; - - $expectedCachePut = [ - 'encrypted' => $expectedEncrypted, - ]; - if ($expectedEncrypted === true) { - $expectedCachePut['encryptedVersion'] = 1; - } - - $this->arrayCache->expects($this->never())->method('set'); - - $this->cache->expects($this->once()) - ->method('put') - ->with($sourceInternalPath, $expectedCachePut); - - $this->invokePrivate($this->instance, 'copyBetweenStorage', [$storage2, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename]); - - $this->assertFalse(false); - } - - /** - * - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $copyResult - * @param bool $encrypted - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyBetweenStorageVersions')] - public function testCopyBetweenStorageVersions($sourceInternalPath, $targetInternalPath, $copyResult, $encrypted): void { - $sourceStorage = $this->createMock(\OC\Files\Storage\Storage::class); - - $targetStorage = $this->createMock(\OC\Files\Storage\Storage::class); - - $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') - ->disableOriginalConstructor()->getMock(); - - $mountPoint = '/mountPoint'; - - /** @var Encryption |MockObject $instance */ - $instance = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - [ - 'storage' => $targetStorage, - 'root' => 'foo', - 'mountPoint' => $mountPoint, - 'mount' => $this->mount - ], - $this->encryptionManager, - $this->util, - $this->logger, - $this->file, - null, - $this->keyStore, - $this->mountManager, - $this->arrayCache - ] - ) - ->onlyMethods(['updateUnencryptedSize', 'getCache']) - ->getMock(); - - $targetStorage->expects($this->once())->method('copyFromStorage') - ->with($sourceStorage, $sourceInternalPath, $targetInternalPath) - ->willReturn($copyResult); - - $instance->expects($this->any())->method('getCache') - ->willReturn($cache); - - $this->arrayCache->expects($this->once())->method('set') - ->with('encryption_copy_version_' . $sourceInternalPath, true); - - if ($copyResult) { - $cache->expects($this->once())->method('get') - ->with($sourceInternalPath) - ->willReturn(new CacheEntry(['encrypted' => $encrypted, 'size' => 42])); - if ($encrypted) { - $instance->expects($this->once())->method('updateUnencryptedSize') - ->with($mountPoint . $targetInternalPath, 42); - } else { - $instance->expects($this->never())->method('updateUnencryptedSize'); - } - } else { - $instance->expects($this->never())->method('updateUnencryptedSize'); - } - - $result = $this->invokePrivate( - $instance, - 'copyBetweenStorage', - [ - $sourceStorage, - $sourceInternalPath, - $targetInternalPath, - false, - false - ] - ); - - $this->assertSame($copyResult, $result); - } - - public static function dataTestCopyBetweenStorageVersions(): array { - return [ - ['/files/foo.txt', '/files_versions/foo.txt.768743', true, true], - ['/files/foo.txt', '/files_versions/foo.txt.768743', true, false], - ['/files/foo.txt', '/files_versions/foo.txt.768743', false, true], - ['/files/foo.txt', '/files_versions/foo.txt.768743', false, false], - ['/files_versions/foo.txt.6487634', '/files/foo.txt', true, true], - ['/files_versions/foo.txt.6487634', '/files/foo.txt', true, false], - ['/files_versions/foo.txt.6487634', '/files/foo.txt', false, true], - ['/files_versions/foo.txt.6487634', '/files/foo.txt', false, false], - ]; - } - - /** - * @param string $path - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsVersion')] - public function testIsVersion($path, $expected): void { - $this->assertSame($expected, - $this->invokePrivate($this->instance, 'isVersion', [$path]) - ); - } - - public static function dataTestIsVersion(): array { - return [ - ['files_versions/foo', true], - ['/files_versions/foo', true], - ['//files_versions/foo', true], - ['files/versions/foo', false], - ['files/files_versions/foo', false], - ['files_versions_test/foo', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestShouldEncrypt')] - public function testShouldEncrypt( - bool $encryptionEnabled, - bool|string $encryptMountPoint, - ?bool $encryptionModule, - bool $encryptionModuleShouldEncrypt, - bool $expected, - ): void { - $encryptionManager = $this->createMock(\OC\Encryption\Manager::class); - $util = $this->createMock(Util::class); - $fileHelper = $this->createMock(IFile::class); - $keyStorage = $this->createMock(IStorage::class); - $mountManager = $this->createMock(\OC\Files\Mount\Manager::class); - $mount = $this->createMock(IMountPoint::class); - $arrayCache = $this->createMock(ArrayCache::class); - $path = '/welcome.txt'; - $fullPath = 'admin/files/welcome.txt'; - $defaultEncryptionModule = $this->createMock(IEncryptionModule::class); - - $wrapper = $this->getMockBuilder(Encryption::class) - ->setConstructorArgs( - [ - ['mountPoint' => '', 'mount' => $mount, 'storage' => null], - $encryptionManager, - $util, - $this->logger, - $fileHelper, - null, - $keyStorage, - $mountManager, - $arrayCache - ] - ) - ->onlyMethods(['getFullPath', 'getEncryptionModule']) - ->getMock(); - - $encryptionManager->method('isEnabled')->willReturn($encryptionEnabled); - - if ($encryptionModule === true) { - /** @var IEncryptionModule|MockObject $encryptionModule */ - $encryptionModule = $this->createMock(IEncryptionModule::class); - } - - $wrapper->method('getFullPath')->with($path)->willReturn($fullPath); - $wrapper->expects(($encryptionEnabled && $encryptMountPoint) ? $this->once() : $this->never()) - ->method('getEncryptionModule') - ->with($fullPath) - ->willReturnCallback( - function () use ($encryptionModule) { - if ($encryptionModule === false) { - throw new ModuleDoesNotExistsException(); - } - return $encryptionModule; - } - ); - $mount->expects($encryptionEnabled ? $this->once() : $this->never()) - ->method('getOption')->with('encrypt', true) - ->willReturn($encryptMountPoint); - - if ($encryptionModule !== null && $encryptionModule !== false) { - $encryptionModule - ->method('shouldEncrypt') - ->with($fullPath) - ->willReturn($encryptionModuleShouldEncrypt); - } - - if ($encryptionModule === null) { - $encryptionManager->expects($this->once()) - ->method('getEncryptionModule') - ->willReturn($defaultEncryptionModule); - } - $defaultEncryptionModule->method('shouldEncrypt')->willReturn(true); - - $result = $this->invokePrivate($wrapper, 'shouldEncrypt', [$path]); - - $this->assertSame($expected, $result); - } - - public static function dataTestShouldEncrypt(): array { - return [ - [true, false, false, false, false], - [true, '', false, false, false], - [true, true, false, false, false], - [true, true, true, false, false], - [true, true, true, true, true], - [true, true, null, false, true], - [false, true, true, true, false], - ]; - } -} --- a/tests/lib/Files/Storage/Wrapper/JailTest.php +++ /dev/null @@ -1,56 +0,0 @@ -sourceStorage = new Temporary([]); - $this->sourceStorage->mkdir('foo'); - $this->instance = new Jail([ - 'storage' => $this->sourceStorage, - 'root' => 'foo' - ]); - } - - #[\Override] - protected function tearDown(): void { - // test that nothing outside our jail is touched - $contents = []; - $dh = $this->sourceStorage->opendir(''); - while (($file = readdir($dh)) !== false) { - if (!Filesystem::isIgnoredDir($file)) { - $contents[] = $file; - } - } - $this->assertEquals(['foo'], $contents); - $this->sourceStorage->cleanUp(); - parent::tearDown(); - } - - public function testMkDirRooted(): void { - $this->instance->mkdir('bar'); - $this->assertTrue($this->sourceStorage->is_dir('foo/bar')); - } - - public function testFilePutContentsRooted(): void { - $this->instance->file_put_contents('bar', 'asd'); - $this->assertEquals('asd', $this->sourceStorage->file_get_contents('foo/bar')); - } -} --- a/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php +++ /dev/null @@ -1,69 +0,0 @@ -fakeTime = 0; - $this->sourceStorage = new Temporary([]); - $this->clock = $this->createMock(ClockInterface::class); - $this->clock->method('now')->willReturnCallback(function () { - if ($this->fakeTime) { - return new \DateTimeImmutable("@{$this->fakeTime}"); - } else { - return new \DateTimeImmutable(); - } - }); - $this->instance = $this->getWrappedStorage(); - } - - #[\Override] - protected function tearDown(): void { - $this->sourceStorage->cleanUp(); - parent::tearDown(); - } - - protected function getWrappedStorage() { - return new KnownMtime([ - 'storage' => $this->sourceStorage, - 'clock' => $this->clock, - ]); - } - - public function testNewerKnownMtime(): void { - $future = time() + 1000; - $this->fakeTime = $future; - - $this->instance->file_put_contents('foo.txt', 'bar'); - - // fuzzy match since the clock might have ticked - $this->assertLessThan(2, abs(time() - $this->sourceStorage->filemtime('foo.txt'))); - $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->stat('foo.txt')['mtime']); - $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->getMetaData('foo.txt')['mtime']); - - $this->assertEquals($future, $this->instance->filemtime('foo.txt')); - $this->assertEquals($future, $this->instance->stat('foo.txt')['mtime']); - $this->assertEquals($future, $this->instance->getMetaData('foo.txt')['mtime']); - } -} --- a/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php +++ /dev/null @@ -1,180 +0,0 @@ -sourceStorage = new Temporary([]); - $this->instance = $this->getMaskedStorage(Constants::PERMISSION_ALL); - } - - #[\Override] - protected function tearDown(): void { - $this->sourceStorage->cleanUp(); - parent::tearDown(); - } - - protected function getMaskedStorage($mask) { - return new PermissionsMask([ - 'storage' => $this->sourceStorage, - 'mask' => $mask - ]); - } - - public function testMkdirNoCreate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); - $this->assertFalse($storage->mkdir('foo')); - $this->assertFalse($storage->file_exists('foo')); - } - - public function testRmdirNoDelete(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); - $this->assertTrue($storage->mkdir('foo')); - $this->assertTrue($storage->file_exists('foo')); - $this->assertFalse($storage->rmdir('foo')); - $this->assertTrue($storage->file_exists('foo')); - } - - public function testTouchNewFileNoCreate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); - $this->assertFalse($storage->touch('foo')); - $this->assertFalse($storage->file_exists('foo')); - } - - public function testTouchNewFileNoUpdate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); - $this->assertTrue($storage->touch('foo')); - $this->assertTrue($storage->file_exists('foo')); - } - - public function testTouchExistingFileNoUpdate(): void { - $this->sourceStorage->touch('foo'); - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); - $this->assertFalse($storage->touch('foo')); - } - - public function testUnlinkNoDelete(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); - $this->assertTrue($storage->touch('foo')); - $this->assertTrue($storage->file_exists('foo')); - $this->assertFalse($storage->unlink('foo')); - $this->assertTrue($storage->file_exists('foo')); - } - - public function testPutContentsNewFileNoUpdate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); - $this->assertEquals(3, $storage->file_put_contents('foo', 'bar')); - $this->assertEquals('bar', $storage->file_get_contents('foo')); - } - - public function testPutContentsNewFileNoCreate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); - $this->assertFalse($storage->file_put_contents('foo', 'bar')); - } - - public function testPutContentsExistingFileNoUpdate(): void { - $this->sourceStorage->touch('foo'); - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); - $this->assertFalse($storage->file_put_contents('foo', 'bar')); - } - - public function testFopenExistingFileNoUpdate(): void { - $this->sourceStorage->touch('foo'); - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); - $this->assertFalse($storage->fopen('foo', 'w')); - } - - public function testFopenNewFileNoCreate(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); - $this->assertFalse($storage->fopen('foo', 'w')); - } - - public function testScanNewFiles(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE); - $storage->file_put_contents('foo', 'bar'); - $storage->getScanner()->scan(''); - - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); - $this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions()); - } - - public function testScanNewWrappedFiles(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE); - $wrappedStorage = new Wrapper(['storage' => $storage]); - $wrappedStorage->file_put_contents('foo', 'bar'); - $wrappedStorage->getScanner()->scan(''); - - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); - $this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions()); - } - - public function testScanNewFilesNested(): void { - $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE); - $nestedStorage = new PermissionsMask([ - 'storage' => $storage, - 'mask' => Constants::PERMISSION_READ + Constants::PERMISSION_CREATE - ]); - $wrappedStorage = new Wrapper(['storage' => $nestedStorage]); - $wrappedStorage->file_put_contents('foo', 'bar'); - $wrappedStorage->getScanner()->scan(''); - - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); - $this->assertEquals(Constants::PERMISSION_READ + Constants::PERMISSION_UPDATE, $storage->getCache()->get('foo')->getPermissions()); - $this->assertEquals(Constants::PERMISSION_READ, $wrappedStorage->getCache()->get('foo')->getPermissions()); - } - - public function testScanUnchanged(): void { - $this->sourceStorage->mkdir('foo'); - $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar'); - - $this->sourceStorage->getScanner()->scan('foo'); - - $storage = $this->getMaskedStorage(Constants::PERMISSION_READ); - $scanner = $storage->getScanner(); - $called = false; - $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called): void { - $called = true; - }); - $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE); - - $this->assertFalse($called); - } - - public function testScanUnchangedWrapped(): void { - $this->sourceStorage->mkdir('foo'); - $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar'); - - $this->sourceStorage->getScanner()->scan('foo'); - - $storage = $this->getMaskedStorage(Constants::PERMISSION_READ); - $wrappedStorage = new Wrapper(['storage' => $storage]); - $scanner = $wrappedStorage->getScanner(); - $called = false; - $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called): void { - $called = true; - }); - $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE); - - $this->assertFalse($called); - } -} --- a/tests/lib/Files/Storage/Wrapper/QuotaTest.php +++ /dev/null @@ -1,280 +0,0 @@ -tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $storage = new Local(['datadir' => $this->tmpDir]); - $this->instance = new Quota(['storage' => $storage, 'quota' => 10000000]); - } - - #[\Override] - protected function tearDown(): void { - Files::rmdirr($this->tmpDir); - parent::tearDown(); - } - - /** - * @param integer $limit - */ - protected function getLimitedStorage($limit) { - $storage = new Local(['datadir' => $this->tmpDir]); - $storage->mkdir('files'); - $storage->getScanner()->scan(''); - return new Quota(['storage' => $storage, 'quota' => $limit]); - } - - public function testFilePutContentsNotEnoughSpace(): void { - $instance = $this->getLimitedStorage(3); - $this->assertFalse($instance->file_put_contents('files/foo', 'foobar')); - } - - public function testCopyNotEnoughSpace(): void { - $instance = $this->getLimitedStorage(9); - $this->assertEquals(6, $instance->file_put_contents('files/foo', 'foobar')); - $instance->getScanner()->scan(''); - $this->assertFalse($instance->copy('files/foo', 'files/bar')); - } - - public function testFreeSpace(): void { - $instance = $this->getLimitedStorage(9); - $this->assertEquals(9, $instance->free_space('')); - } - - public function testFreeSpaceWithUsedSpace(): void { - $instance = $this->getLimitedStorage(9); - $instance->getCache()->put( - '', ['size' => 3] - ); - $this->assertEquals(6, $instance->free_space('')); - } - - public function testFreeSpaceWithUnknownDiskSpace(): void { - $storage = $this->getMockBuilder(Local::class) - ->onlyMethods(['free_space']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $storage->expects($this->any()) - ->method('free_space') - ->willReturn(-2); - $storage->getScanner()->scan(''); - - $instance = new Quota(['storage' => $storage, 'quota' => 9]); - $instance->getCache()->put( - '', ['size' => 3] - ); - $this->assertEquals(6, $instance->free_space('')); - } - - public function testFreeSpaceWithUsedSpaceAndEncryption(): void { - $instance = $this->getLimitedStorage(9); - $instance->getCache()->put( - '', ['size' => 7] - ); - $this->assertEquals(2, $instance->free_space('')); - } - - public function testFWriteNotEnoughSpace(): void { - $instance = $this->getLimitedStorage(9); - $stream = $instance->fopen('files/foo', 'w+'); - $this->assertEquals(6, fwrite($stream, 'foobar')); - $this->assertEquals(3, fwrite($stream, 'qwerty')); - fclose($stream); - $this->assertEquals('foobarqwe', $instance->file_get_contents('files/foo')); - } - - public function testStreamCopyWithEnoughSpace(): void { - $instance = $this->getLimitedStorage(16); - $inputStream = fopen('data://text/plain,foobarqwerty', 'r'); - $outputStream = $instance->fopen('files/foo', 'w+'); - $count = stream_copy_to_stream($inputStream, $outputStream); - $this->assertEquals(12, $count); - fclose($inputStream); - fclose($outputStream); - } - - public function testStreamCopyNotEnoughSpace(): void { - $instance = $this->getLimitedStorage(9); - $inputStream = fopen('data://text/plain,foobarqwerty', 'r'); - $outputStream = $instance->fopen('files/foo', 'w+'); - $count = stream_copy_to_stream($inputStream, $outputStream); - $this->assertFalse($count); - fclose($inputStream); - fclose($outputStream); - } - - public function testReturnFalseWhenFopenFailed(): void { - $failStorage = $this->getMockBuilder(Local::class) - ->onlyMethods(['fopen']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $failStorage->expects($this->any()) - ->method('fopen') - ->willReturn(false); - - $instance = new Quota(['storage' => $failStorage, 'quota' => 1000]); - - $this->assertFalse($instance->fopen('failedfopen', 'r')); - } - - public function testReturnRegularStreamOnRead(): void { - $instance = $this->getLimitedStorage(9); - - // create test file first - $stream = $instance->fopen('files/foo', 'w+'); - fwrite($stream, 'blablacontent'); - fclose($stream); - - $stream = $instance->fopen('files/foo', 'r'); - $meta = stream_get_meta_data($stream); - $this->assertEquals('plainfile', $meta['wrapper_type']); - fclose($stream); - - $stream = $instance->fopen('files/foo', 'rb'); - $meta = stream_get_meta_data($stream); - $this->assertEquals('plainfile', $meta['wrapper_type']); - fclose($stream); - } - - public function testReturnRegularStreamWhenOutsideFiles(): void { - $instance = $this->getLimitedStorage(9); - $instance->mkdir('files_other'); - - // create test file first - $stream = $instance->fopen('files_other/foo', 'w+'); - $meta = stream_get_meta_data($stream); - $this->assertEquals('plainfile', $meta['wrapper_type']); - fclose($stream); - } - - public function testReturnQuotaStreamOnWrite(): void { - $instance = $this->getLimitedStorage(9); - $stream = $instance->fopen('files/foo', 'w+'); - $meta = stream_get_meta_data($stream); - $expected_type = 'user-space'; - $this->assertEquals($expected_type, $meta['wrapper_type']); - fclose($stream); - } - - public function testSpaceRoot(): void { - $storage = $this->getMockBuilder(Local::class)->disableOriginalConstructor()->getMock(); - $cache = $this->getMockBuilder('\OC\Files\Cache\Cache')->disableOriginalConstructor()->getMock(); - $storage->expects($this->once()) - ->method('getCache') - ->willReturn($cache); - $storage->expects($this->once()) - ->method('free_space') - ->willReturn(2048); - $cache->expects($this->once()) - ->method('get') - ->with('files') - ->willReturn(new CacheEntry(['size' => 50])); - - $instance = new Quota(['storage' => $storage, 'quota' => 1024, 'root' => 'files']); - - $this->assertEquals(1024 - 50, $instance->free_space('')); - } - - public function testInstanceOfStorageWrapper(): void { - $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Local')); - $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Wrapper')); - $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')); - } - - public function testNoMkdirQuotaZero(): void { - $instance = $this->getLimitedStorage(0.0); - $this->assertFalse($instance->mkdir('files')); - $this->assertFalse($instance->mkdir('files/foobar')); - } - - public function testMkdirQuotaZeroTrashbin(): void { - $instance = $this->getLimitedStorage(0.0); - $this->assertTrue($instance->mkdir('files_trashbin')); - $this->assertTrue($instance->mkdir('files_trashbin/files')); - $this->assertTrue($instance->mkdir('files_versions')); - $this->assertTrue($instance->mkdir('cache')); - } - - public function testNoTouchQuotaZero(): void { - $instance = $this->getLimitedStorage(0.0); - $this->assertFalse($instance->touch('foobar')); - } - - public function testNoFopenQuotaZero(): void { - $instance = $this->getLimitedStorage(0.0); - $fh = $instance->fopen('files/test.txt', 'w'); - $this->assertFalse($fh); - } - - public function testNoWriteStreamQuota(): void { - $instance = $this->getLimitedStorage(5.0); - $stream = fopen('php://temp', 'w+'); - fwrite($stream, 'foo'); - rewind($stream); - $instance->writeStream('files/test.txt', $stream); - - $stream = fopen('php://temp', 'w+'); - fwrite($stream, 'foobar'); - rewind($stream); - $this->expectException(NotEnoughSpaceException::class); - $instance->writeStream('files/test.txt', $stream); - } - - public function testWriteStreamAllowsUploadPathWithUnlimitedFreeSpace(): void { - $storage = $this->getMockBuilder(Local::class) - ->onlyMethods(['free_space']) - ->setConstructorArgs([['datadir' => $this->tmpDir]]) - ->getMock(); - $storage->expects($this->any()) - ->method('free_space') - ->willReturn(Files\FileInfo::SPACE_UNLIMITED); - $storage->mkdir('uploads'); - - $instance = new Quota(['storage' => $storage, 'quota' => 5.0]); - - $stream = fopen('php://temp', 'w+'); - fwrite($stream, 'foobar'); - rewind($stream); - - $this->assertEquals(6, $instance->writeStream('uploads/chunk', $stream, 6)); - $this->assertEquals('foobar', $instance->file_get_contents('uploads/chunk')); - } - - public function testNoWriteStreamQuotaZero(): void { - $instance = $this->getLimitedStorage(0.0); - $stream = fopen('php://temp', 'w+'); - $this->expectException(NotEnoughSpaceException::class); - $instance->writeStream('files/test.txt', $stream); - } -} --- a/tests/lib/Files/Storage/Wrapper/WrapperTest.php +++ /dev/null @@ -1,42 +0,0 @@ -tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); - $storage = new Local(['datadir' => $this->tmpDir]); - $this->instance = new Wrapper(['storage' => $storage]); - } - - #[\Override] - protected function tearDown(): void { - Files::rmdirr($this->tmpDir); - parent::tearDown(); - } - - public function testInstanceOfStorageWrapper(): void { - $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Local')); - $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Wrapper')); - } -} --- a/tests/lib/Files/Stream/DummyEncryptionWrapper.php +++ /dev/null @@ -1,26 +0,0 @@ - $wrapper - * @return resource - */ - protected function getStream(string $fileName, string $mode, int $unencryptedSize, string $wrapper = self::DEFAULT_WRAPPER, int $unencryptedSizeOnClose = 0) { - clearstatcache(); - $size = filesize($fileName); - $source = fopen($fileName, $mode); - $internalPath = $fileName; - $fullPath = $fileName; - $header = []; - $uid = ''; - $this->encryptionModule = $this->buildMockModule(); - $cache = $this->createMock(ICache::class); - $storage = $this->getMockBuilder('\OC\Files\Storage\Storage') - ->disableOriginalConstructor()->getMock(); - $encStorage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Encryption') - ->disableOriginalConstructor()->getMock(); - $config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $arrayCache = $this->createMock(ArrayCache::class); - $groupManager = $this->getMockBuilder('\OC\Group\Manager') - ->disableOriginalConstructor() - ->getMock(); - $file = $this->getMockBuilder('\OC\Encryption\File') - ->disableOriginalConstructor() - ->onlyMethods(['getAccessList']) - ->getMock(); - $file->expects($this->any())->method('getAccessList')->willReturn([]); - $util = $this->getMockBuilder('\OC\Encryption\Util') - ->onlyMethods(['getUidAndFilename']) - ->setConstructorArgs([new View(), new \OC\User\Manager( - $config, - $this->createMock(ICacheFactory::class), - $this->createMock(IEventDispatcher::class), - $this->createMock(LoggerInterface::class), - ), $groupManager, $config, $arrayCache]) - ->getMock(); - $util->expects($this->any()) - ->method('getUidAndFilename') - ->willReturn(['user1', $internalPath]); - $storage->expects($this->any())->method('getCache')->willReturn($cache); - $entry = new CacheEntry([ - 'fileid' => 5, - 'encryptedVersion' => 2, - 'unencrypted_size' => $unencryptedSizeOnClose, - ]); - $cache->expects($this->any())->method('get')->willReturn($entry); - $cache->expects($this->any())->method('update')->with(5, ['encrypted' => 3, 'encryptedVersion' => 3, 'unencrypted_size' => $unencryptedSizeOnClose]); - - return $wrapper::wrap( - $source, - $internalPath, - $fullPath, - $header, - $uid, - $this->encryptionModule, - $storage, - $encStorage, - $util, - $file, - $mode, - $size, - $unencryptedSize, - 8192, - true, - $wrapper, - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderStreamOpen')] - public function testStreamOpen( - $isMasterKeyUsed, - $mode, - $fullPath, - $fileExists, - $expectedSharePath, - $expectedSize, - $expectedUnencryptedSize, - $expectedReadOnly, - ): void { - // build mocks - $encryptionModuleMock = $this->createMock(IEncryptionModule::class); - $encryptionModuleMock->expects($this->any())->method('needDetailedAccessList')->willReturn(!$isMasterKeyUsed); - $encryptionModuleMock->expects($this->once()) - ->method('getUnencryptedBlockSize')->willReturn(99); - $encryptionModuleMock->expects($this->once()) - ->method('begin')->willReturn([]); - - $storageMock = $this->createMock(Storage::class); - $storageMock->expects($this->once())->method('file_exists')->willReturn($fileExists); - - $fileMock = $this->createMock(File::class); - if ($isMasterKeyUsed) { - $fileMock->expects($this->never())->method('getAccessList'); - } else { - $fileMock->expects($this->once())->method('getAccessList') - ->willReturnCallback(function ($sharePath) use ($expectedSharePath) { - $this->assertSame($expectedSharePath, $sharePath); - return []; - }); - } - $utilMock = $this->getMockBuilder(Util::class) - ->disableOriginalConstructor()->getMock(); - $utilMock->expects($this->any()) - ->method('getHeaderSize') - ->willReturn(8192); - - // get a instance of the stream wrapper - $streamWrapper = $this->getMockBuilder(Encryption::class) - ->onlyMethods(['loadContext', 'writeHeader', 'skipHeader']) - ->disableOriginalConstructor() - ->getMock(); - - // set internal properties of the stream wrapper - $stream = new \ReflectionClass(Encryption::class); - $encryptionModule = $stream->getProperty('encryptionModule'); - $encryptionModule->setValue($streamWrapper, $encryptionModuleMock); - $storage = $stream->getProperty('storage'); - $storage->setValue($streamWrapper, $storageMock); - $file = $stream->getProperty('file'); - $file->setValue($streamWrapper, $fileMock); - $util = $stream->getProperty('util'); - $util->setValue($streamWrapper, $utilMock); - $fullPathP = $stream->getProperty('fullPath'); - $fullPathP->setValue($streamWrapper, $fullPath); - $header = $stream->getProperty('header'); - $header->setValue($streamWrapper, []); - $this->invokePrivate($streamWrapper, 'signed', [true]); - $this->invokePrivate($streamWrapper, 'internalPath', [$fullPath]); - $this->invokePrivate($streamWrapper, 'uid', ['test']); - - // call stream_open, that's the method we want to test - $dummyVar = 'foo'; - $streamWrapper->stream_open('', $mode, '', $dummyVar); - - // check internal properties - $size = $stream->getProperty('size'); - $this->assertSame($expectedSize, $size->getValue($streamWrapper)); - - $unencryptedSize = $stream->getProperty('unencryptedSize'); - $this->assertSame($expectedUnencryptedSize, $unencryptedSize->getValue($streamWrapper)); - - $readOnly = $stream->getProperty('readOnly'); - $this->assertSame($expectedReadOnly, $readOnly->getValue($streamWrapper)); - } - - public static function dataProviderStreamOpen(): array { - return [ - [false, 'r', '/foo/bar/test.txt', true, '/foo/bar/test.txt', null, null, true], - [false, 'r', '/foo/bar/test.txt', false, '/foo/bar', null, null, true], - [false, 'w', '/foo/bar/test.txt', true, '/foo/bar/test.txt', 8192, 0, false], - [true, 'r', '/foo/bar/test.txt', true, '/foo/bar/test.txt', null, null, true], - [true, 'r', '/foo/bar/test.txt', false, '/foo/bar', null, null, true], - [true, 'w', '/foo/bar/test.txt', true, '/foo/bar/test.txt', 8192, 0, false], - ]; - } - - public function testWriteRead(): void { - $fileName = tempnam('/tmp', 'FOO'); - $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 6); - $this->assertEquals(6, fwrite($stream, 'foobar')); - fclose($stream); - - $stream = $this->getStream($fileName, 'r', 6); - $this->assertEquals('foobar', fread($stream, 100)); - fclose($stream); - - $stream = $this->getStream($fileName, 'r+', 6, self::DEFAULT_WRAPPER, 6); - $this->assertEquals(3, fwrite($stream, 'bar')); - fclose($stream); - - $stream = $this->getStream($fileName, 'r', 6); - $this->assertEquals('barbar', fread($stream, 100)); - fclose($stream); - - unlink($fileName); - } - - public function testRewind(): void { - $fileName = tempnam('/tmp', 'FOO'); - $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 6); - $this->assertEquals(6, fwrite($stream, 'foobar')); - $this->assertEquals(true, rewind($stream)); - $this->assertEquals('foobar', fread($stream, 100)); - $this->assertEquals(true, rewind($stream)); - $this->assertEquals(3, fwrite($stream, 'bar')); - fclose($stream); - - $stream = $this->getStream($fileName, 'r', 6); - $this->assertEquals('barbar', fread($stream, 100)); - fclose($stream); - - unlink($fileName); - } - - public function testSeek(): void { - $fileName = tempnam('/tmp', 'FOO'); - - $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, 9); - $this->assertEquals(6, fwrite($stream, 'foobar')); - $this->assertEquals(0, fseek($stream, 3)); - $this->assertEquals(6, fwrite($stream, 'foobar')); - fclose($stream); - - $stream = $this->getStream($fileName, 'r', 9); - $this->assertEquals('foofoobar', fread($stream, 100)); - $this->assertEquals(-1, fseek($stream, 10)); - $this->assertEquals(0, fseek($stream, 9)); - $this->assertEquals(-1, fseek($stream, -10, SEEK_CUR)); - $this->assertEquals(0, fseek($stream, -9, SEEK_CUR)); - $this->assertEquals(-1, fseek($stream, -10, SEEK_END)); - $this->assertEquals(0, fseek($stream, -9, SEEK_END)); - fclose($stream); - - unlink($fileName); - } - - public static function dataFilesProvider(): array { - return [ - ['lorem-big.txt'], - ['block-aligned.txt'], - ['block-aligned-plus-one.txt'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataFilesProvider')] - public function testWriteReadBigFile($testFile): void { - $expectedData = file_get_contents(\OC::$SERVERROOT . '/tests/data/' . $testFile); - // write it - $fileName = tempnam('/tmp', 'FOO'); - $stream = $this->getStream($fileName, 'w+', 0, self::DEFAULT_WRAPPER, strlen($expectedData)); - // while writing the file from the beginning to the end we should never try - // to read parts of the file. This should only happen for write operations - // in the middle of a file - $this->encryptionModule->expects($this->never())->method('decrypt'); - fwrite($stream, $expectedData); - fclose($stream); - - // read it all - $stream = $this->getStream($fileName, 'r', strlen($expectedData)); - $data = stream_get_contents($stream); - fclose($stream); - - $this->assertEquals($expectedData, $data); - - // another read test with a loop like we do in several places: - $stream = $this->getStream($fileName, 'r', strlen($expectedData)); - $data = ''; - while (!feof($stream)) { - $data .= fread($stream, 8192); - } - fclose($stream); - - $this->assertEquals($expectedData, $data); - - unlink($fileName); - } - - /** - * simulate a non-seekable storage - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataFilesProvider')] - public function testWriteToNonSeekableStorage($testFile): void { - $wrapper = $this->getMockBuilder(Encryption::class) - ->onlyMethods(['parentStreamSeek']) - ->getMock(); - $wrapper->expects($this->any()) - ->method('parentStreamSeek') - ->willReturn(false); - - $expectedData = file_get_contents(\OC::$SERVERROOT . '/tests/data/' . $testFile); - // write it - $fileName = tempnam('/tmp', 'FOO'); - $stream = $this->getStream($fileName, 'w+', 0, '\Test\Files\Stream\DummyEncryptionWrapper', strlen($expectedData)); - // while writing the file from the beginning to the end we should never try - // to read parts of the file. This should only happen for write operations - // in the middle of a file - $this->encryptionModule->expects($this->never())->method('decrypt'); - fwrite($stream, $expectedData); - fclose($stream); - - // read it all - $stream = $this->getStream($fileName, 'r', strlen($expectedData), '\Test\Files\Stream\DummyEncryptionWrapper', strlen($expectedData)); - $data = stream_get_contents($stream); - fclose($stream); - - $this->assertEquals($expectedData, $data); - - // another read test with a loop like we do in several places: - $stream = $this->getStream($fileName, 'r', strlen($expectedData)); - $data = ''; - while (!feof($stream)) { - $data .= fread($stream, 8192); - } - fclose($stream); - - $this->assertEquals($expectedData, $data); - - unlink($fileName); - } - - protected function buildMockModule(): IEncryptionModule&MockObject { - $encryptionModule = $this->getMockBuilder(IEncryptionModule::class) - ->disableOriginalConstructor() - ->onlyMethods(['getId', 'getDisplayName', 'begin', 'end', 'encrypt', 'decrypt', 'update', 'shouldEncrypt', 'getUnencryptedBlockSize', 'isReadable', 'encryptAll', 'prepareDecryptAll', 'isReadyForUser', 'needDetailedAccessList']) - ->getMock(); - - $encryptionModule->expects($this->any())->method('getId')->willReturn('UNIT_TEST_MODULE'); - $encryptionModule->expects($this->any())->method('getDisplayName')->willReturn('Unit test module'); - $encryptionModule->expects($this->any())->method('begin')->willReturn([]); - $encryptionModule->expects($this->any())->method('end')->willReturn(''); - $encryptionModule->expects($this->any())->method('isReadable')->willReturn(true); - $encryptionModule->expects($this->any())->method('needDetailedAccessList')->willReturn(false); - $encryptionModule->expects($this->any())->method('encrypt')->willReturnCallback(function ($data) { - // simulate different block size by adding some padding to the data - if (isset($data[6125])) { - return str_pad($data, 8192, 'X'); - } - // last block - return $data; - }); - $encryptionModule->expects($this->any())->method('decrypt')->willReturnCallback(function ($data) { - if (isset($data[8191])) { - return substr($data, 0, 6126); - } - // last block - return $data; - }); - $encryptionModule->expects($this->any())->method('update')->willReturn(true); - $encryptionModule->expects($this->any())->method('shouldEncrypt')->willReturn(true); - $encryptionModule->expects($this->any())->method('getUnencryptedBlockSize')->willReturn(6126); - return $encryptionModule; - } -} --- a/tests/lib/Files/Stream/HashWrapperTest.php +++ /dev/null @@ -1,40 +0,0 @@ -assertEquals($hash, $result); - }); - stream_get_contents($wrapper); - } - - public static function hashProvider(): array { - return [ - ['foo', 'md5', 'acbd18db4cc2f85cedef654fccc4a4d8'], - ['foo', 'sha1', '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'], - ['foo', 'sha256', '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'], - [str_repeat('foo', 8192), 'md5', '96684d2b796a2c54a026b5d60f9de819'], - ]; - } -} --- a/tests/lib/Files/Stream/QuotaTest.php +++ /dev/null @@ -1,153 +0,0 @@ -getStream('w+', 100); - $this->assertEquals(6, fwrite($stream, 'foobar')); - rewind($stream); - $this->assertEquals('foobar', fread($stream, 100)); - } - - public function testWriteNotEnoughSpace(): void { - $stream = $this->getStream('w+', 3); - $this->assertEquals(3, fwrite($stream, 'foobar')); - rewind($stream); - $this->assertEquals('foo', fread($stream, 100)); - } - - public function testWriteNotEnoughSpaceSecondTime(): void { - $stream = $this->getStream('w+', 9); - $this->assertEquals(6, fwrite($stream, 'foobar')); - $this->assertEquals(3, fwrite($stream, 'qwerty')); - rewind($stream); - $this->assertEquals('foobarqwe', fread($stream, 100)); - } - - public function testWriteEnoughSpaceRewind(): void { - $stream = $this->getStream('w+', 6); - $this->assertEquals(6, fwrite($stream, 'foobar')); - rewind($stream); - $this->assertEquals(3, fwrite($stream, 'qwe')); - rewind($stream); - $this->assertEquals('qwebar', fread($stream, 100)); - } - - public function testWriteNotEnoughSpaceRead(): void { - $stream = $this->getStream('w+', 6); - $this->assertEquals(6, fwrite($stream, 'foobar')); - rewind($stream); - $this->assertEquals('foobar', fread($stream, 6)); - $this->assertEquals(0, fwrite($stream, 'qwe')); - } - - public function testWriteNotEnoughSpaceExistingStream(): void { - $source = fopen('php://temp', 'w+'); - fwrite($source, 'foobar'); - $stream = Quota::wrap($source, 3); - $this->assertEquals(3, fwrite($stream, 'foobar')); - rewind($stream); - $this->assertEquals('foobarfoo', fread($stream, 100)); - } - - public function testWriteNotEnoughSpaceExistingStreamRewind(): void { - $source = fopen('php://temp', 'w+'); - fwrite($source, 'foobar'); - $stream = Quota::wrap($source, 3); - rewind($stream); - $this->assertEquals(6, fwrite($stream, 'qwerty')); - rewind($stream); - $this->assertEquals('qwerty', fread($stream, 100)); - } - - public function testFseekReturnsSuccess(): void { - $stream = $this->getStream('w+', 100); - fwrite($stream, '0123456789'); - $this->assertEquals(0, fseek($stream, 3, SEEK_SET)); - $this->assertEquals(0, fseek($stream, -1, SEEK_CUR)); - $this->assertEquals(0, fseek($stream, -4, SEEK_END)); - } - - public function testWriteAfterSeekEndWithEnoughSpace(): void { - $stream = $this->getStream('w+', 100); - fwrite($stream, '0123456789'); - fseek($stream, -3, SEEK_END); - $this->assertEquals(11, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdefghijk', fread($stream, 100)); - } - - public function testWriteAfterSeekEndWithNotEnoughSpace(): void { - $stream = $this->getStream('w+', 13); - fwrite($stream, '0123456789'); - // seek forward first to potentially week out - // potential limit calculation errors - fseek($stream, 4, SEEK_SET); - // seek to the end - fseek($stream, -3, SEEK_END); - $this->assertEquals(6, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdef', fread($stream, 100)); - } - - public function testWriteAfterSeekSetWithEnoughSpace(): void { - $stream = $this->getStream('w+', 100); - fwrite($stream, '0123456789'); - fseek($stream, 7, SEEK_SET); - $this->assertEquals(11, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdefghijk', fread($stream, 100)); - } - - public function testWriteAfterSeekSetWithNotEnoughSpace(): void { - $stream = $this->getStream('w+', 13); - fwrite($stream, '0123456789'); - fseek($stream, 7, SEEK_SET); - $this->assertEquals(6, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdef', fread($stream, 100)); - } - - public function testWriteAfterSeekCurWithEnoughSpace(): void { - $stream = $this->getStream('w+', 100); - fwrite($stream, '0123456789'); - rewind($stream); - fseek($stream, 3, SEEK_CUR); - fseek($stream, 5, SEEK_CUR); - fseek($stream, -1, SEEK_CUR); - $this->assertEquals(11, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdefghijk', fread($stream, 100)); - } - - public function testWriteAfterSeekCurWithNotEnoughSpace(): void { - $stream = $this->getStream('w+', 13); - fwrite($stream, '0123456789'); - rewind($stream); - fseek($stream, 3, SEEK_CUR); - fseek($stream, 5, SEEK_CUR); - fseek($stream, -1, SEEK_CUR); - $this->assertEquals(6, fwrite($stream, 'abcdefghijk')); - rewind($stream); - $this->assertEquals('0123456abcdef', fread($stream, 100)); - } -} --- a/tests/lib/Files/Template/TemplateManagerTest.php +++ /dev/null @@ -1,114 +0,0 @@ -createMock(IL10N::class); - $l10n->method('t') - ->willReturnCallback(fn ($string, $params) => sprintf($string, ...$params)); - $l10nFactory = $this->createMock(IFactory::class); - $l10nFactory->method('get') - ->willReturn($l10n); - $database = $this->createMock(IDBConnection::class); - $database->method('supports4ByteText')->willReturn(true); - $config = $this->createMock(IConfig::class); - $logger = new NullLogger(); - - $filenameValidator = new FilenameValidator( - $l10nFactory, - $database, - $config, - $logger, - ); - - $serverContainer = $this->createMock(ContainerInterface::class); - $eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->bootstrapCoordinator = $this->createMock(Coordinator::class); - $this->bootstrapCoordinator->method('getRegistrationContext') - ->willReturn(new RegistrationContext($logger)); - $this->rootFolder = $this->createMock(IRootFolder::class); - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user1'); - $userSession = $this->createMock(IUserSession::class); - $userSession->method('getUser') - ->willReturn($user); - $userManager = $this->createMock(IUserManager::class); - $previewManager = $this->createMock(IPreview::class); - - $this->templateManager = new TemplateManager( - $serverContainer, - $eventDispatcher, - $this->bootstrapCoordinator, - $this->rootFolder, - $userSession, - $userManager, - $previewManager, - $config, - $l10nFactory, - $logger, - $filenameValidator, - \OC::$SERVERROOT, - ); - } - - public function testCreateFromTemplateShoudValidateFilename(): void { - $this->expectException(GenericFileException::class); - - $fileDirectory = '/'; - $filePath = $fileDirectory . str_repeat('a', 251); - - $userFolder = $this->createMock(Folder::class); - $userFolder->method('get') - ->willReturnCallback(function ($path) use ($filePath, $fileDirectory) { - if ($path === $filePath) { - throw new NotFoundException(); - } - return $this->createMock(Folder::class); - }); - $userFolder->method('nodeExists') - ->willReturnCallback(function ($path) use ($filePath, $fileDirectory): bool { - return $path === $fileDirectory; - }); - $this->rootFolder->method('getUserFolder') - ->willReturn($userFolder); - - $this->templateManager->createFromTemplate($filePath); - } -} --- a/tests/lib/Files/Type/DetectionTest.php +++ /dev/null @@ -1,400 +0,0 @@ -detection = new Detection( - Server::get(IURLGenerator::class), - Server::get(LoggerInterface::class), - \OC::$SERVERROOT . '/config/', - \OC::$SERVERROOT . '/resources/config/' - ); - } - - public static function dataDetectPath(): array { - return [ - ['foo.txt', 'text/plain'], - ['foo.png', 'image/png'], - ['foo.bar.png', 'image/png'], - ['.hidden.png', 'image/png'], - ['.hidden.foo.png', 'image/png'], - ['.hidden/foo.png', 'image/png'], - ['.hidden/.hidden.png', 'image/png'], - ['test.jpg/foo.png', 'image/png'], - ['.png', 'application/octet-stream'], - ['..hidden', 'application/octet-stream'], - ['foo', 'application/octet-stream'], - ['', 'application/octet-stream'], - ['foo.png.ocTransferId123456789.part', 'image/png'], - ['foo.png.v1234567890', 'image/png'], - ]; - } - - /** - * - * @param string $path - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDetectPath')] - public function testDetectPath(string $path, string $expected): void { - $this->assertEquals($expected, $this->detection->detectPath($path)); - } - - public static function dataDetectContent(): array { - return [ - ['/', 'httpd/unix-directory'], - ['/data.tar.gz', 'application/gzip'], - ['/data.zip', 'application/zip'], - ['/testimage.mp3', 'audio/mpeg'], - ['/testimage.png', 'image/png'], - ]; - } - - /** - * - * @param string $path - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDetectContent')] - public function testDetectContent(string $path, string $expected): void { - $this->assertEquals($expected, $this->detection->detectContent(\OC::$SERVERROOT . '/tests/data' . $path)); - } - - public static function dataDetect(): array { - return [ - ['/', 'httpd/unix-directory'], - ['/data.tar.gz', 'application/gzip'], - ['/data.zip', 'application/zip'], - ['/testimagelarge.svg', 'image/svg+xml'], - ['/testimage.png', 'image/png'], - ]; - } - - /** - * - * @param string $path - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDetect')] - public function testDetect(string $path, string $expected): void { - $this->assertEquals($expected, $this->detection->detect(\OC::$SERVERROOT . '/tests/data' . $path)); - } - - public function testDetectString(): void { - $result = $this->detection->detectString('/data/data.tar.gz'); - $expected = 'text/plain'; - $this->assertEquals($expected, $result); - } - - public static function dataMimeTypeCustom(): array { - return [ - ['123', 'foobar/123'], - ['a123', 'foobar/123'], - ['bar', 'foobar/bar'], - ]; - } - - /** - * - * @param string $ext - * @param string $mime - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataMimeTypeCustom')] - public function testDetectMimeTypeCustom(string $ext, string $mime): void { - $tmpDir = sys_get_temp_dir() . '/nc-detect-' . uniqid('', true); - mkdir($tmpDir, 0700); - - try { - // Create a default / shipped mapping file (dist) - file_put_contents($tmpDir . '/mimetypemapping.dist.json', json_encode([])); - - // Create new custom mapping file that should be picked up by Detection - file_put_contents($tmpDir . '/mimetypemapping.json', json_encode([$ext => [$mime]])); - - /** @var IURLGenerator $urlGenerator */ - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var LoggerInterface $logger */ - $logger = $this->createMock(LoggerInterface::class); - - $detection = new Detection($urlGenerator, $logger, $tmpDir, $tmpDir); - $mappings = $detection->getAllMappings(); - - $this->assertArrayHasKey($ext, $mappings); - $this->assertEquals($mime, $detection->detectPath('foo.' . $ext)); - } finally { - // cleanup - @unlink($tmpDir . '/mimetypemapping.json'); - @unlink($tmpDir . '/mimetypemapping.dist.json'); - @rmdir($tmpDir); - } - } - - public function testDetectMimeTypePreservesLeadingZeroKeys(): void { - $tmpDir = sys_get_temp_dir() . '/nc-detect-' . uniqid(); - mkdir($tmpDir, 0700); - try { - // Create a default / shipped mapping file (dist) - file_put_contents($tmpDir . '/mimetypemapping.dist.json', json_encode([])); - - // Create new custom mapping file with potentially problematic keys - $mappings = [ - '001' => ['application/x-zeroone', null], - '1' => ['application/x-one', null], - ]; - file_put_contents($tmpDir . '/mimetypemapping.json', json_encode($mappings)); - - /** @var IURLGenerator $urlGenerator */ - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var LoggerInterface $logger */ - $logger = $this->createMock(LoggerInterface::class); - - $detection = new Detection($urlGenerator, $logger, $tmpDir, $tmpDir); - $mappings = $detection->getAllMappings(); - - $this->assertArrayHasKey('001', $mappings, 'Expected mapping to contain key "001"'); - $this->assertArrayHasKey('1', $mappings, 'Expected mapping to contain key "1"'); - - $this->assertEquals('application/x-zeroone', $detection->detectPath('foo.001')); - $this->assertEquals('application/x-one', $detection->detectPath('foo.1')); - } finally { - @unlink($tmpDir . '/mimetypemapping.json'); - @unlink($tmpDir . '/mimetypemapping.dist.json'); - @rmdir($tmpDir); - } - } - - public static function dataGetSecureMimeType(): array { - return [ - ['image/svg+xml', 'text/plain'], - ['image/png', 'image/png'], - ]; - } - - /** - * - * @param string $mimeType - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetSecureMimeType')] - public function testGetSecureMimeType(string $mimeType, string $expected): void { - $this->assertEquals($expected, $this->detection->getSecureMimeType($mimeType)); - } - - public function testMimeTypeIcon(): void { - if (!class_exists('org\\bovigo\\vfs\\vfsStream')) { - $this->markTestSkipped('Package vfsStream not installed'); - } - $confDir = \org\bovigo\vfs\vfsStream::setup(); - $mimetypealiases_dist = \org\bovigo\vfs\vfsStream::newFile('mimetypealiases.dist.json')->at($confDir); - - //Empty alias file - $mimetypealiases_dist->setContent(json_encode([], JSON_FORCE_OBJECT)); - - /* - * Test dir mimetype - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var LoggerInterface $logger */ - $logger = $this->createMock(LoggerInterface::class); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/folder.png')) - ->willReturn('folder.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('dir'); - $this->assertEquals('folder.svg', $mimeType); - - /* - * Test dir-shareed mimetype - */ - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/folder-shared.png')) - ->willReturn('folder-shared.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('dir-shared'); - $this->assertEquals('folder-shared.svg', $mimeType); - - /* - * Test dir external - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/folder-external.png')) - ->willReturn('folder-external.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('dir-external'); - $this->assertEquals('folder-external.svg', $mimeType); - - /* - * Test complete mimetype - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/my-type.png')) - ->willReturn('my-type.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('my-type'); - $this->assertEquals('my-type.svg', $mimeType); - - /* - * Test subtype - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $calls = [ - ['core', 'filetypes/my-type.png'], - ['core', 'filetypes/my.png'], - ]; - $urlGenerator->expects($this->exactly(2)) - ->method('imagePath') - ->willReturnCallback( - function ($appName, $file) use (&$calls) { - $expected = array_shift($calls); - $this->assertEquals($expected, [$appName, $file]); - if ($file === 'filetypes/my.png') { - return 'my.svg'; - } - throw new \RuntimeException(); - } - ); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('my-type'); - $this->assertEquals('my.svg', $mimeType); - - /* - * Test default mimetype - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $calls = [ - ['core', 'filetypes/foo-bar.png'], - ['core', 'filetypes/foo.png'], - ['core', 'filetypes/file.png'], - ]; - $urlGenerator->expects($this->exactly(3)) - ->method('imagePath') - ->willReturnCallback( - function ($appName, $file) use (&$calls) { - $expected = array_shift($calls); - $this->assertEquals($expected, [$appName, $file]); - if ($file === 'filetypes/file.png') { - return 'file.svg'; - } - throw new \RuntimeException(); - } - ); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('foo-bar'); - $this->assertEquals('file.svg', $mimeType); - - /* - * Test chaching - */ - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/foo-bar.png')) - ->willReturn('foo-bar.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('foo-bar'); - $this->assertEquals('foo-bar.svg', $mimeType); - $mimeType = $detection->mimeTypeIcon('foo-bar'); - $this->assertEquals('foo-bar.svg', $mimeType); - - /* - * Test aliases - */ - - //Put alias - $mimetypealiases_dist->setContent(json_encode(['foo' => 'foobar/baz'], JSON_FORCE_OBJECT)); - - //Mock UrlGenerator - $urlGenerator = $this->getMockBuilder(IURLGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - - //Only call the url generator once - $urlGenerator->expects($this->once()) - ->method('imagePath') - ->with($this->equalTo('core'), $this->equalTo('filetypes/foobar-baz.png')) - ->willReturn('foobar-baz.svg'); - - $detection = new Detection($urlGenerator, $logger, $confDir->url(), $confDir->url()); - $mimeType = $detection->mimeTypeIcon('foo'); - $this->assertEquals('foobar-baz.svg', $mimeType); - } -} --- a/tests/lib/Files/Type/LoaderTest.php +++ /dev/null @@ -1,87 +0,0 @@ -db = Server::get(IDBConnection::class); - $this->loader = new Loader($this->db); - } - - #[\Override] - protected function tearDown(): void { - $deleteMimetypes = $this->db->getQueryBuilder(); - $deleteMimetypes->delete('mimetypes') - ->where($deleteMimetypes->expr()->like( - 'mimetype', $deleteMimetypes->createPositionalParameter('testing/%') - )); - $deleteMimetypes->executeStatement(); - parent::tearDown(); - } - - public function testGetMimetype(): void { - $qb = $this->db->getQueryBuilder(); - $qb->insert('mimetypes') - ->values([ - 'mimetype' => $qb->createPositionalParameter('testing/mymimetype') - ]); - $qb->executeStatement(); - - $this->assertTrue($this->loader->exists('testing/mymimetype')); - $mimetypeId = $this->loader->getId('testing/mymimetype'); - $this->assertNotNull($mimetypeId); - - $mimetype = $this->loader->getMimetypeById($mimetypeId); - $this->assertEquals('testing/mymimetype', $mimetype); - } - - public function testGetNonexistentMimetype(): void { - $this->assertFalse($this->loader->exists('testing/nonexistent')); - // hopefully this ID doesn't exist - $this->assertNull($this->loader->getMimetypeById(12345)); - } - - public function testStore(): void { - $this->assertFalse($this->loader->exists('testing/mymimetype')); - $mimetypeId = $this->loader->getId('testing/mymimetype'); - - $qb = $this->db->getQueryBuilder(); - $qb->select('mimetype') - ->from('mimetypes') - ->where($qb->expr()->eq('id', $qb->createPositionalParameter($mimetypeId))); - - $result = $qb->executeQuery(); - $mimetype = $result->fetchAssociative(); - $result->closeCursor(); - $this->assertEquals('testing/mymimetype', $mimetype['mimetype']); - - $this->assertEquals('testing/mymimetype', $this->loader->getMimetypeById($mimetypeId)); - $this->assertEquals($mimetypeId, $this->loader->getId('testing/mymimetype')); - } - - public function testStoreExists(): void { - $mimetypeId = $this->loader->getId('testing/mymimetype'); - $mimetypeId2 = $this->loader->getId('testing/mymimetype'); - - $this->assertEquals($mimetypeId, $mimetypeId2); - } -} --- a/tests/lib/Files/Utils/ScannerTest.php +++ /dev/null @@ -1,247 +0,0 @@ - $mounts */ - private array $mounts = []; - - public function addMount(MountPoint $mount): void { - $this->mounts[$mount->getMountPoint()] = $mount; - } - - #[\Override] - protected function getMounts(string $dir): array { - return $this->mounts; - } -} - -#[Group('DB')] -class ScannerTest extends TestCase { - private Dummy $userBackend; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->userBackend = new Dummy(); - Server::get(IUserManager::class)->registerBackend($this->userBackend); - $this->loginAsUser(); - } - - #[\Override] - protected function tearDown(): void { - $this->logout(); - Server::get(IUserManager::class)->removeBackend($this->userBackend); - parent::tearDown(); - } - - public function testReuseExistingRoot(): void { - $storage = new Temporary([]); - $mount = new MountPoint($storage, ''); - Filesystem::getMountManager()->addMount($mount); - $cache = $storage->getCache(); - - $storage->mkdir('folder'); - $storage->file_put_contents('foo.txt', 'qwerty'); - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - - $scanner = new TestScanner( - Server::get(IUserManager::class)->get(''), - Server::get(IDBConnection::class), - $this->createMock(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->addMount($mount); - - $scanner->scan(''); - $this->assertTrue($cache->inCache('folder/bar.txt')); - $oldRoot = $cache->get(''); - - $scanner->scan(''); - $newRoot = $cache->get(''); - $this->assertEquals($oldRoot, $newRoot); - } - - public function testReuseExistingFile(): void { - $storage = new Temporary([]); - $mount = new MountPoint($storage, ''); - Filesystem::getMountManager()->addMount($mount); - $cache = $storage->getCache(); - - $storage->mkdir('folder'); - $storage->file_put_contents('foo.txt', 'qwerty'); - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - - $scanner = new TestScanner( - Server::get(IUserManager::class)->get(''), - Server::get(IDBConnection::class), - $this->createMock(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->addMount($mount); - - $scanner->scan(''); - $this->assertTrue($cache->inCache('folder/bar.txt')); - $old = $cache->get('folder/bar.txt'); - - $scanner->scan(''); - $new = $cache->get('folder/bar.txt'); - $this->assertEquals($old, $new); - } - - public function testScanSubMount(): void { - $uid = $this->getUniqueID(); - $this->userBackend->createUser($uid, 'test'); - - $mountProvider = $this->createMock(IMountProvider::class); - - $storage = new Temporary([]); - $mount = new MountPoint($storage, '/' . $uid . '/files/foo'); - - $mountProvider->expects($this->any()) - ->method('getMountsForUser') - ->willReturnCallback(function (IUser $user, IStorageFactory $storageFactory) use ($mount, $uid) { - if ($user->getUID() === $uid) { - return [$mount]; - } else { - return []; - } - }); - - Server::get(IMountProviderCollection::class)->registerProvider($mountProvider); - $cache = $storage->getCache(); - - $storage->mkdir('folder'); - $storage->file_put_contents('foo.txt', 'qwerty'); - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - - $scanner = new Scanner( - Server::get(IUserManager::class)->get($uid), - Server::get(IDBConnection::class), - Server::get(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - - $this->assertFalse($cache->inCache('folder/bar.txt')); - $scanner->scan('/' . $uid . '/files/foo'); - $this->assertTrue($cache->inCache('folder/bar.txt')); - } - - public static function invalidPathProvider(): \Generator { - yield [ '../' ]; - yield [ '..\\' ]; - yield [ '../..\\../' ]; - } - - #[DataProvider(methodName: 'invalidPathProvider')] - public function testInvalidPathScanning(string $invalidPath): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid path to scan'); - - $scanner = new TestScanner( - Server::get(IUserManager::class)->get(''), - Server::get(IDBConnection::class), - $this->createMock(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->scan($invalidPath); - } - - public function testPropagateEtag(): void { - $storage = new Temporary([]); - $mount = new MountPoint($storage, ''); - Filesystem::getMountManager()->addMount($mount); - $cache = $storage->getCache(); - - $storage->mkdir('folder'); - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - $storage->touch('folder/bar.txt', time() - 200); - - $scanner = new TestScanner( - Server::get(IUserManager::class)->get(''), - Server::get(IDBConnection::class), - $this->createMock(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->addMount($mount); - - $scanner->scan(''); - $this->assertTrue($cache->inCache('folder/bar.txt')); - $oldRoot = $cache->get(''); - - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - $scanner->scan(''); - $newRoot = $cache->get(''); - - $this->assertNotEquals($oldRoot->getEtag(), $newRoot->getEtag()); - } - - public function testShallow(): void { - $storage = new Temporary([]); - $mount = new MountPoint($storage, ''); - Filesystem::getMountManager()->addMount($mount); - $cache = $storage->getCache(); - - $storage->mkdir('folder'); - $storage->mkdir('folder/subfolder'); - $storage->file_put_contents('foo.txt', 'qwerty'); - $storage->file_put_contents('folder/bar.txt', 'qwerty'); - $storage->file_put_contents('folder/subfolder/foobar.txt', 'qwerty'); - - $scanner = new TestScanner( - Server::get(IUserManager::class)->get(''), - Server::get(IDBConnection::class), - $this->createMock(IEventDispatcher::class), - Server::get(LoggerInterface::class), - Server::get(SetupManager::class), - ); - $scanner->addMount($mount); - - $scanner->scan('', false); - $this->assertTrue($cache->inCache('folder')); - $this->assertFalse($cache->inCache('folder/subfolder')); - $this->assertTrue($cache->inCache('foo.txt')); - $this->assertFalse($cache->inCache('folder/bar.txt')); - $this->assertFalse($cache->inCache('folder/subfolder/foobar.txt')); - - $scanner->scan('folder', false); - $this->assertTrue($cache->inCache('folder')); - $this->assertTrue($cache->inCache('folder/subfolder')); - $this->assertTrue($cache->inCache('foo.txt')); - $this->assertTrue($cache->inCache('folder/bar.txt')); - $this->assertFalse($cache->inCache('folder/subfolder/foobar.txt')); - } -} --- a/tests/lib/Files/ViewTest.php +++ /dev/null @@ -1,2939 +0,0 @@ -clearBackends(); - Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy()); - - $userManager = Server::get(IUserManager::class); - $groupManager = Server::get(IGroupManager::class); - - self::$userObject = $userManager->createUser(self::$user, self::$user); - self::$groupObject = $groupManager->createGroup('group1'); - self::$groupObject->addUser(self::$userObject); - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - \OC_Hook::clear(); - - self::loginAsUser(self::$user); - - $manager = Server::get(IMountManager::class); - $manager->removeMount('/test'); - - $this->tempStorage = null; - } - - #[\Override] - protected function tearDown(): void { - \OC_User::setUserId(self::$user); - foreach ($this->storages as $storage) { - $cache = $storage->getCache(); - $ids = $cache->getAll(); - $cache->clear(); - } - - if ($this->tempStorage) { - system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir())); - } - - self::logout(); - - /** @var SetupManager $setupManager */ - $setupManager = Server::get(SetupManager::class); - $setupManager->setupRoot(); - - $mountProviderCollection = Server::get(IMountProviderCollection::class); - self::invokePrivate($mountProviderCollection, 'providers', [[]]); - - parent::tearDown(); - } - - #[Override] - public static function tearDownAfterClass(): void { - self::$userObject->delete(); - self::$groupObject->delete(); - } - - public function testCacheAPI(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $storage3 = $this->getTestStorage(); - $root = self::getUniqueID('/'); - Filesystem::mount($storage1, [], $root . '/'); - Filesystem::mount($storage2, [], $root . '/substorage'); - Filesystem::mount($storage3, [], $root . '/folder/anotherstorage'); - $textSize = strlen("dummy file data\n"); - $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $storageSize = $textSize * 2 + $imageSize; - - $storageInfo = $storage3->getCache()->get(''); - $this->assertEquals($storageSize, $storageInfo['size']); - - $rootView = new View($root); - - $cachedData = $rootView->getFileInfo('/foo.txt'); - $this->assertEquals($textSize, $cachedData['size']); - $this->assertEquals('text/plain', $cachedData['mimetype']); - $this->assertNotEquals(-1, $cachedData['permissions']); - - $cachedData = $rootView->getFileInfo('/'); - $this->assertEquals($storageSize * 3, $cachedData['size']); - $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']); - - // get cached data excluding mount points - $cachedData = $rootView->getFileInfo('/', false); - $this->assertEquals($storageSize, $cachedData['size']); - $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']); - - $cachedData = $rootView->getFileInfo('/folder'); - $this->assertEquals($storageSize + $textSize, $cachedData['size']); - $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']); - - $folderData = $rootView->getDirectoryContent('/'); - usort($folderData, fn (FileInfo $a, FileInfo $b) => $a->getName() <=> $b->getName()); - /** - * expected entries: - * folder - * foo.png - * foo.txt - * substorage - */ - $this->assertCount(4, $folderData); - $this->assertEquals('folder', $folderData[0]['name']); - $this->assertEquals('foo.png', $folderData[1]['name']); - $this->assertEquals('foo.txt', $folderData[2]['name']); - $this->assertEquals('substorage', $folderData[3]['name']); - - $this->assertEquals($storageSize + $textSize, $folderData[0]['size']); - $this->assertEquals($imageSize, $folderData[1]['size']); - $this->assertEquals($textSize, $folderData[2]['size']); - $this->assertEquals($storageSize, $folderData[3]['size']); - - $folderData = $rootView->getDirectoryContent('/substorage'); - usort($folderData, fn (FileInfo $a, FileInfo $b) => $a->getName() <=> $b->getName()); - /** - * expected entries: - * folder - * foo.png - * foo.txt - */ - $this->assertCount(3, $folderData); - $this->assertEquals('folder', $folderData[0]['name']); - $this->assertEquals('foo.png', $folderData[1]['name']); - $this->assertEquals('foo.txt', $folderData[2]['name']); - - $folderView = new View($root . '/folder'); - $this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/')); - - $cachedData = $rootView->getFileInfo('/foo.txt'); - $this->assertFalse($cachedData['encrypted']); - $id = $rootView->putFileInfo('/foo.txt', ['encrypted' => true]); - $cachedData = $rootView->getFileInfo('/foo.txt'); - $this->assertTrue($cachedData['encrypted']); - $this->assertEquals($cachedData['fileid'], $id); - - $this->assertFalse($rootView->getFileInfo('/non/existing')); - $this->assertEquals([], $rootView->getDirectoryContent('/non/existing')); - } - - public function testGetPath(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('test'); - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $storage3 = $this->getTestStorage(); - - Filesystem::mount($storage1, [], '/test/files'); - Filesystem::mount($storage2, [], '/test/files/substorage'); - Filesystem::mount($storage3, [], '/test/files/folder/anotherstorage'); - - $userMountCache = Server::get(IUserMountCache::class); - $userMountCache->registerMounts($user, [ - new MountPoint($storage1, '/test/files'), - new MountPoint($storage2, '/test/files/substorage'), - new MountPoint($storage3, '/test/files/folder/anotherstorage'), - ]); - - $rootView = new View('/test/files'); - - $cachedData = $rootView->getFileInfo('/foo.txt'); - $id1 = $cachedData->getId(); - $this->assertEquals('/foo.txt', $rootView->getPath($id1)); - - $cachedData = $rootView->getFileInfo('/substorage/foo.txt'); - $id2 = $cachedData->getId(); - $this->assertEquals('/substorage/foo.txt', $rootView->getPath($id2)); - - $folderView = new View('/test/files/substorage'); - $this->assertEquals('/foo.txt', $folderView->getPath($id2)); - } - - public function testGetPathNotExisting(): void { - $this->expectException(NotFoundException::class); - - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - - $rootView = new View(''); - $cachedData = $rootView->getFileInfo('/foo.txt'); - /** @var int $id1 */ - $id1 = $cachedData['fileid']; - $folderView = new View('/substorage'); - $this->assertNull($folderView->getPath($id1)); - } - - public function testMountPointOverwrite(): void { - $storage1 = $this->getTestStorage(false); - $storage2 = $this->getTestStorage(); - $storage1->mkdir('substorage'); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - - $rootView = new View(''); - $folderContent = $rootView->getDirectoryContent('/'); - $this->assertCount(4, $folderContent); - } - - public static function sharingDisabledPermissionProvider(): array { - return [ - ['no', '', true], - ['yes', 'group1', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('sharingDisabledPermissionProvider')] - public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable): void { - // Reset sharing disabled for users cache - self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]); - - $config = Server::get(IConfig::class); - $oldExcludeGroupsFlag = $config->getAppValue('core', 'shareapi_exclude_groups', 'no'); - $oldExcludeGroupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', ''); - $config->setAppValue('core', 'shareapi_exclude_groups', $excludeGroups); - $config->setAppValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList); - - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/mount'); - - $view = new View('/'); - - $folderContent = $view->getDirectoryContent(''); - $this->assertEquals($expectedShareable, $folderContent[0]->isShareable()); - - $folderContent = $view->getDirectoryContent('mount'); - $this->assertEquals($expectedShareable, $folderContent[0]->isShareable()); - - $config->setAppValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag); - $config->setAppValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList); - - // Reset sharing disabled for users cache - self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]); - } - - public function testCacheIncompleteFolder(): void { - $storage1 = $this->getTestStorage(false); - Filesystem::mount($storage1, [], '/incomplete'); - $rootView = new View('/incomplete'); - - $entries = $rootView->getDirectoryContent('/'); - $this->assertCount(3, $entries); - - // /folder will already be in the cache but not scanned - $entries = $rootView->getDirectoryContent('/folder'); - $this->assertCount(1, $entries); - } - - public function testAutoScan(): void { - $storage1 = $this->getTestStorage(false); - $storage2 = $this->getTestStorage(false); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - $textSize = strlen("dummy file data\n"); - - $rootView = new View(''); - - $cachedData = $rootView->getFileInfo('/'); - $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']); - $this->assertEquals(-1, $cachedData['size']); - - $folderData = $rootView->getDirectoryContent('/substorage/folder'); - $this->assertEquals('text/plain', $folderData[0]['mimetype']); - $this->assertEquals($textSize, $folderData[0]['size']); - } - - public function testSearch(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $storage3 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - Filesystem::mount($storage3, [], '/folder/anotherstorage'); - - $rootView = new View(''); - - $results = $rootView->search('foo'); - $this->assertCount(6, $results); - $paths = []; - foreach ($results as $result) { - $this->assertEquals($result['path'], Filesystem::normalizePath($result['path'])); - $paths[] = $result['path']; - } - $this->assertContains('/foo.txt', $paths); - $this->assertContains('/foo.png', $paths); - $this->assertContains('/substorage/foo.txt', $paths); - $this->assertContains('/substorage/foo.png', $paths); - $this->assertContains('/folder/anotherstorage/foo.txt', $paths); - $this->assertContains('/folder/anotherstorage/foo.png', $paths); - - $folderView = new View('/folder'); - $results = $folderView->search('bar'); - $this->assertCount(2, $results); - $paths = []; - foreach ($results as $result) { - $paths[] = $result['path']; - } - $this->assertContains('/folder/anotherstorage/folder/bar.txt', $paths); - $this->assertContains('/folder/bar.txt', $paths); - - $results = $folderView->search('foo'); - $this->assertCount(2, $results); - $paths = []; - foreach ($results as $result) { - $paths[] = $result['path']; - } - $this->assertContains('/folder/anotherstorage/foo.txt', $paths); - $this->assertContains('/folder/anotherstorage/foo.png', $paths); - - $this->assertCount(6, $rootView->searchByMime('text')); - $this->assertCount(3, $folderView->searchByMime('text')); - } - - public function testWatcher(): void { - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - $storage1->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS); - - $rootView = new View(''); - - $cachedData = $rootView->getFileInfo('foo.txt'); - $this->assertEquals(16, $cachedData['size']); - - $rootView->putFileInfo('foo.txt', ['storage_mtime' => 10]); - $storage1->file_put_contents('foo.txt', 'foo'); - clearstatcache(); - - $cachedData = $rootView->getFileInfo('foo.txt'); - $this->assertEquals(3, $cachedData['size']); - } - - /** - * Regression test for View::getCacheEntry unconditionally calling propagateChange - * when the watcher reports needsUpdate = true, regardless of whether the scanner - * found any actual storage change. - * - * Backends like Amazon S3 always return hasUpdated() = true for directory paths - * because S3 has no real directory objects. Before the fix, every getFileInfo() - * call on a folder fired propagateChange(path, time()), stamping all ancestor - * folders in the filecache with the current request timestamp. - * - * After the fix, propagateChange is only called when watcher->update() actually - * changes the cached metadata for the path. - */ - public function testWatcherDoesNotPropagateWhenStorageMtimeUnchanged(): void { - $storage = $this->getTestStorage(true, TemporaryAlwaysUpdated::class); - Filesystem::mount($storage, [], '/'); - $storage->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS); - - $rootView = new View(''); - - // Use an mtime that cannot accidentally equal the current timestamp. This makes - // the test fail reliably if propagateChange() is called during this request. - $rootEntry = $storage->getCache()->get(''); - $storage->getCache()->update($rootEntry->getId(), [ - 'mtime' => 1, - 'etag' => 'unchanged-root-etag', - ]); - - // Access a subfolder. The watcher will fire (hasUpdated always returns true), - // but the scanner leaves the cached metadata for 'folder' unchanged. - // getCacheEntry must therefore NOT call propagateChange('folder', time()), - // which would update the root entry's mtime and etag. - $rootView->getFileInfo('folder'); - - // Read the root entry directly from the cache to avoid triggering another watcher cycle. - $rootEntryAfter = $storage->getCache()->get(''); - - $this->assertSame( - 1, - $rootEntryAfter->getMTime(), - 'Root folder mtime must not change when the watcher finds no metadata change', - ); - $this->assertSame( - 'unchanged-root-etag', - $rootEntryAfter->getEtag(), - 'Root folder etag must not change when the watcher finds no metadata change', - ); - } - - public function testCopyBetweenStorageNoCross(): void { - $storage1 = $this->getTestStorage(true, TemporaryNoCross::class); - $storage2 = $this->getTestStorage(true, TemporaryNoCross::class); - $this->copyBetweenStorages($storage1, $storage2); - } - - public function testCopyBetweenStorageCross(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $this->copyBetweenStorages($storage1, $storage2); - } - - public function testCopyBetweenStorageCrossNonLocal(): void { - $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class); - $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class); - $this->copyBetweenStorages($storage1, $storage2); - } - - public function copyBetweenStorages($storage1, $storage2) { - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - - $rootView = new View(''); - $rootView->mkdir('substorage/emptyfolder'); - $rootView->copy('substorage', 'anotherfolder'); - $this->assertTrue($rootView->is_dir('/anotherfolder')); - $this->assertTrue($rootView->is_dir('/substorage')); - $this->assertTrue($rootView->is_dir('/anotherfolder/emptyfolder')); - $this->assertTrue($rootView->is_dir('/substorage/emptyfolder')); - $this->assertTrue($rootView->file_exists('/anotherfolder/foo.txt')); - $this->assertTrue($rootView->file_exists('/anotherfolder/foo.png')); - $this->assertTrue($rootView->file_exists('/anotherfolder/folder/bar.txt')); - $this->assertTrue($rootView->file_exists('/substorage/foo.txt')); - $this->assertTrue($rootView->file_exists('/substorage/foo.png')); - $this->assertTrue($rootView->file_exists('/substorage/folder/bar.txt')); - } - - public function testMoveBetweenStorageNoCross(): void { - $storage1 = $this->getTestStorage(true, TemporaryNoCross::class); - $storage2 = $this->getTestStorage(true, TemporaryNoCross::class); - $this->moveBetweenStorages($storage1, $storage2); - } - - public function testMoveBetweenStorageCross(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $this->moveBetweenStorages($storage1, $storage2); - } - - public function testMoveBetweenStorageCrossNonLocal(): void { - $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class); - $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class); - $this->moveBetweenStorages($storage1, $storage2); - } - - public function moveBetweenStorages($storage1, $storage2) { - Filesystem::mount($storage1, [], '/' . self::$user . '/'); - Filesystem::mount($storage2, [], '/' . self::$user . '/substorage'); - - $rootView = new View('/' . self::$user); - $rootView->rename('foo.txt', 'substorage/folder/foo.txt'); - $this->assertFalse($rootView->file_exists('foo.txt')); - $this->assertTrue($rootView->file_exists('substorage/folder/foo.txt')); - $rootView->rename('substorage/folder', 'anotherfolder'); - $this->assertFalse($rootView->is_dir('substorage/folder')); - $this->assertTrue($rootView->file_exists('anotherfolder/foo.txt')); - $this->assertTrue($rootView->file_exists('anotherfolder/bar.txt')); - } - - public function testUnlink(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - - $rootView = new View(''); - $rootView->file_put_contents('/foo.txt', 'asd'); - $rootView->file_put_contents('/substorage/bar.txt', 'asd'); - - $this->assertTrue($rootView->file_exists('foo.txt')); - $this->assertTrue($rootView->file_exists('substorage/bar.txt')); - - $this->assertTrue($rootView->unlink('foo.txt')); - $this->assertTrue($rootView->unlink('substorage/bar.txt')); - - $this->assertFalse($rootView->file_exists('foo.txt')); - $this->assertFalse($rootView->file_exists('substorage/bar.txt')); - } - - public static function rmdirOrUnlinkDataProvider(): array { - return [['rmdir'], ['unlink']]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('rmdirOrUnlinkDataProvider')] - public function testRmdir($method): void { - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - - $rootView = new View(''); - $rootView->mkdir('sub'); - $rootView->mkdir('sub/deep'); - $rootView->file_put_contents('/sub/deep/foo.txt', 'asd'); - - $this->assertTrue($rootView->file_exists('sub/deep/foo.txt')); - - $this->assertTrue($rootView->$method('sub')); - - $this->assertFalse($rootView->file_exists('sub')); - } - - public function testUnlinkRootMustFail(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); - - $rootView = new View(''); - $rootView->file_put_contents('/foo.txt', 'asd'); - $rootView->file_put_contents('/substorage/bar.txt', 'asd'); - - $this->assertFalse($rootView->unlink('')); - $this->assertFalse($rootView->unlink('/')); - $this->assertFalse($rootView->unlink('substorage')); - $this->assertFalse($rootView->unlink('/substorage')); - } - - public function testTouch(): void { - $storage = $this->getTestStorage(true, TemporaryNoTouch::class); - - Filesystem::mount($storage, [], '/'); - - $rootView = new View(''); - $oldCachedData = $rootView->getFileInfo('foo.txt'); - - $rootView->touch('foo.txt', 500); - - $cachedData = $rootView->getFileInfo('foo.txt'); - $this->assertEquals(500, $cachedData['mtime']); - $this->assertEquals($oldCachedData['storage_mtime'], $cachedData['storage_mtime']); - - $rootView->putFileInfo('foo.txt', ['storage_mtime' => 1000]); //make sure the watcher detects the change - $rootView->file_put_contents('foo.txt', 'asd'); - $cachedData = $rootView->getFileInfo('foo.txt'); - $this->assertGreaterThanOrEqual($oldCachedData['mtime'], $cachedData['mtime']); - $this->assertEquals($cachedData['storage_mtime'], $cachedData['mtime']); - } - - public function testViewHooks(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $defaultRoot = Filesystem::getRoot(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], $defaultRoot . '/substorage'); - \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook'); - - $rootView = new View(''); - $subView = new View($defaultRoot . '/substorage'); - $this->hookPath = null; - - $rootView->file_put_contents('/foo.txt', 'asd'); - $this->assertNull($this->hookPath); - - $subView->file_put_contents('/foo.txt', 'asd'); - $this->assertEquals('/substorage/foo.txt', $this->hookPath); - } - - private $hookPath; - - public function dummyHook($params) { - $this->hookPath = $params['path']; - } - - public function testSearchNotOutsideView(): void { - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - $storage1->rename('folder', 'foo'); - $scanner = $storage1->getScanner(); - $scanner->scan(''); - - $view = new View('/foo'); - - $result = $view->search('.txt'); - $this->assertCount(1, $result); - } - - /** - * @param bool $scan - * @param string $class - * @return Storage - */ - private function getTestStorage($scan = true, $class = Temporary::class) { - /** - * @var Storage $storage - */ - $storage = new $class([]); - $textData = "dummy file data\n"; - $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png'); - $storage->mkdir('folder'); - $storage->file_put_contents('foo.txt', $textData); - $storage->file_put_contents('foo.png', $imgData); - $storage->file_put_contents('folder/bar.txt', $textData); - - if ($scan) { - $scanner = $storage->getScanner(); - $scanner->scan(''); - } - $this->storages[] = $storage; - return $storage; - } - - public function testViewHooksIfRootStartsTheSame(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $defaultRoot = Filesystem::getRoot(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], $defaultRoot . '_substorage'); - \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook'); - - $subView = new View($defaultRoot . '_substorage'); - $this->hookPath = null; - - $subView->file_put_contents('/foo.txt', 'asd'); - $this->assertNull($this->hookPath); - } - - private $hookWritePath; - private $hookCreatePath; - private $hookUpdatePath; - - public function dummyHookWrite($params) { - $this->hookWritePath = $params['path']; - } - - public function dummyHookUpdate($params) { - $this->hookUpdatePath = $params['path']; - } - - public function dummyHookCreate($params) { - $this->hookCreatePath = $params['path']; - } - - public function testEditNoCreateHook(): void { - $storage1 = $this->getTestStorage(); - $storage2 = $this->getTestStorage(); - $defaultRoot = Filesystem::getRoot(); - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], $defaultRoot); - \OC_Hook::connect('OC_Filesystem', 'post_create', $this, 'dummyHookCreate'); - \OC_Hook::connect('OC_Filesystem', 'post_update', $this, 'dummyHookUpdate'); - \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHookWrite'); - - $view = new View($defaultRoot); - $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null; - - $view->file_put_contents('/asd.txt', 'foo'); - $this->assertEquals('/asd.txt', $this->hookCreatePath); - $this->assertNull($this->hookUpdatePath); - $this->assertEquals('/asd.txt', $this->hookWritePath); - - $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null; - - $view->file_put_contents('/asd.txt', 'foo'); - $this->assertNull($this->hookCreatePath); - $this->assertEquals('/asd.txt', $this->hookUpdatePath); - $this->assertEquals('/asd.txt', $this->hookWritePath); - - \OC_Hook::clear('OC_Filesystem', 'post_create'); - \OC_Hook::clear('OC_Filesystem', 'post_update'); - \OC_Hook::clear('OC_Filesystem', 'post_write'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('resolvePathTestProvider')] - public function testResolvePath($expected, $pathToTest): void { - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - - $view = new View(''); - - $result = $view->resolvePath($pathToTest); - $this->assertEquals($expected, $result[1]); - - $exists = $view->file_exists($pathToTest); - $this->assertTrue($exists); - - $exists = $view->file_exists($result[1]); - $this->assertTrue($exists); - } - - public static function resolvePathTestProvider(): array { - return [ - ['foo.txt', 'foo.txt'], - ['foo.txt', '/foo.txt'], - ['folder', 'folder'], - ['folder', '/folder'], - ['folder', 'folder/'], - ['folder', '/folder/'], - ['folder/bar.txt', 'folder/bar.txt'], - ['folder/bar.txt', '/folder/bar.txt'], - ['', ''], - ['', '/'], - ]; - } - - public function testUTF8Names(): void { - $names = ['虚', '和知しゃ和で', 'regular ascii', 'sɨˈrɪlɪk', 'ѨѬ', 'أنا أحب القراءة كثيرا']; - - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - - $rootView = new View(''); - foreach ($names as $name) { - $rootView->file_put_contents('/' . $name, 'dummy content'); - } - - $list = $rootView->getDirectoryContent('/'); - - $this->assertCount(count($names), $list); - foreach ($list as $item) { - $this->assertContains($item['name'], $names); - } - - $cache = $storage->getCache(); - $scanner = $storage->getScanner(); - $scanner->scan(''); - - $list = $cache->getFolderContents(''); - - $this->assertCount(count($names), $list); - foreach ($list as $item) { - $this->assertContains($item['name'], $names); - } - } - - public function xtestLongPath() { - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - - $rootView = new View(''); - - $longPath = ''; - $ds = DIRECTORY_SEPARATOR; - /* - * 4096 is the maximum path length in file_cache.path in *nix - */ - $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789'; - $tmpdirLength = strlen(Server::get(ITempManager::class)->getTemporaryFolder()); - $depth = ((4000 - $tmpdirLength) / 57); - - foreach (range(0, $depth - 1) as $i) { - $longPath .= $ds . $folderName; - $result = $rootView->mkdir($longPath); - $this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath)); - - $result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem'); - $this->assertEquals(5, $result, "file_put_contents failed on $i"); - - $this->assertTrue($rootView->file_exists($longPath)); - $this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt")); - } - - $cache = $storage->getCache(); - $scanner = $storage->getScanner(); - $scanner->scan(''); - - $longPath = $folderName; - foreach (range(0, $depth - 1) as $i) { - $cachedFolder = $cache->get($longPath); - $this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i"); - $this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i"); - - $cachedFile = $cache->get($longPath . '/test.txt'); - $this->assertTrue(is_array($cachedFile), "No cache entry for file at $i"); - $this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i"); - - $longPath .= $ds . $folderName; - } - } - - public function testTouchNotSupported(): void { - $storage = new TemporaryNoTouch([]); - $scanner = $storage->getScanner(); - Filesystem::mount($storage, [], '/test/'); - $past = time() - 100; - $storage->file_put_contents('test', 'foobar'); - $scanner->scan(''); - $view = new View(''); - $info = $view->getFileInfo('/test/test'); - - $view->touch('/test/test', $past); - $scanner->scanFile('test', Scanner::REUSE_ETAG); - - $info2 = $view->getFileInfo('/test/test'); - $this->assertSame($info['etag'], $info2['etag']); - } - - public function testWatcherEtagCrossStorage(): void { - $storage1 = new Temporary([]); - $storage2 = new Temporary([]); - $scanner1 = $storage1->getScanner(); - $scanner2 = $storage2->getScanner(); - $storage1->mkdir('sub'); - Filesystem::mount($storage1, [], '/test/'); - Filesystem::mount($storage2, [], '/test/sub/storage'); - - $past = time() - 100; - $storage2->file_put_contents('test.txt', 'foobar'); - $scanner1->scan(''); - $scanner2->scan(''); - $view = new View(''); - - $storage2->getWatcher('')->setPolicy(Watcher::CHECK_ALWAYS); - - $oldFileInfo = $view->getFileInfo('/test/sub/storage/test.txt'); - $oldFolderInfo = $view->getFileInfo('/test'); - - $storage2->getCache()->update($oldFileInfo->getId(), [ - 'storage_mtime' => $past, - ]); - - $oldEtag = $oldFolderInfo->getEtag(); - - $view->getFileInfo('/test/sub/storage/test.txt'); - $newFolderInfo = $view->getFileInfo('/test'); - - $this->assertNotEquals($newFolderInfo->getEtag(), $oldEtag); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('absolutePathProvider')] - public function testGetAbsolutePath($expectedPath, $relativePath): void { - $view = new View('/files'); - $this->assertEquals($expectedPath, $view->getAbsolutePath($relativePath)); - } - - public function testPartFileInfo(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - Filesystem::mount($storage, [], '/test/'); - $sizeWritten = $storage->file_put_contents('test.part', 'foobar'); - $scanner->scan(''); - $view = new View('/test'); - $info = $view->getFileInfo('test.part'); - - $this->assertInstanceOf('\OCP\Files\FileInfo', $info); - $this->assertNull($info->getId()); - $this->assertEquals(6, $sizeWritten); - $this->assertEquals(6, $info->getSize()); - $this->assertEquals('foobar', $view->file_get_contents('test.part')); - } - - public static function absolutePathProvider(): array { - return [ - ['/files', ''], - ['/files/0', '0'], - ['/files/false', 'false'], - ['/files/true', 'true'], - ['/files', '/'], - ['/files/test', 'test'], - ['/files/test', '/test'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('chrootRelativePathProvider')] - public function testChrootGetRelativePath($root, $absolutePath, $expectedPath): void { - $view = new View('/files'); - $view->chroot($root); - $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath)); - } - - public static function chrootRelativePathProvider(): array { - return self::relativePathProvider('/'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('initRelativePathProvider')] - public function testInitGetRelativePath($root, $absolutePath, $expectedPath): void { - $view = new View($root); - $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath)); - } - - public static function initRelativePathProvider(): array { - return self::relativePathProvider(null); - } - - public static function relativePathProvider($missingRootExpectedPath): array { - return [ - // No root - returns the path - ['', '/files', '/files'], - ['', '/files/', '/files/'], - - // Root equals path - / - ['/files/', '/files/', '/'], - ['/files/', '/files', '/'], - ['/files', '/files/', '/'], - ['/files', '/files', '/'], - - // False negatives: chroot fixes those by adding the leading slash. - // But setting them up with this root (instead of chroot($root)) - // will fail them, although they should be the same. - // TODO init should be fixed, so it also adds the leading slash - ['files/', '/files/', $missingRootExpectedPath], - ['files', '/files/', $missingRootExpectedPath], - ['files/', '/files', $missingRootExpectedPath], - ['files', '/files', $missingRootExpectedPath], - - // False negatives: Paths provided to the method should have a leading slash - // TODO input should be checked to have a leading slash - ['/files/', 'files/', null], - ['/files', 'files/', null], - ['/files/', 'files', null], - ['/files', 'files', null], - - // with trailing slashes - ['/files/', '/files/0', '0'], - ['/files/', '/files/false', 'false'], - ['/files/', '/files/true', 'true'], - ['/files/', '/files/test', 'test'], - ['/files/', '/files/test/foo', 'test/foo'], - - // without trailing slashes - // TODO false expectation: Should match "with trailing slashes" - ['/files', '/files/0', '/0'], - ['/files', '/files/false', '/false'], - ['/files', '/files/true', '/true'], - ['/files', '/files/test', '/test'], - ['/files', '/files/test/foo', '/test/foo'], - - // leading slashes - ['/files/', '/files_trashbin/', null], - ['/files', '/files_trashbin/', null], - ['/files/', '/files_trashbin', null], - ['/files', '/files_trashbin', null], - - // no leading slashes - ['files/', 'files_trashbin/', null], - ['files', 'files_trashbin/', null], - ['files/', 'files_trashbin', null], - ['files', 'files_trashbin', null], - - // mixed leading slashes - ['files/', '/files_trashbin/', null], - ['/files/', 'files_trashbin/', null], - ['files', '/files_trashbin/', null], - ['/files', 'files_trashbin/', null], - ['files/', '/files_trashbin', null], - ['/files/', 'files_trashbin', null], - ['files', '/files_trashbin', null], - ['/files', 'files_trashbin', null], - - ['files', 'files_trashbin/test', null], - ['/files', '/files_trashbin/test', null], - ['/files', 'files_trashbin/test', null], - ]; - } - - public function testFileView(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $storage->file_put_contents('foo.txt', 'bar'); - Filesystem::mount($storage, [], '/test/'); - $scanner->scan(''); - $view = new View('/test/foo.txt'); - - $this->assertEquals('bar', $view->file_get_contents('')); - $fh = tmpfile(); - fwrite($fh, 'foo'); - rewind($fh); - $view->file_put_contents('', $fh); - $this->assertEquals('foo', $view->file_get_contents('')); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('tooLongPathDataProvider')] - public function testTooLongPath($operation, $param0 = null): void { - $this->expectException(InvalidPathException::class); - - $longPath = ''; - // 4000 is the maximum path length in file_cache.path - $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789'; - $depth = (4000 / 57); - foreach (range(0, $depth + 1) as $i) { - $longPath .= '/' . $folderName; - } - - $storage = new Temporary([]); - $this->tempStorage = $storage; // for later hard cleanup - Filesystem::mount($storage, [], '/'); - - $rootView = new View(''); - - if ($param0 === '@0') { - $param0 = $longPath; - } - - if ($operation === 'hash') { - $param0 = $longPath; - $longPath = 'md5'; - } - - call_user_func([$rootView, $operation], $longPath, $param0); - } - - public static function tooLongPathDataProvider(): array { - return [ - ['getAbsolutePath'], - ['getRelativePath'], - ['getMountPoint'], - ['resolvePath'], - ['getLocalFile'], - ['mkdir'], - ['rmdir'], - ['opendir'], - ['is_dir'], - ['is_file'], - ['stat'], - ['filetype'], - ['filesize'], - ['readfile'], - ['isCreatable'], - ['isReadable'], - ['isUpdatable'], - ['isDeletable'], - ['isSharable'], - ['file_exists'], - ['filemtime'], - ['touch'], - ['file_get_contents'], - ['unlink'], - ['deleteAll'], - ['toTmpFile'], - ['getMimeType'], - ['free_space'], - ['getFileInfo'], - ['getDirectoryContent'], - ['getOwner'], - ['getETag'], - ['file_put_contents', 'ipsum'], - ['rename', '@0'], - ['copy', '@0'], - ['fopen', 'r'], - ['fromTmpFile', '@0'], - ['hash'], - ['hasUpdated', 0], - ['putFileInfo', []], - ]; - } - - public function testRenameCrossStoragePreserveMtime(): void { - $storage1 = new Temporary([]); - $storage2 = new Temporary([]); - $storage1->mkdir('sub'); - $storage1->mkdir('foo'); - $storage1->file_put_contents('foo.txt', 'asd'); - $storage1->file_put_contents('foo/bar.txt', 'asd'); - Filesystem::mount($storage1, [], '/test/'); - Filesystem::mount($storage2, [], '/test/sub/storage'); - - $view = new View(''); - $time = time() - 200; - $view->touch('/test/foo.txt', $time); - $view->touch('/test/foo', $time); - $view->touch('/test/foo/bar.txt', $time); - - $view->rename('/test/foo.txt', '/test/sub/storage/foo.txt'); - - $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo.txt')); - - $view->rename('/test/foo', '/test/sub/storage/foo'); - - $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo/bar.txt')); - } - - public function testRenameFailDeleteTargetKeepSource(): void { - $this->doTestCopyRenameFail('rename'); - } - - public function testCopyFailDeleteTargetKeepSource(): void { - $this->doTestCopyRenameFail('copy'); - } - - private function doTestCopyRenameFail($operation) { - $storage1 = new Temporary([]); - /** @var \PHPUnit\Framework\MockObject\MockObject|Temporary $storage2 */ - $storage2 = $this->getMockBuilder(TemporaryNoCross::class) - ->setConstructorArgs([[]]) - ->onlyMethods(['fopen', 'writeStream']) - ->getMock(); - - $storage2->method('writeStream') - ->willThrowException(new GenericFileException('Failed to copy stream')); - - $storage2->method('fopen') - ->willReturn(false); - - $storage1->mkdir('sub'); - $storage1->file_put_contents('foo.txt', '0123456789ABCDEFGH'); - $storage1->mkdir('dirtomove'); - $storage1->file_put_contents('dirtomove/indir1.txt', '0123456'); // fits - $storage1->file_put_contents('dirtomove/indir2.txt', '0123456789ABCDEFGH'); // doesn't fit - $storage2->file_put_contents('existing.txt', '0123'); - $storage1->getScanner()->scan(''); - $storage2->getScanner()->scan(''); - Filesystem::mount($storage1, [], '/test/'); - Filesystem::mount($storage2, [], '/test/sub/storage'); - - // move file - $view = new View(''); - $this->assertTrue($storage1->file_exists('foo.txt')); - $this->assertFalse($storage2->file_exists('foo.txt')); - $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/foo.txt')); - $this->assertFalse($storage2->file_exists('foo.txt')); - $this->assertFalse($storage2->getCache()->get('foo.txt')); - $this->assertTrue($storage1->file_exists('foo.txt')); - - // if target exists, it will be deleted too - $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/existing.txt')); - $this->assertFalse($storage2->file_exists('existing.txt')); - $this->assertFalse($storage2->getCache()->get('existing.txt')); - $this->assertTrue($storage1->file_exists('foo.txt')); - - // move folder - $this->assertFalse($view->$operation('/test/dirtomove/', '/test/sub/storage/dirtomove/')); - // since the move failed, the full source tree is kept - $this->assertTrue($storage1->file_exists('dirtomove/indir1.txt')); - $this->assertTrue($storage1->file_exists('dirtomove/indir2.txt')); - // second file not moved/copied - $this->assertFalse($storage2->file_exists('dirtomove/indir2.txt')); - $this->assertFalse($storage2->getCache()->get('dirtomove/indir2.txt')); - } - - public function testDeleteFailKeepCache(): void { - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->setConstructorArgs([[]]) - ->onlyMethods(['unlink']) - ->getMock(); - $storage->expects($this->once()) - ->method('unlink') - ->willReturn(false); - $scanner = $storage->getScanner(); - $cache = $storage->getCache(); - $storage->file_put_contents('foo.txt', 'asd'); - $scanner->scan(''); - Filesystem::mount($storage, [], '/test/'); - - $view = new View('/test'); - - $this->assertFalse($view->unlink('foo.txt')); - $this->assertTrue($cache->inCache('foo.txt')); - } - - public static function directoryTraversalProvider(): array { - return [ - ['../test/'], - ['..\\test\\my/../folder'], - ['/test/my/../foo\\'], - ]; - } - - /** - * @param string $root - */ - #[\PHPUnit\Framework\Attributes\DataProvider('directoryTraversalProvider')] - public function testConstructDirectoryTraversalException($root): void { - $this->expectException(\Exception::class); - - new View($root); - } - - public function testRenameOverWrite(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $storage->mkdir('sub'); - $storage->mkdir('foo'); - $storage->file_put_contents('foo.txt', 'asd'); - $storage->file_put_contents('foo/bar.txt', 'asd'); - $scanner->scan(''); - Filesystem::mount($storage, [], '/test/'); - $view = new View(''); - $this->assertTrue($view->rename('/test/foo.txt', '/test/foo/bar.txt')); - } - - public function testSetMountOptionsInStorage(): void { - $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['foo' => 'bar']); - Filesystem::getMountManager()->addMount($mount); - /** @var Common $storage */ - $storage = $mount->getStorage(); - $this->assertEquals($storage->getMountOption('foo'), 'bar'); - } - - public function testSetMountOptionsWatcherPolicy(): void { - $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['filesystem_check_changes' => Watcher::CHECK_NEVER]); - Filesystem::getMountManager()->addMount($mount); - /** @var Common $storage */ - $storage = $mount->getStorage(); - $watcher = $storage->getWatcher(); - $this->assertEquals(Watcher::CHECK_NEVER, $watcher->getPolicy()); - } - - public function testGetAbsolutePathOnNull(): void { - $view = new View(); - $this->assertNull($view->getAbsolutePath(null)); - } - - public function testGetRelativePathOnNull(): void { - $view = new View(); - $this->assertNull($view->getRelativePath(null)); - } - - public function testNullAsRoot(): void { - $this->expectException(\TypeError::class); - - new View(null); - } - - /** - * e.g. reading from a folder that's being renamed - * - * - * - * @param string $rootPath - * @param string $pathPrefix - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')] - public function testReadFromWriteLockedPath($rootPath, $pathPrefix): void { - $this->expectException(LockedException::class); - - $rootPath = str_replace('{folder}', 'files', $rootPath); - $pathPrefix = str_replace('{folder}', 'files', $pathPrefix); - - $view = new View($rootPath); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE)); - $view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED); - } - - /** - * Reading from a files_encryption folder that's being renamed - * - * - * @param string $rootPath - * @param string $pathPrefix - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')] - public function testReadFromWriteUnlockablePath($rootPath, $pathPrefix): void { - $rootPath = str_replace('{folder}', 'files_encryption', $rootPath); - $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix); - - $view = new View($rootPath); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED)); - } - - /** - * e.g. writing a file that's being downloaded - * - * - * - * @param string $rootPath - * @param string $pathPrefix - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')] - public function testWriteToReadLockedFile($rootPath, $pathPrefix): void { - $this->expectException(LockedException::class); - - $rootPath = str_replace('{folder}', 'files', $rootPath); - $pathPrefix = str_replace('{folder}', 'files', $pathPrefix); - - $view = new View($rootPath); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED)); - $view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE); - } - - /** - * Writing a file that's being downloaded - * - * - * @param string $rootPath - * @param string $pathPrefix - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')] - public function testWriteToReadUnlockableFile($rootPath, $pathPrefix): void { - $rootPath = str_replace('{folder}', 'files_encryption', $rootPath); - $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix); - - $view = new View($rootPath); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE)); - } - - /** - * Test that locks are on mount point paths instead of mount root - */ - public function testLockLocalMountPointPathInsteadOfStorageRoot(): void { - $lockingProvider = Server::get(ILockingProvider::class); - $view = new View('/testuser/files/'); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $mountedStorage = new Temporary([]); - Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint'); - - $this->assertTrue( - $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, true), - 'Can lock mount point' - ); - - // no exception here because storage root was not locked - $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - - $thrown = false; - try { - $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - } catch (LockedException $e) { - $thrown = true; - } - $this->assertTrue($thrown, 'Mount point path was locked on root storage'); - - $lockingProvider->releaseAll(); - } - - /** - * Test that locks are on mount point paths and also mount root when requested - */ - public function testLockStorageRootButNotLocalMountPoint(): void { - $lockingProvider = Server::get(ILockingProvider::class); - $view = new View('/testuser/files/'); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $mountedStorage = new Temporary([]); - Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint'); - - $this->assertTrue( - $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, false), - 'Can lock mount point' - ); - - $thrown = false; - try { - $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - } catch (LockedException $e) { - $thrown = true; - } - $this->assertTrue($thrown, 'Mount point storage root was locked on original storage'); - - // local mount point was not locked - $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - - $lockingProvider->releaseAll(); - } - - /** - * Test that locks are on mount point paths and also mount root when requested - */ - public function testLockMountPointPathFailReleasesBoth(): void { - $lockingProvider = Server::get(ILockingProvider::class); - $view = new View('/testuser/files/'); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - $mountedStorage = new Temporary([]); - Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint.txt'); - - // this would happen if someone is writing on the mount point - $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - - $thrown = false; - try { - // this actually acquires two locks, one on the mount point and one on the storage root, - // but the one on the storage root will fail - $view->lockFile('/mountpoint.txt', ILockingProvider::LOCK_SHARED); - } catch (LockedException $e) { - $thrown = true; - } - $this->assertTrue($thrown, 'Cannot acquire shared lock because storage root is already locked'); - - // from here we expect that the lock on the local mount point was released properly - // so acquiring an exclusive lock will succeed - $storage->acquireLock('/testuser/files/mountpoint.txt', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); - - $lockingProvider->releaseAll(); - } - - public static function dataLockPaths(): array { - return [ - ['/testuser/{folder}', ''], - ['/testuser', '/{folder}'], - ['', '/testuser/{folder}'], - ]; - } - - public static function pathRelativeToFilesProvider(): array { - return [ - ['admin/files', ''], - ['admin/files/x', 'x'], - ['/admin/files', ''], - ['/admin/files/sub', 'sub'], - ['/admin/files/sub/', 'sub'], - ['/admin/files/sub/sub2', 'sub/sub2'], - ['//admin//files/sub//sub2', 'sub/sub2'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProvider')] - public function testGetPathRelativeToFiles($path, $expectedPath): void { - $view = new View(); - $this->assertEquals($expectedPath, $view->getPathRelativeToFiles($path)); - } - - public static function pathRelativeToFilesProviderExceptionCases(): array { - return [ - [''], - ['x'], - ['files'], - ['/files'], - ['/admin/files_versions/abc'], - ]; - } - - /** - * @param string $path - */ - #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProviderExceptionCases')] - public function testGetPathRelativeToFilesWithInvalidArgument($path): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('$absolutePath must be relative to "files"'); - - $view = new View(); - $view->getPathRelativeToFiles($path); - } - - public function testChangeLock(): void { - $view = new View('/testuser/files/'); - $storage = new Temporary([]); - Filesystem::mount($storage, [], '/'); - - $view->lockFile('/test/sub', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE)); - - $view->changeLock('//test/sub', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE)); - - $view->changeLock('test/sub', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED)); - - $view->unlockFile('/test/sub/', ILockingProvider::LOCK_SHARED); - - $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public static function hookPathProvider(): array { - return [ - ['/foo/files', '/foo', true], - ['/foo/files/bar', '/foo', true], - ['/foo', '/foo', false], - ['/foo', '/files/foo', true], - ['/foo', 'filesfoo', false], - ['', '/foo/files', true], - ['', '/foo/files/bar.txt', true], - ]; - } - - /** - * @param $root - * @param $path - * @param $shouldEmit - */ - #[\PHPUnit\Framework\Attributes\DataProvider('hookPathProvider')] - public function testHookPaths($root, $path, $shouldEmit): void { - $filesystemReflection = new \ReflectionClass(Filesystem::class); - $defaultRootValue = $filesystemReflection->getProperty('defaultInstance'); - $oldRoot = $defaultRootValue->getValue(); - $defaultView = new View('/foo/files'); - $defaultRootValue->setValue(null, $defaultView); - $view = new View($root); - $result = self::invokePrivate($view, 'shouldEmitHooks', [$path]); - $defaultRootValue->setValue(null, $oldRoot); - $this->assertEquals($shouldEmit, $result); - } - - /** - * Create test movable mount points - * - * @param array $mountPoints array of mount point locations - * @return array array of MountPoint objects - */ - private function createTestMovableMountPoints($mountPoints) { - $mounts = []; - foreach ($mountPoints as $mountPoint) { - $storage = $this->getMockBuilder(Storage::class) - ->onlyMethods([]) - ->getMock(); - $storage->method('getId')->willReturn('non-null-id'); - $storage->method('getStorageCache')->willReturnCallback(function () use ($storage) { - return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class)); - }); - - $mount = $this->getMockBuilder(TestMoveableMountPoint::class) - ->onlyMethods(['moveMount', 'getNumericStorageId']) - ->setConstructorArgs([$storage, $mountPoint]) - ->getMock(); - - $mount->method('getNumericStorageId') - ->willReturn(1); - - $mounts[] = $mount; - } - - /** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */ - $mountProvider = $this->createMock(IMountProvider::class); - $mountProvider->expects($this->any()) - ->method('getMountsForUser') - ->willReturn($mounts); - - $mountProviderCollection = Server::get(IMountProviderCollection::class); - $mountProviderCollection->registerProvider($mountProvider); - - return $mounts; - } - - /** - * Test mount point move - */ - public function testMountPointMove(): void { - self::loginAsUser(self::$user); - - [$mount1, $mount2] = $this->createTestMovableMountPoints([ - self::$user . '/files/mount1', - self::$user . '/files/mount2', - ]); - $mount1->expects($this->once()) - ->method('moveMount') - ->willReturn(true); - - $mount2->expects($this->once()) - ->method('moveMount') - ->willReturn(true); - - $view = new View('/' . self::$user . '/files/'); - $view->mkdir('sub'); - - $this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point'); - $this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory'); - } - - public function testMoveMountPointOverwrite(): void { - self::loginAsUser(self::$user); - - [$mount1, $mount2] = $this->createTestMovableMountPoints([ - self::$user . '/files/mount1', - self::$user . '/files/mount2', - ]); - - $mount1->expects($this->never()) - ->method('moveMount'); - - $mount2->expects($this->never()) - ->method('moveMount'); - - $view = new View('/' . self::$user . '/files/'); - - $this->expectException(ForbiddenException::class); - $view->rename('mount1', 'mount2'); - } - - public function testMoveMountPointIntoMount(): void { - self::loginAsUser(self::$user); - - [$mount1, $mount2] = $this->createTestMovableMountPoints([ - self::$user . '/files/mount1', - self::$user . '/files/mount2', - ]); - - $mount1->expects($this->never()) - ->method('moveMount'); - - $mount2->expects($this->never()) - ->method('moveMount'); - - $view = new View('/' . self::$user . '/files/'); - - $this->expectException(ForbiddenException::class); - $view->rename('mount1', 'mount2/sub'); - } - - /** - * Test that moving a mount point into a shared folder is forbidden - */ - public function testMoveMountPointIntoSharedFolder(): void { - self::loginAsUser(self::$user); - - [$mount1, $mount2] = $this->createTestMovableMountPoints([ - self::$user . '/files/mount1', - self::$user . '/files/mount2', - ]); - - $mount1->expects($this->never()) - ->method('moveMount'); - - $mount2->expects($this->once()) - ->method('moveMount') - ->willReturn(true); - - $view = new View('/' . self::$user . '/files/'); - $view->mkdir('shareddir'); - $view->mkdir('shareddir/sub'); - $view->mkdir('shareddir/sub2'); - // Create a similar named but non-shared folder - $view->mkdir('shareddir notshared'); - - $fileId = $view->getFileInfo('shareddir')->getId(); - $userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses'); - - $userFolder = \OC::$server->getUserFolder(self::$user); - $shareDir = $userFolder->get('shareddir'); - $shareManager = Server::get(IShareManager::class); - $share = $shareManager->newShare(); - $share->setSharedWith('test2') - ->setSharedBy(self::$user) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_READ) - ->setNode($shareDir); - $shareManager->createShare($share); - - try { - $view->rename('mount1', 'shareddir'); - $this->fail('Cannot overwrite shared folder'); - } catch (ForbiddenException $e) { - - } - try { - $view->rename('mount1', 'shareddir/sub'); - $this->fail('Cannot move mount point into shared folder'); - } catch (ForbiddenException $e) { - - } - try { - $view->rename('mount1', 'shareddir/sub/sub2'); - $this->fail('Cannot move mount point into shared subfolder'); - } catch (ForbiddenException $e) { - - } - $this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder'); - - $shareManager->deleteShare($share); - $userObject->delete(); - } - - public static function basicOperationProviderForLocks(): array { - return [ - // --- write hook ---- - [ - 'touch', - ['touch-create.txt'], - 'touch-create.txt', - 'create', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - ILockingProvider::LOCK_SHARED, - ], - [ - 'fopen', - ['test-write.txt', 'w'], - 'test-write.txt', - 'write', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - null, - // exclusive lock stays until fclose - ILockingProvider::LOCK_EXCLUSIVE, - ], - [ - 'mkdir', - ['newdir'], - 'newdir', - 'write', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - ILockingProvider::LOCK_SHARED, - ], - [ - 'file_put_contents', - ['file_put_contents.txt', 'blah'], - 'file_put_contents.txt', - 'write', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - ILockingProvider::LOCK_SHARED, - null, - 0, - ], - - // ---- delete hook ---- - [ - 'rmdir', - ['dir'], - 'dir', - 'delete', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - ILockingProvider::LOCK_SHARED, - ], - [ - 'unlink', - ['test.txt'], - 'test.txt', - 'delete', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_EXCLUSIVE, - ILockingProvider::LOCK_SHARED, - ], - - // ---- read hook (no post hooks) ---- - [ - 'file_get_contents', - ['test.txt'], - 'test.txt', - 'read', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - null, - false, - ], - [ - 'fopen', - ['test.txt', 'r'], - 'test.txt', - 'read', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - ], - [ - 'opendir', - ['dir'], - 'dir', - 'read', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - ], - - // ---- no lock, touch hook --- - ['touch', ['test.txt'], 'test.txt', 'touch', null, null, null], - - // ---- no hooks, no locks --- - ['is_dir', ['dir'], 'dir', ''], - ['is_file', ['dir'], 'dir', ''], - [ - 'stat', - ['dir'], - 'dir', - '', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - false, - ], - [ - 'filetype', - ['dir'], - 'dir', - '', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - false, - ], - [ - 'filesize', - ['dir'], - 'dir', - '', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - /* Return an int */ - 100 - ], - ['isCreatable', ['dir'], 'dir', ''], - ['isReadable', ['dir'], 'dir', ''], - ['isUpdatable', ['dir'], 'dir', ''], - ['isDeletable', ['dir'], 'dir', ''], - ['isSharable', ['dir'], 'dir', ''], - ['file_exists', ['dir'], 'dir', ''], - [ - 'filemtime', - ['dir'], - 'dir', - '', - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - ILockingProvider::LOCK_SHARED, - null, - false, - ], - ]; - } - - /** - * Test whether locks are set before and after the operation - * - * - * @param string $operation operation name on the view - * @param array $operationArgs arguments for the operation - * @param string $lockedPath path of the locked item to check - * @param string $hookType hook type - * @param ?int $expectedLockBefore expected lock during pre hooks - * @param ?int $expectedLockDuring expected lock during operation - * @param ?int $expectedLockAfter expected lock during post hooks - * @param ?int $expectedStrayLock expected lock after returning, should - * be null (unlock) for most operations - */ - #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')] - public function testLockBasicOperation( - string $operation, - array $operationArgs, - string $lockedPath, - string $hookType, - ?int $expectedLockBefore = ILockingProvider::LOCK_SHARED, - ?int $expectedLockDuring = ILockingProvider::LOCK_SHARED, - ?int $expectedLockAfter = ILockingProvider::LOCK_SHARED, - ?int $expectedStrayLock = null, - mixed $returnValue = true, - ): void { - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary&MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$operation]) - ->getMock(); - - /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */ - Server::get(ITrashManager::class)->pauseTrash(); - /* Same thing with encryption wrapper */ - Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); - - Filesystem::mount($storage, [], self::$user . '/'); - - // work directly on disk because mkdir might be mocked - $realPath = $storage->getSourcePath(''); - mkdir($realPath . '/files'); - mkdir($realPath . '/files/dir'); - file_put_contents($realPath . '/files/test.txt', 'blah'); - $storage->getScanner()->scan('files'); - - $storage->expects($this->once()) - ->method($operation) - ->willReturnCallback( - function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) { - $lockTypeDuring = $this->getFileLockType($view, $lockedPath); - - return $returnValue; - } - ); - - $this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation'); - - $this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost); - - // do operation - call_user_func_array([$view, $operation], $operationArgs); - - if ($hookType !== '') { - $this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook'); - $this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook'); - $this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation'); - } else { - $this->assertNull($lockTypeDuring, 'File not locked during operation'); - } - - $this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath)); - - /* Resume trash to avoid side effects */ - Server::get(ITrashManager::class)->resumeTrash(); - } - - /** - * Test locks for file_put_content with stream. - * This code path uses $storage->fopen instead - */ - public function testLockFilePutContentWithStream(): void { - $view = new View('/' . self::$user . '/files/'); - - $path = 'test_file_put_contents.txt'; - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods(['fopen']) - ->getMock(); - - Filesystem::mount($storage, [], self::$user . '/'); - $storage->mkdir('files'); - - $storage->expects($this->once()) - ->method('fopen') - ->willReturnCallback( - function () use ($view, $path, &$lockTypeDuring) { - $lockTypeDuring = $this->getFileLockType($view, $path); - - return fopen('php://temp', 'r+'); - } - ); - - $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost); - - $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation'); - - // do operation - $view->file_put_contents($path, fopen('php://temp', 'r+')); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation'); - - $this->assertNull($this->getFileLockType($view, $path)); - } - - /** - * Test locks for fopen with fclose at the end - */ - public function testLockFopen(): void { - $view = new View('/' . self::$user . '/files/'); - - $path = 'test_file_put_contents.txt'; - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods(['fopen']) - ->getMock(); - - Filesystem::mount($storage, [], self::$user . '/'); - $storage->mkdir('files'); - - $storage->expects($this->once()) - ->method('fopen') - ->willReturnCallback( - function () use ($view, $path, &$lockTypeDuring) { - $lockTypeDuring = $this->getFileLockType($view, $path); - - return fopen('php://temp', 'r+'); - } - ); - - $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost); - - $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation'); - - // do operation - $res = $view->fopen($path, 'w'); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation'); - $this->assertNull($lockTypePost, 'No post hook, no lock check possible'); - - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen'); - - fclose($res); - - $this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose'); - } - - /** - * Test locks for fopen with fclose at the end - * - * - * @param string $operation operation name on the view - * @param array $operationArgs arguments for the operation - * @param string $path path of the locked item to check - */ - #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')] - public function testLockBasicOperationUnlocksAfterException( - $operation, - $operationArgs, - $path, - ): void { - if ($operation === 'touch') { - $this->markTestSkipped('touch handles storage exceptions internally'); - } - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$operation]) - ->getMock(); - - /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */ - Server::get(ITrashManager::class)->pauseTrash(); - /* Same thing with encryption wrapper */ - Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); - - Filesystem::mount($storage, [], self::$user . '/'); - - // work directly on disk because mkdir might be mocked - $realPath = $storage->getSourcePath(''); - mkdir($realPath . '/files'); - mkdir($realPath . '/files/dir'); - file_put_contents($realPath . '/files/test.txt', 'blah'); - $storage->getScanner()->scan('files'); - - $storage->expects($this->once()) - ->method($operation) - ->willReturnCallback( - function (): void { - throw new \Exception('Simulated exception'); - } - ); - - $thrown = false; - try { - call_user_func_array([$view, $operation], $operationArgs); - } catch (\Exception $e) { - $thrown = true; - $this->assertEquals('Simulated exception', $e->getMessage()); - } - $this->assertTrue($thrown, 'Exception was rethrown'); - $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception'); - - /* Resume trash to avoid side effects */ - Server::get(ITrashManager::class)->resumeTrash(); - } - - public function testLockBasicOperationUnlocksAfterLockException(): void { - $view = new View('/' . self::$user . '/files/'); - - $storage = new Temporary([]); - - Filesystem::mount($storage, [], self::$user . '/'); - - $storage->mkdir('files'); - $storage->mkdir('files/dir'); - $storage->file_put_contents('files/test.txt', 'blah'); - $storage->getScanner()->scan('files'); - - // get a shared lock - $handle = $view->fopen('test.txt', 'r'); - - $thrown = false; - try { - // try (and fail) to get a write lock - $view->unlink('test.txt'); - } catch (\Exception $e) { - $thrown = true; - $this->assertInstanceOf(LockedException::class, $e); - } - $this->assertTrue($thrown, 'Exception was rethrown'); - - // clean shared lock - fclose($handle); - - $this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked'); - } - - /** - * Test locks for fopen with fclose at the end - * - * - * @param string $operation operation name on the view - * @param array $operationArgs arguments for the operation - * @param string $path path of the locked item to check - * @param string $hookType hook type - */ - #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')] - public function testLockBasicOperationUnlocksAfterCancelledHook( - $operation, - $operationArgs, - $path, - $hookType, - ): void { - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$operation]) - ->getMock(); - - Filesystem::mount($storage, [], self::$user . '/'); - $storage->mkdir('files'); - - Util::connectHook( - Filesystem::CLASSNAME, - $hookType, - HookHelper::class, - 'cancellingCallback' - ); - - call_user_func_array([$view, $operation], $operationArgs); - - $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception'); - } - - public static function lockFileRenameOrCopyDataProvider(): array { - return [ - ['rename', ILockingProvider::LOCK_EXCLUSIVE], - ['copy', ILockingProvider::LOCK_SHARED], - ]; - } - - /** - * Test locks for rename or copy operation - * - * - * @param string $operation operation to be done on the view - * @param int $expectedLockTypeSourceDuring expected lock type on source file during - * the operation - */ - #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')] - public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void { - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$operation, 'getMetaData', 'filemtime']) - ->getMock(); - - $storage->expects($this->any()) - ->method('getMetaData') - ->willReturn([ - 'mtime' => 1885434487, - 'etag' => '', - 'mimetype' => 'text/plain', - 'permissions' => Constants::PERMISSION_ALL, - 'size' => 3 - ]); - $storage->expects($this->any()) - ->method('filemtime') - ->willReturn(123456789); - - $sourcePath = 'original.txt'; - $targetPath = 'target.txt'; - - /* Disable encryption wrapper to avoid it intercepting mocked call */ - Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); - - Filesystem::mount($storage, [], self::$user . '/'); - $storage->mkdir('files'); - $view->file_put_contents($sourcePath, 'meh'); - - $storage->expects($this->once()) - ->method($operation) - ->willReturnCallback( - function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) { - $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath); - $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath); - - return true; - } - ); - - $this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost); - $this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation'); - - $view->$operation($sourcePath, $targetPath); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook'); - $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook'); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook'); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation'); - } - - /** - * simulate a failed copy operation. - * We expect that we catch the exception, free the lock and re-throw it. - * - */ - public function testLockFileCopyException(): void { - $this->expectException(\Exception::class); - - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods(['copy']) - ->getMock(); - - $sourcePath = 'original.txt'; - $targetPath = 'target.txt'; - - /* Disable encryption wrapper to avoid it intercepting mocked call */ - Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); - - Filesystem::mount($storage, [], self::$user . '/'); - $storage->mkdir('files'); - $view->file_put_contents($sourcePath, 'meh'); - - $storage->expects($this->once()) - ->method('copy') - ->willReturnCallback( - function (): void { - throw new \Exception(); - } - ); - - $this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost); - $this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation'); - - try { - $view->copy($sourcePath, $targetPath); - } catch (\Exception $e) { - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation'); - throw $e; - } - } - - /** - * Test rename operation: unlock first path when second path was locked - */ - public function testLockFileRenameUnlockOnException(): void { - self::loginAsUser('test'); - - $view = new View('/' . self::$user . '/files/'); - - $sourcePath = 'original.txt'; - $targetPath = 'target.txt'; - $view->file_put_contents($sourcePath, 'meh'); - - // simulate that the target path is already locked - $view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation'); - - $thrown = false; - try { - $view->rename($sourcePath, $targetPath); - } catch (LockedException $e) { - $thrown = true; - } - - $this->assertTrue($thrown, 'LockedException thrown'); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation'); - - $view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE); - } - - /** - * Test rename operation: unlock first path when second path was locked - */ - public function testGetOwner(): void { - self::loginAsUser('test'); - - $view = new View('/test/files/'); - - $path = 'foo.txt'; - $view->file_put_contents($path, 'meh'); - - $this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID()); - - $folderInfo = $view->getDirectoryContent(''); - $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) { - return $info->getName() === 'foo.txt'; - })); - - $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID()); - - $subStorage = new Temporary(); - Filesystem::mount($subStorage, [], '/test/files/asd'); - - $folderInfo = $view->getDirectoryContent(''); - $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) { - return $info->getName() === 'asd'; - })); - - $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID()); - } - - public static function lockFileRenameOrCopyCrossStorageDataProvider(): array { - return [ - ['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE], - ['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED], - ]; - } - - /** - * Test locks for rename or copy operation cross-storage - * - * - * @param string $viewOperation operation to be done on the view - * @param string $storageOperation operation to be mocked on the storage - * @param int $expectedLockTypeSourceDuring expected lock type on source file during - * the operation - */ - #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')] - public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void { - $view = new View('/' . self::$user . '/files/'); - - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$storageOperation]) - ->getMock(); - /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */ - $storage2 = $this->getMockBuilder(Temporary::class) - ->onlyMethods([$storageOperation, 'getMetaData', 'filemtime']) - ->getMock(); - - $storage2->expects($this->any()) - ->method('getMetaData') - ->willReturn([ - 'mtime' => 1885434487, - 'etag' => '', - 'mimetype' => 'text/plain', - 'permissions' => Constants::PERMISSION_ALL, - 'size' => 3 - ]); - $storage2->expects($this->any()) - ->method('filemtime') - ->willReturn(123456789); - - $sourcePath = 'original.txt'; - $targetPath = 'substorage/target.txt'; - - /* Disable encryption wrapper to avoid it intercepting mocked call */ - Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); - - Filesystem::mount($storage, [], self::$user . '/'); - Filesystem::mount($storage2, [], self::$user . '/files/substorage'); - $storage->mkdir('files'); - $view->file_put_contents($sourcePath, 'meh'); - $storage2->getUpdater()->update(''); - - $storage->expects($this->never()) - ->method($storageOperation); - $storage2->expects($this->once()) - ->method($storageOperation) - ->willReturnCallback( - function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) { - $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath); - $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath); - - return true; - } - ); - - $this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost); - $this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation'); - - $view->$viewOperation($sourcePath, $targetPath); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook'); - $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook'); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook'); - - $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation'); - $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation'); - } - - /** - * Test locks when moving a mount point - */ - public function testLockMoveMountPoint(): void { - self::loginAsUser('test'); - - [$mount] = $this->createTestMovableMountPoints([ - self::$user . '/files/substorage', - ]); - - $view = new View('/' . self::$user . '/files/'); - $view->mkdir('subdir'); - - $sourcePath = 'substorage'; - $targetPath = 'subdir/substorage_moved'; - - $mount->expects($this->once()) - ->method('moveMount') - ->willReturnCallback( - function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) { - $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true); - $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true); - - $lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false); - - $mount->setMountPoint($target); - - return true; - } - ); - - $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true); - $this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true); - // in pre-hook, mount point is still on $sourcePath - $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false); - // in post-hook, mount point is now on $targetPath - $this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false); - - $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation'); - $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation'); - $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation'); - - $view->rename($sourcePath, $targetPath); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook'); - - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook'); - $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation'); - $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook'); - - $this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook'); - $this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move'); - $this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook'); - - $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation'); - $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation'); - $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation'); - } - - /** - * Connect hook callbacks for hook type - * - * @param string $hookType hook type or null for none - * @param View $view view to check the lock on - * @param string $path path for which to check the lock - * @param int $lockTypePre variable to receive lock type that was active in the pre-hook - * @param int $lockTypePost variable to receive lock type that was active in the post-hook - * @param bool $onMountPoint true to check the mount point instead of the - * mounted storage - */ - private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) { - if ($hookType === null) { - return; - } - - $eventHandler = $this->getMockBuilder(TestEventHandler::class) - ->onlyMethods(['preCallback', 'postCallback']) - ->getMock(); - - $eventHandler->expects($this->any()) - ->method('preCallback') - ->willReturnCallback( - function () use ($view, $path, $onMountPoint, &$lockTypePre): void { - $lockTypePre = $this->getFileLockType($view, $path, $onMountPoint); - } - ); - $eventHandler->expects($this->any()) - ->method('postCallback') - ->willReturnCallback( - function () use ($view, $path, $onMountPoint, &$lockTypePost): void { - $lockTypePost = $this->getFileLockType($view, $path, $onMountPoint); - } - ); - - if ($hookType !== '') { - Util::connectHook( - Filesystem::CLASSNAME, - $hookType, - $eventHandler, - 'preCallback' - ); - Util::connectHook( - Filesystem::CLASSNAME, - 'post_' . $hookType, - $eventHandler, - 'postCallback' - ); - } - } - - /** - * Returns the file lock type - * - * @param View $view view - * @param string $path path - * @param bool $onMountPoint true to check the mount point instead of the - * mounted storage - * - * @return int lock type or null if file was not locked - */ - private function getFileLockType(View $view, $path, $onMountPoint = false) { - if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) { - return ILockingProvider::LOCK_EXCLUSIVE; - } elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) { - return ILockingProvider::LOCK_SHARED; - } - return null; - } - - public function testRemoveMoveableMountPoint(): void { - $mountPoint = '/' . self::$user . '/files/mount/'; - - // Mock the mount point - /** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */ - $mount = $this->createMock(TestMoveableMountPoint::class); - $mount->expects($this->once()) - ->method('getMountPoint') - ->willReturn($mountPoint); - $mount->expects($this->once()) - ->method('removeMount') - ->willReturn(true); - $mount->expects($this->any()) - ->method('getInternalPath') - ->willReturn(''); - - // Register mount - Filesystem::getMountManager()->addMount($mount); - - // Listen for events - $eventHandler = $this->getMockBuilder(TestEventHandler::class) - ->onlyMethods(['umount', 'post_umount']) - ->getMock(); - $eventHandler->expects($this->once()) - ->method('umount') - ->with([Filesystem::signal_param_path => '/mount']); - $eventHandler->expects($this->once()) - ->method('post_umount') - ->with([Filesystem::signal_param_path => '/mount']); - Util::connectHook( - Filesystem::CLASSNAME, - 'umount', - $eventHandler, - 'umount' - ); - Util::connectHook( - Filesystem::CLASSNAME, - 'post_umount', - $eventHandler, - 'post_umount' - ); - - //Delete the mountpoint - $view = new View('/' . self::$user . '/files'); - $this->assertEquals('foo', $view->rmdir('mount')); - } - - public static function mimeFilterProvider(): array { - return [ - [null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']], - ['text/plain', ['test1.txt', 'test2.txt']], - ['text/markdown', ['test3.md']], - ['text', ['test1.txt', 'test2.txt', 'test3.md']], - ]; - } - - /** - * @param string $filter - * @param string[] $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')] - public function testGetDirectoryContentMimeFilter($filter, $expected): void { - $storage1 = new Temporary(); - $root = self::getUniqueID('/'); - Filesystem::mount($storage1, [], $root . '/'); - $view = new View($root); - - $view->file_put_contents('test1.txt', 'asd'); - $view->file_put_contents('test2.txt', 'asd'); - $view->file_put_contents('test3.md', 'asd'); - $view->file_put_contents('test4.png', ''); - - $content = $view->getDirectoryContent('', $filter); - - $files = array_map(function (FileInfo $info) { - return $info->getName(); - }, $content); - sort($files); - - $this->assertEquals($expected, $files); - } - - public function testFilePutContentsClearsChecksum(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $storage->file_put_contents('foo.txt', 'bar'); - Filesystem::mount($storage, [], '/test/'); - $scanner->scan(''); - - $view = new View('/test/foo.txt'); - $view->putFileInfo('.', ['checksum' => '42']); - - $this->assertEquals('bar', $view->file_get_contents('')); - $fh = tmpfile(); - fwrite($fh, 'fooo'); - rewind($fh); - clearstatcache(); - $view->file_put_contents('', $fh); - $this->assertEquals('fooo', $view->file_get_contents('')); - $data = $view->getFileInfo('.'); - $this->assertEquals('', $data->getChecksum()); - } - - public function testDeleteGhostFile(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $cache = $storage->getCache(); - $storage->file_put_contents('foo.txt', 'bar'); - Filesystem::mount($storage, [], '/test/'); - $scanner->scan(''); - - $storage->unlink('foo.txt'); - - $this->assertTrue($cache->inCache('foo.txt')); - - $view = new View('/test'); - $rootInfo = $view->getFileInfo(''); - $this->assertEquals(3, $rootInfo->getSize()); - $view->unlink('foo.txt'); - $newInfo = $view->getFileInfo(''); - - $this->assertFalse($cache->inCache('foo.txt')); - $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag()); - $this->assertEquals(0, $newInfo->getSize()); - } - - public function testDeleteGhostFolder(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $cache = $storage->getCache(); - $storage->mkdir('foo'); - $storage->file_put_contents('foo/foo.txt', 'bar'); - Filesystem::mount($storage, [], '/test/'); - $scanner->scan(''); - - $storage->rmdir('foo'); - - $this->assertTrue($cache->inCache('foo')); - $this->assertTrue($cache->inCache('foo/foo.txt')); - - $view = new View('/test'); - $rootInfo = $view->getFileInfo(''); - $this->assertEquals(3, $rootInfo->getSize()); - $view->rmdir('foo'); - $newInfo = $view->getFileInfo(''); - - $this->assertFalse($cache->inCache('foo')); - $this->assertFalse($cache->inCache('foo/foo.txt')); - $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag()); - $this->assertEquals(0, $newInfo->getSize()); - } - - public function testCreateParentDirectories(): void { - $view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->onlyMethods([ - 'is_file', - 'file_exists', - 'mkdir', - ]) - ->getMock(); - - $view->expects($this->exactly(3)) - ->method('is_file') - ->willReturnMap([ - ['/new', false], - ['/new/folder', false], - ['/new/folder/structure', false], - ]); - $view->expects($this->exactly(3)) - ->method('file_exists') - ->willReturnMap([ - ['/new', true], - ['/new/folder', false], - ['/new/folder/structure', false], - ]); - - $calls = ['/new/folder', '/new/folder/structure']; - $view->expects($this->exactly(2)) - ->method('mkdir') - ->willReturnCallback(function ($dir) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $dir); - }); - - $this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure'])); - } - - public function testCreateParentDirectoriesWithExistingFile(): void { - $view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->onlyMethods([ - 'is_file', - 'file_exists', - 'mkdir', - ]) - ->getMock(); - - $view - ->expects($this->once()) - ->method('is_file') - ->with('/file.txt') - ->willReturn(true); - $this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure'])); - } - - public function testCacheExtension(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $storage->file_put_contents('foo.txt', 'bar'); - $scanner->scan(''); - - Filesystem::mount($storage, [], '/test/'); - $view = new View('/test'); - - $info = $view->getFileInfo('/foo.txt'); - $this->assertEquals(0, $info->getUploadTime()); - $this->assertEquals(0, $info->getCreationTime()); - - $view->putFileInfo('/foo.txt', ['upload_time' => 25]); - - $info = $view->getFileInfo('/foo.txt'); - $this->assertEquals(25, $info->getUploadTime()); - $this->assertEquals(0, $info->getCreationTime()); - } - - public function testFopenGone(): void { - $storage = new Temporary([]); - $scanner = $storage->getScanner(); - $storage->file_put_contents('foo.txt', 'bar'); - $scanner->scan(''); - $cache = $storage->getCache(); - - Filesystem::mount($storage, [], '/test/'); - $view = new View('/test'); - - $storage->unlink('foo.txt'); - - $this->assertTrue($cache->inCache('foo.txt')); - - $this->assertFalse($view->fopen('foo.txt', 'r')); - - $this->assertFalse($cache->inCache('foo.txt')); - } - - /** - * Regression test: checks that when fopen fails, the view checks the correct path and removes the cache entry. - */ - public function testFopenGoneViewBelowMountRoot(): void { - $storage = new Temporary([]); - $storage->mkdir('sub'); - $storage->file_put_contents('foo.txt', 'bar'); - $storage->file_put_contents('sub/foo.txt', 'bar'); - $storage->getScanner()->scan(''); - $cache = $storage->getCache(); - - Filesystem::mount($storage, [], '/test/'); - $view = new View('/test/sub'); - - $storage->unlink('sub/foo.txt'); - - $this->assertTrue($cache->inCache('sub/foo.txt')); - $this->assertFalse($view->fopen('foo.txt', 'r')); - $this->assertFalse($cache->inCache('sub/foo.txt')); - } - - public function testFopenFailureOnExistingFileDoesNotPropagate(): void { - /** @var Temporary&MockObject $storage */ - $storage = $this->getMockBuilder(Temporary::class) - ->setConstructorArgs([[]]) - ->onlyMethods(['fopen']) - ->getMock(); - $storage->method('fopen') - ->willReturn(false); - $storage->mkdir('sub'); - $storage->file_put_contents('sub/foo.txt', 'bar'); - $storage->getScanner()->scan(''); - $cache = $storage->getCache(); - $cache->update($cache->getId('sub'), ['mtime' => 100]); - - Filesystem::mount($storage, [], '/test/'); - $view = new View('/test/sub'); - - $this->assertFalse($view->fopen('foo.txt', 'r')); - $this->assertTrue($cache->inCache('sub/foo.txt')); - $this->assertEquals(100, $cache->get('sub')->getMTime()); - } - - public function testMountpointParentsCreated(): void { - $storage1 = $this->getTestStorage(); - Filesystem::mount($storage1, [], '/'); - - $storage2 = $this->getTestStorage(); - Filesystem::mount($storage2, [], '/A/B/C'); - - $rootView = new View(''); - - $folderData = $rootView->getDirectoryContent('/'); - usort($folderData, fn (FileInfo $a, FileInfo $b) => $a->getName() <=> $b->getName()); - $this->assertCount(4, $folderData); - - $this->assertEquals('A', $folderData[0]['name']); - $this->assertEquals('folder', $folderData[1]['name']); - $this->assertEquals('foo.png', $folderData[2]['name']); - $this->assertEquals('foo.txt', $folderData[3]['name']); - - $folderData = $rootView->getDirectoryContent('/A'); - $this->assertCount(1, $folderData); - $this->assertEquals('B', $folderData[0]['name']); - - $folderData = $rootView->getDirectoryContent('/A/B'); - $this->assertCount(1, $folderData); - $this->assertEquals('C', $folderData[0]['name']); - - $folderData = $rootView->getDirectoryContent('/A/B/C'); - usort($folderData, fn (FileInfo $a, FileInfo $b) => $a->getName() <=> $b->getName()); - $this->assertCount(3, $folderData); - $this->assertEquals('folder', $folderData[0]['name']); - $this->assertEquals('foo.png', $folderData[1]['name']); - $this->assertEquals('foo.txt', $folderData[2]['name']); - } - - public function testCopyPreservesContent() { - $viewUser1 = new View('/' . 'userId' . '/files'); - $viewUser1->mkdir(''); - $viewUser1->file_put_contents('foo.txt', 'foo'); - $viewUser1->copy('foo.txt', 'bar.txt'); - $this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt')); - } -} --- a/tests/lib/FilesMetadata/FilesMetadataManagerTest.php +++ /dev/null @@ -1,132 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace Test\FilesMetadata; - -use OC\BackgroundJob\JobList; -use OC\Files\Storage\Temporary; -use OC\FilesMetadata\FilesMetadataManager; -use OC\FilesMetadata\Service\IndexRequestService; -use OC\FilesMetadata\Service\MetadataRequestService; -use OCP\DB\QueryBuilder\IExpressionBuilder; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Folder; -use OCP\Files\IRootFolder; -use OCP\FilesMetadata\AMetadataEvent; -use OCP\IAppConfig; -use OCP\IDBConnection; -use OCP\Server; -use Psr\Log\LoggerInterface; -use Test\TestCase; -use Test\Traits\MountProviderTrait; -use Test\Traits\UserTrait; - -#[\PHPUnit\Framework\Attributes\Group('DB')] -class FilesMetadataManagerTest extends TestCase { - use UserTrait; - use MountProviderTrait; - - private IEventDispatcher $eventDispatcher; - private JobList $jobList; - private IAppConfig $appConfig; - private LoggerInterface $logger; - private MetadataRequestService $metadataRequestService; - private IndexRequestService $indexRequestService; - private FilesMetadataManager $manager; - private IDBConnection $connection; - private Folder $userFolder; - private array $metadata = []; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->jobList = $this->createMock(JobList::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->eventDispatcher->method('dispatchTyped')->willReturnCallback(function (Event $event): void { - if ($event instanceof AMetadataEvent) { - $name = $event->getNode()->getName(); - if (isset($this->metadata[$name])) { - $meta = $event->getMetadata(); - foreach ($this->metadata[$name] as $key => $value) { - $meta->setString($key, $value); - } - } - } - }); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->connection = Server::get(IDBConnection::class); - $this->metadataRequestService = new MetadataRequestService($this->connection, $this->logger); - $this->indexRequestService = new IndexRequestService($this->connection, $this->logger); - $this->manager = new FilesMetadataManager( - $this->eventDispatcher, - $this->jobList, - $this->appConfig, - $this->logger, - $this->metadataRequestService, - $this->indexRequestService, - ); - - $this->createUser('metatest', ''); - $this->registerMount('metatest', new Temporary([]), '/metatest'); - - $rootFolder = Server::get(IRootFolder::class); - $this->userFolder = $rootFolder->getUserFolder('metatest'); - } - - public function testRefreshMetadata(): void { - $this->metadata['test.txt'] = [ - 'istest' => 'yes' - ]; - $file = $this->userFolder->newFile('test.txt', 'test'); - $stored = $this->manager->refreshMetadata($file); - $this->assertEquals($file->getId(), $stored->getFileId()); - $this->assertEquals('yes', $stored->getString('istest')); - - $retrieved = $this->manager->getMetadata($file->getId()); - $this->assertEquals($file->getId(), $retrieved->getFileId()); - $this->assertEquals('yes', $retrieved->getString('istest')); - } - - public function testDropMetadataForFilesChunking(): void { - $connection = $this->createMock(IDBConnection::class); - $qb = $this->createMock(IQueryBuilder::class); - $expr = $this->createMock(IExpressionBuilder::class); - - $connection->method('getQueryBuilder')->willReturn($qb); - $qb->method('expr')->willReturn($expr); - $qb->method('delete')->willReturnSelf(); - $qb->method('where')->willReturnSelf(); - $qb->method('hintShardKey')->willReturnSelf(); - - $fileIds = range(1, IQueryBuilder::MAX_IN_PARAMETERS * 2 + 1); - $expectedChunks = array_chunk($fileIds, IQueryBuilder::MAX_IN_PARAMETERS); - $boundChunks = []; - - $qb->expects($this->exactly(count($expectedChunks))) - ->method('createNamedParameter') - ->willReturnCallback(function (array $chunk, $type) use (&$boundChunks): string { - $this->assertSame(IQueryBuilder::PARAM_INT_ARRAY, $type); - $boundChunks[] = $chunk; - return ':param'; - }); - - $qb->expects($this->exactly(count($expectedChunks))) - ->method('executeStatement') - ->willReturn(1); - - $service = new MetadataRequestService($connection, $this->logger); - $service->dropMetadataForFiles(123, $fileIds); - - $this->assertSame($expectedChunks, $boundChunks); - } -} --- a/tests/lib/FilesTest.php +++ /dev/null @@ -1,42 +0,0 @@ -getTemporaryFolder() . '/'; - mkdir($baseDir . 'a/b/c/d/e', 0777, true); - mkdir($baseDir . 'a/b/c1/d/e', 0777, true); - mkdir($baseDir . 'a/b/c2/d/e', 0777, true); - mkdir($baseDir . 'a/b1/c1/d/e', 0777, true); - mkdir($baseDir . 'a/b2/c1/d/e', 0777, true); - mkdir($baseDir . 'a/b3/c1/d/e', 0777, true); - mkdir($baseDir . 'a1/b', 0777, true); - mkdir($baseDir . 'a1/c', 0777, true); - file_put_contents($baseDir . 'a/test.txt', 'Hello file!'); - file_put_contents($baseDir . 'a/b1/c1/test one.txt', 'Hello file one!'); - file_put_contents($baseDir . 'a1/b/test two.txt', 'Hello file two!'); - Files::rmdirr($baseDir . 'a'); - - $this->assertFalse(file_exists($baseDir . 'a')); - $this->assertTrue(file_exists($baseDir . 'a1')); - - Files::rmdirr($baseDir); - $this->assertFalse(file_exists($baseDir)); - } -} --- a/tests/lib/GlobalScale/ConfigTest.php +++ /dev/null @@ -1,76 +0,0 @@ -config = $this->createMock(IConfig::class); - } - - /** - * @param array $mockMethods - * @return Config|\PHPUnit\Framework\MockObject\MockObject - */ - public function getInstance($mockMethods = []) { - if (!empty($mockMethods)) { - return $this->getMockBuilder(Config::class) - ->setConstructorArgs([$this->config]) - ->onlyMethods($mockMethods) - ->getMock(); - } - - return new Config($this->config); - } - - public function testIsGlobalScaleEnabled(): void { - $gsConfig = $this->getInstance(); - $this->config->expects($this->once())->method('getSystemValueBool') - ->with('gs.enabled', false)->willReturn(true); - - $result = $gsConfig->isGlobalScaleEnabled(); - - $this->assertTrue($result); - } - - /** - * - * @param bool $gsEnabled - * @param string $gsFederation - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestOnlyInternalFederation')] - public function testOnlyInternalFederation($gsEnabled, $gsFederation, $expected): void { - $gsConfig = $this->getInstance(['isGlobalScaleEnabled']); - - $gsConfig->expects($this->any())->method('isGlobalScaleEnabled')->willReturn($gsEnabled); - - $this->config->expects($this->any())->method('getSystemValueString') - ->with('gs.federation', 'internal')->willReturn($gsFederation); - - $this->assertSame($expected, $gsConfig->onlyInternalFederation()); - } - - public static function dataTestOnlyInternalFederation(): array { - return [ - [true, 'global', false], - [true, 'internal', true], - [false, 'global', false], - [false, 'internal', false] - ]; - } -} --- a/tests/lib/Group/Backend.php +++ /dev/null @@ -1,144 +0,0 @@ -getUniqueID('test_'); - } else { - return $name; - } - } - - /** - * get a new unique user name - * test cases can override this in order to clean up created user - * - * @return string - */ - public function getUserName() { - return $this->getUniqueID('test_'); - } - - public function testAddRemove(): void { - //get the number of groups we start with, in case there are exising groups - $startCount = count($this->backend->getGroups()); - - $name1 = $this->getGroupName(); - $name2 = $this->getGroupName(); - $this->backend->createGroup($name1); - $count = count($this->backend->getGroups()) - $startCount; - $this->assertEquals(1, $count); - $this->assertTrue((array_search($name1, $this->backend->getGroups()) !== false)); - $this->assertFalse((array_search($name2, $this->backend->getGroups()) !== false)); - $this->backend->createGroup($name2); - $count = count($this->backend->getGroups()) - $startCount; - $this->assertEquals(2, $count); - $this->assertTrue((array_search($name1, $this->backend->getGroups()) !== false)); - $this->assertTrue((array_search($name2, $this->backend->getGroups()) !== false)); - - $this->backend->deleteGroup($name2); - $count = count($this->backend->getGroups()) - $startCount; - $this->assertEquals(1, $count); - $this->assertTrue((array_search($name1, $this->backend->getGroups()) !== false)); - $this->assertFalse((array_search($name2, $this->backend->getGroups()) !== false)); - } - - public function testUser(): void { - $group1 = $this->getGroupName(); - $group2 = $this->getGroupName(); - $this->backend->createGroup($group1); - $this->backend->createGroup($group2); - - $user1 = $this->getUserName(); - $user2 = $this->getUserName(); - - $this->assertFalse($this->backend->inGroup($user1, $group1)); - $this->assertFalse($this->backend->inGroup($user2, $group1)); - $this->assertFalse($this->backend->inGroup($user1, $group2)); - $this->assertFalse($this->backend->inGroup($user2, $group2)); - - $this->assertTrue($this->backend->addToGroup($user1, $group1)); - - $this->assertTrue($this->backend->inGroup($user1, $group1)); - $this->assertFalse($this->backend->inGroup($user2, $group1)); - $this->assertFalse($this->backend->inGroup($user1, $group2)); - $this->assertFalse($this->backend->inGroup($user2, $group2)); - - $this->assertFalse($this->backend->addToGroup($user1, $group1)); - - $this->assertEquals([$user1], $this->backend->usersInGroup($group1)); - $this->assertEquals([], $this->backend->usersInGroup($group2)); - - $this->assertEquals([$group1], $this->backend->getUserGroups($user1)); - $this->assertEquals([], $this->backend->getUserGroups($user2)); - - $this->backend->deleteGroup($group1); - $this->assertEquals([], $this->backend->getUserGroups($user1)); - $this->assertEquals([], $this->backend->usersInGroup($group1)); - $this->assertFalse($this->backend->inGroup($user1, $group1)); - } - - public function testSearchGroups(): void { - $name1 = $this->getGroupName('foobarbaz'); - $name2 = $this->getGroupName('bazfoobarfoo'); - $name3 = $this->getGroupName('notme'); - - $this->backend->createGroup($name1); - $this->backend->createGroup($name2); - $this->backend->createGroup($name3); - - $result = $this->backend->getGroups('foobar'); - $this->assertSame(2, count($result)); - } - - public function testSearchUsers(): void { - $group = $this->getGroupName(); - $this->backend->createGroup($group); - - $name1 = 'foobarbaz'; - $name2 = 'bazbarfoo'; - $name3 = 'notme'; - - $this->backend->addToGroup($name1, $group); - $this->backend->addToGroup($name2, $group); - $this->backend->addToGroup($name3, $group); - - $result = $this->backend->usersInGroup($group, 'bar'); - $this->assertSame(2, count($result)); - - $result = $this->backend->countUsersInGroup($group, 'bar'); - $this->assertSame(2, $result); - } - - public function testAddDouble(): void { - $group = $this->getGroupName(); - - $this->backend->createGroup($group); - $this->backend->createGroup($group); - - $this->addToAssertionCount(1); - } -} --- a/tests/lib/Group/DatabaseTest.php +++ /dev/null @@ -1,63 +0,0 @@ -groups[] = $name; - return $name; - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->backend = new Database(); - } - - #[\Override] - protected function tearDown(): void { - foreach ($this->groups as $group) { - $this->backend->deleteGroup($group); - } - parent::tearDown(); - } - - public function testAddDoubleNoCache(): void { - $group = $this->getGroupName(); - - $this->backend->createGroup($group); - - $backend = new Database(); - $this->assertNull($backend->createGroup($group)); - } - - public function testAddLongGroupName(): void { - $groupName = $this->getUniqueID('test_', 100); - - $gidCreated = $this->backend->createGroup($groupName); - $this->assertEquals(64, strlen($gidCreated)); - - $group = $this->backend->getGroupDetails($gidCreated); - $this->assertEquals(['displayName' => $groupName], $group); - } -} --- a/tests/lib/Group/Dummy.php +++ /dev/null @@ -1,24 +0,0 @@ -backend = new \Test\Util\Group\Dummy(); - } -} --- a/tests/lib/Group/GroupTest.php +++ /dev/null @@ -1,514 +0,0 @@ -dispatcher = $this->createMock(IEventDispatcher::class); - } - - /** - * @param string $uid - * @param \OC\User\Backend $backend - * @return User - */ - private function newUser($uid, \OC\User\Backend $backend) { - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn($uid); - $user->method('getBackend') - ->willReturn($backend); - - return $user; - } - - /** - * @return \OC\User\Manager - */ - protected function getUserManager() { - $userManager = $this->getMockBuilder('\OC\User\Manager') - ->disableOriginalConstructor() - ->getMock(); - $backend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $user1 = $this->newUser('user1', $backend); - $user2 = $this->newUser('user2', $backend); - $user3 = $this->newUser('user3', $backend); - $userManager->expects($this->any()) - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ['user3', $user3] - ]); - return $userManager; - } - - public function testGetUsersSingleBackend(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('usersInGroup') - ->with('group1') - ->willReturn(['user1', 'user2']); - - $users = $group->getUsers(); - - $this->assertEquals(2, count($users)); - $user1 = $users['user1']; - $user2 = $users['user2']; - $this->assertEquals('user1', $user1->getUID()); - $this->assertEquals('user2', $user2->getUID()); - } - - public function testGetUsersMultipleBackends(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('usersInGroup') - ->with('group1') - ->willReturn(['user1', 'user2']); - - $backend2->expects($this->once()) - ->method('usersInGroup') - ->with('group1') - ->willReturn(['user2', 'user3']); - - $users = $group->getUsers(); - - $this->assertEquals(3, count($users)); - $user1 = $users['user1']; - $user2 = $users['user2']; - $user3 = $users['user3']; - $this->assertEquals('user1', $user1->getUID()); - $this->assertEquals('user2', $user2->getUID()); - $this->assertEquals('user3', $user3->getUID()); - } - - public function testInGroupSingleBackend(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - - $this->assertTrue($group->inGroup($this->newUser('user1', $userBackend))); - } - - public function testInGroupMultipleBackends(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder(\OC\User\Backend::class) - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(false); - - $backend2->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - - $this->assertTrue($group->inGroup($this->newUser('user1', $userBackend))); - } - - public function testAddUser(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(false); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend->expects($this->once()) - ->method('addToGroup') - ->with('user1', 'group1'); - - $group->addUser($this->newUser('user1', $userBackend)); - } - - public function testAddUserAlreadyInGroup(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend->expects($this->never()) - ->method('addToGroup'); - - $group->addUser($this->newUser('user1', $userBackend)); - } - - public function testRemoveUser(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend->expects($this->once()) - ->method('removeFromGroup') - ->with('user1', 'group1'); - - $group->removeUser($this->newUser('user1', $userBackend)); - } - - public function testRemoveUserNotInGroup(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder(\OC\User\Backend::class) - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(false); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend->expects($this->never()) - ->method('removeFromGroup'); - - $group->removeUser($this->newUser('user1', $userBackend)); - } - - public function testRemoveUserMultipleBackends(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $userBackend = $this->getMockBuilder('\OC\User\Backend') - ->disableOriginalConstructor() - ->getMock(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - $backend1->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend1->expects($this->once()) - ->method('removeFromGroup') - ->with('user1', 'group1'); - - $backend2->expects($this->once()) - ->method('inGroup') - ->with('user1', 'group1') - ->willReturn(true); - $backend2->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend2->expects($this->once()) - ->method('removeFromGroup') - ->with('user1', 'group1'); - - $group->removeUser($this->newUser('user1', $userBackend)); - } - - public function testSearchUsers(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('searchInGroup') - ->with('group1', '2') - ->willReturn(['user2' => new User('user2', null, $this->dispatcher)]); - - $users = $group->searchUsers('2'); - - $this->assertEquals(1, count($users)); - $user2 = reset($users); - $this->assertEquals('user2', $user2->getUID()); - } - - public function testSearchUsersMultipleBackends(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('searchInGroup') - ->with('group1', '2') - ->willReturn(['user2' => new User('user2', null, $this->dispatcher)]); - $backend2->expects($this->once()) - ->method('searchInGroup') - ->with('group1', '2') - ->willReturn(['user2' => new User('user2', null, $this->dispatcher)]); - - $users = $group->searchUsers('2'); - - $this->assertEquals(1, count($users)); - $user2 = reset($users); - $this->assertEquals('user2', $user2->getUID()); - } - - public function testSearchUsersLimitAndOffset(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('searchInGroup') - ->with('group1', 'user', 1, 1) - ->willReturn(['user2' => new User('user2', null, $this->dispatcher)]); - - $users = $group->searchUsers('user', 1, 1); - - $this->assertEquals(1, count($users)); - $user2 = reset($users); - $this->assertEquals('user2', $user2->getUID()); - } - - public function testSearchUsersMultipleBackendsLimitAndOffset(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('searchInGroup') - ->with('group1', 'user', 2, 1) - ->willReturn(['user2' => new User('user2', null, $this->dispatcher)]); - $backend2->expects($this->once()) - ->method('searchInGroup') - ->with('group1', 'user', 2, 1) - ->willReturn(['user1' => new User('user1', null, $this->dispatcher)]); - - $users = $group->searchUsers('user', 2, 1); - - $this->assertEquals(2, count($users)); - $user2 = reset($users); - $user1 = next($users); - $this->assertEquals('user2', $user2->getUID()); - $this->assertEquals('user1', $user1->getUID()); - } - - public function testCountUsers(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('countUsersInGroup') - ->with('group1', '2') - ->willReturn(3); - - $backend1->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $users = $group->count('2'); - - $this->assertSame(3, $users); - } - - public function testCountUsersMultipleBackends(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $backend2 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $userManager); - - $backend1->expects($this->once()) - ->method('countUsersInGroup') - ->with('group1', '2') - ->willReturn(3); - $backend1->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $backend2->expects($this->once()) - ->method('countUsersInGroup') - ->with('group1', '2') - ->willReturn(4); - $backend2->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $users = $group->count('2'); - - $this->assertSame(7, $users); - } - - public function testCountUsersNoMethod(): void { - $backend1 = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend1], $this->dispatcher, $userManager); - - $backend1->expects($this->never()) - ->method('countUsersInGroup'); - $backend1->expects($this->any()) - ->method('implementsActions') - ->willReturn(false); - - $users = $group->count('2'); - - $this->assertSame(false, $users); - } - - public function testSetDisplayNameDispatchesOldValue(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - - $dispatcher = $this->createMock(IEventDispatcher::class); - $invocation = 0; - $dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$invocation): void { - $invocation++; - if ($invocation === 1) { - $this->assertInstanceOf(BeforeGroupChangedEvent::class, $event); - $this->assertSame('displayName', $event->getFeature()); - $this->assertSame('New Name', $event->getValue()); - $this->assertSame('Old Name', $event->getOldValue()); - return; - } - - $this->assertInstanceOf(GroupChangedEvent::class, $event); - $this->assertSame('displayName', $event->getFeature()); - $this->assertSame('New Name', $event->getValue()); - $this->assertSame('Old Name', $event->getOldValue()); - }); - - $backend->expects($this->once()) - ->method('setDisplayName') - ->with('group1', 'New Name') - ->willReturn(true); - - $group = new Group('group1', [$backend], $dispatcher, $userManager, null, 'Old Name'); - $this->assertTrue($group->setDisplayName('New Name')); - } - - public function testDelete(): void { - $backend = $this->getMockBuilder('OC\Group\Database') - ->disableOriginalConstructor() - ->getMock(); - $userManager = $this->getUserManager(); - $group = new Group('group1', [$backend], $this->dispatcher, $userManager); - - $backend->expects($this->once()) - ->method('deleteGroup') - ->with('group1'); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturn(true); - - $group->delete(); - } -} --- a/tests/lib/Group/HideFromCollaborationTest.php +++ /dev/null @@ -1,53 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - } - - public function testHideFromCollaboration(): void { - // Arrange - $backend1 = $this->createMock(HideFromCollaborationBackendTest::class); - $backend1->method('hideGroup') - ->willReturn(false); - $backend2 = $this->createMock(HideFromCollaborationBackendTest::class); - $backend2->method('hideGroup') - ->willReturn(true); - $group = new Group('group1', [$backend1, $backend2], $this->dispatcher, $this->userManager); - - // Act - $result = $group->hideFromCollaboration(); - - // Assert - $this->assertTrue($result); - } -} --- a/tests/lib/Group/ManagerTest.php +++ /dev/null @@ -1,995 +0,0 @@ -userManager = $this->createMock(Manager::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->cache = $this->createMock(ICacheFactory::class); - $this->userSession = $this->createMock(IUserSession::class); - - $this->remoteIpAddress = $this->createMock(IRemoteAddress::class); - $this->remoteIpAddress->method('allowsAdminActions')->willReturn(true); - } - - private function getTestUser($userId) { - $mockUser = $this->createMock(IUser::class); - $mockUser->expects($this->any()) - ->method('getUID') - ->willReturn($userId); - $mockUser->expects($this->any()) - ->method('getDisplayName') - ->willReturn($userId); - return $mockUser; - } - - /** - * @param null|int $implementedActions - * @return \PHPUnit\Framework\MockObject\MockObject - */ - private function getTestBackend($implementedActions = null) { - if ($implementedActions === null) { - $implementedActions - = GroupInterface::ADD_TO_GROUP - | GroupInterface::REMOVE_FROM_GOUP - | GroupInterface::COUNT_USERS - | GroupInterface::CREATE_GROUP - | GroupInterface::DELETE_GROUP; - } - // need to declare it this way due to optional methods - // thanks to the implementsActions logic - $backend = $this->getMockBuilder(TestBackend::class) - ->disableOriginalConstructor() - ->onlyMethods([ - 'getGroupDetails', - 'implementsActions', - 'getUserGroups', - 'inGroup', - 'getGroups', - 'groupExists', - 'groupsExists', - 'usersInGroup', - 'createGroup', - 'addToGroup', - 'removeFromGroup', - 'searchInGroup', - ]) - ->getMock(); - $backend->expects($this->any()) - ->method('implementsActions') - ->willReturnCallback(function ($actions) use ($implementedActions) { - return (bool)($actions & $implementedActions); - }); - return $backend; - } - - public function testGet(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $group = $manager->get('group1'); - $this->assertNotNull($group); - $this->assertEquals('group1', $group->getGID()); - } - - public function testGetNoBackend(): void { - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - - $this->assertNull($manager->get('group1')); - } - - public function testGetNotExists(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('groupExists') - ->with('group1') - ->willReturn(false); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $this->assertNull($manager->get('group1')); - } - - public function testGetDeleted(): void { - $backend = new \Test\Util\Group\Dummy(); - $backend->createGroup('group1'); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $group = $manager->get('group1'); - $group->delete(); - $this->assertNull($manager->get('group1')); - } - - public function testGetMultipleBackends(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend1 - */ - $backend1 = $this->getTestBackend(); - $backend1->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(false); - - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend2 - */ - $backend2 = $this->getTestBackend(); - $backend2->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend1); - $manager->addBackend($backend2); - - $group = $manager->get('group1'); - $this->assertNotNull($group); - $this->assertEquals('group1', $group->getGID()); - } - - public function testCreate(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend */ - $backendGroupCreated = false; - $backend = $this->getTestBackend(); - $backend->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturnCallback(function () use (&$backendGroupCreated) { - return $backendGroupCreated; - }); - $backend->expects($this->once()) - ->method('createGroup') - ->willReturnCallback(function () use (&$backendGroupCreated) { - $backendGroupCreated = true; - return true; - }); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $group = $manager->createGroup('group1'); - $this->assertEquals('group1', $group->getGID()); - } - - public function testCreateFailure(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend */ - $backendGroupCreated = false; - $backend = $this->getTestBackend( - GroupInterface::ADD_TO_GROUP - | GroupInterface::REMOVE_FROM_GOUP - | GroupInterface::COUNT_USERS - | GroupInterface::CREATE_GROUP - | GroupInterface::DELETE_GROUP - | GroupInterface::GROUP_DETAILS - ); - $backend->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(false); - $backend->expects($this->once()) - ->method('createGroup') - ->willReturn(false); - $backend->expects($this->once()) - ->method('getGroupDetails') - ->willReturn([]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $group = $manager->createGroup('group1'); - $this->assertEquals(null, $group); - } - - public function testCreateTooLong(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend */ - $backendGroupCreated = false; - $backend = $this->getTestBackend( - GroupInterface::ADD_TO_GROUP - | GroupInterface::REMOVE_FROM_GOUP - | GroupInterface::COUNT_USERS - | GroupInterface::CREATE_GROUP - | GroupInterface::DELETE_GROUP - | GroupInterface::GROUP_DETAILS - ); - $groupName = str_repeat('x', 256); - $backend->expects($this->any()) - ->method('groupExists') - ->with($groupName) - ->willReturn(false); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $this->expectException(\Exception::class); - $group = $manager->createGroup($groupName); - } - - public function testCreateExists(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend */ - $backend = $this->getTestBackend(); - $backend->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(true); - $backend->expects($this->never()) - ->method('createGroup'); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $group = $manager->createGroup('group1'); - $this->assertEquals('group1', $group->getGID()); - } - - public function testSearch(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('getGroups') - ->with('1') - ->willReturn(['group1']); - $backend->expects($this->once()) - ->method('getGroupDetails') - ->willReturnMap([ - ['group1', ['displayName' => 'group1']], - ]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $groups = $manager->search('1'); - $this->assertCount(1, $groups); - $group1 = reset($groups); - $this->assertEquals('group1', $group1->getGID()); - } - - public function testSearchMultipleBackends(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend1 - */ - $backend1 = $this->getTestBackend(); - $backend1->expects($this->once()) - ->method('getGroups') - ->with('1') - ->willReturn(['group1']); - $backend1->expects($this->any()) - ->method('getGroupDetails') - ->willReturnMap([ - ['group1', ['displayName' => 'group1']], - ['group12', []], - ]); - - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend2 - */ - $backend2 = $this->getTestBackend(); - $backend2->expects($this->once()) - ->method('getGroups') - ->with('1') - ->willReturn(['group12', 'group1']); - $backend2->expects($this->any()) - ->method('getGroupDetails') - ->willReturnMap([ - ['group12', ['displayName' => 'group12']], - ['group1', ['displayName' => 'group1']], - ]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend1); - $manager->addBackend($backend2); - - $groups = $manager->search('1'); - $this->assertCount(2, $groups); - $group1 = reset($groups); - $group12 = next($groups); - $this->assertEquals('group1', $group1->getGID()); - $this->assertEquals('group12', $group12->getGID()); - } - - public function testSearchMultipleBackendsLimitAndOffset(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend1 - */ - $backend1 = $this->getTestBackend(); - $backend1->expects($this->once()) - ->method('getGroups') - ->with('1', 2, 1) - ->willReturn(['group1']); - $backend1->expects($this->any()) - ->method('getGroupDetails') - ->willReturnMap([ - [1, []], - [2, []], - ['group1', ['displayName' => 'group1']], - ['group12', []], - ]); - - /** - * @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend2 - */ - $backend2 = $this->getTestBackend(); - $backend2->expects($this->once()) - ->method('getGroups') - ->with('1', 2, 1) - ->willReturn(['group12']); - $backend2->expects($this->any()) - ->method('getGroupDetails') - ->willReturnMap([ - [1, []], - [2, []], - ['group1', []], - ['group12', ['displayName' => 'group12']], - ]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend1); - $manager->addBackend($backend2); - - $groups = $manager->search('1', 2, 1); - $this->assertCount(2, $groups); - $group1 = reset($groups); - $group12 = next($groups); - $this->assertEquals('group1', $group1->getGID()); - $this->assertEquals('group12', $group12->getGID()); - } - - public function testSearchResultExistsButGroupDoesNot(): void { - /** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend */ - $backend = $this->createMock(Database::class); - $backend->expects($this->once()) - ->method('getGroups') - ->with('1') - ->willReturn(['group1']); - $backend->expects($this->never()) - ->method('groupExists'); - $backend->expects($this->once()) - ->method('getGroupsDetails') - ->with(['group1']) - ->willReturn([]); - - /** @var \OC\User\Manager $userManager */ - $userManager = $this->createMock(Manager::class); - - $manager = new \OC\Group\Manager($userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $groups = $manager->search('1'); - $this->assertEmpty($groups); - } - - public function testGetUserGroups(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1']); - $backend->expects($this->any()) - ->method('getGroupDetails') - ->with('group1') - ->willReturn(['displayName' => 'group1']); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $groups = $manager->getUserGroups($this->getTestUser('user1')); - $this->assertCount(1, $groups); - $group1 = reset($groups); - $this->assertEquals('group1', $group1->getGID()); - } - - public function testGetUserGroupIds(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->method('getUserGroups') - ->with('myUID') - ->willReturn(['123', 'abc']); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - /** @var User|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('myUID'); - - $groups = $manager->getUserGroupIds($user); - $this->assertCount(2, $groups); - - foreach ($groups as $group) { - $this->assertIsString($group); - } - } - - public function testGetUserGroupsWithDeletedGroup(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->createMock(Database::class); - $backend->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1']); - $backend->expects($this->any()) - ->method('getGroupsDetails') - ->with(['group1']) - ->willReturn(['group1' => []]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - /** @var User|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('user1'); - - $groups = $manager->getUserGroups($user); - $this->assertEmpty($groups); - } - - public function testInGroup(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1', 'admin', 'group2']); - $backend->expects($this->any()) - ->method('groupExists') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $this->assertTrue($manager->isInGroup('user1', 'group1')); - } - - public function testIsAdmin(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1', 'admin', 'group2']); - $backend->expects($this->any()) - ->method('groupExists') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $this->assertTrue($manager->isAdmin('user1')); - } - - public function testNotAdmin(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1', 'group2']); - $backend->expects($this->any()) - ->method('groupExists') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $this->assertFalse($manager->isAdmin('user1')); - } - - public function testGetUserGroupsMultipleBackends(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend1 - */ - $backend1 = $this->getTestBackend(); - $backend1->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1']); - $backend1->expects($this->any()) - ->method('getGroupDetails') - ->willReturnCallback(fn ($gid) => $gid === 'group1' ? ['displayName' => 'group1'] : []); - - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend2 - */ - $backend2 = $this->getTestBackend(); - $backend2->expects($this->once()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(['group1', 'group2']); - $backend2->expects($this->any()) - ->method('getGroupDetails') - ->willReturnCallback(fn ($gid) => ['displayName' => $gid]); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend1); - $manager->addBackend($backend2); - - $groups = $manager->getUserGroups($this->getTestUser('user1')); - $this->assertCount(2, $groups); - $group1 = reset($groups); - $group2 = next($groups); - $this->assertEquals('group1', $group1->getGID()); - $this->assertEquals('group2', $group2->getGID()); - } - - public function testDisplayNamesInGroupWithOneUserBackend(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->any()) - ->method('inGroup') - ->willReturnCallback(function ($uid, $gid) { - switch ($uid) { - case 'user1': return false; - case 'user2': return true; - case 'user3': return false; - case 'user33': return true; - default: - return null; - } - }); - - $this->userManager->expects($this->any()) - ->method('searchDisplayName') - ->with('user3') - ->willReturnCallback(function ($search, $limit, $offset) { - switch ($offset) { - case 0: return ['user3' => $this->getTestUser('user3'), - 'user33' => $this->getTestUser('user33')]; - case 2: return []; - } - return null; - }); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'user1': return $this->getTestUser('user1'); - case 'user2': return $this->getTestUser('user2'); - case 'user3': return $this->getTestUser('user3'); - case 'user33': return $this->getTestUser('user33'); - default: - return null; - } - }); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', 'user3'); - $this->assertCount(1, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertFalse(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertTrue(isset($users['user33'])); - } - - public function testDisplayNamesInGroupWithOneUserBackendWithLimitSpecified(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->any()) - ->method('inGroup') - ->willReturnCallback(function ($uid, $gid) { - switch ($uid) { - case 'user1': return false; - case 'user2': return true; - case 'user3': return false; - case 'user33': return true; - case 'user333': return true; - default: - return null; - } - }); - - $this->userManager->expects($this->any()) - ->method('searchDisplayName') - ->with('user3') - ->willReturnCallback(function ($search, $limit, $offset) { - switch ($offset) { - case 0: return ['user3' => $this->getTestUser('user3'), - 'user33' => $this->getTestUser('user33')]; - case 2: return ['user333' => $this->getTestUser('user333')]; - } - return null; - }); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'user1': return $this->getTestUser('user1'); - case 'user2': return $this->getTestUser('user2'); - case 'user3': return $this->getTestUser('user3'); - case 'user33': return $this->getTestUser('user33'); - case 'user333': return $this->getTestUser('user333'); - default: - return null; - } - }); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', 'user3', 1); - $this->assertCount(1, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertFalse(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertTrue(isset($users['user33'])); - $this->assertFalse(isset($users['user333'])); - } - - public function testDisplayNamesInGroupWithOneUserBackendWithLimitAndOffsetSpecified(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->any()) - ->method('inGroup') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'user1': return false; - case 'user2': return true; - case 'user3': return false; - case 'user33': return true; - case 'user333': return true; - default: - return null; - } - }); - - $this->userManager->expects($this->any()) - ->method('searchDisplayName') - ->with('user3') - ->willReturnCallback(function ($search, $limit, $offset) { - switch ($offset) { - case 0: - return [ - 'user3' => $this->getTestUser('user3'), - 'user33' => $this->getTestUser('user33'), - 'user333' => $this->getTestUser('user333') - ]; - } - return null; - }); - $this->userManager->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($uid) { - switch ($uid) { - case 'user1': return $this->getTestUser('user1'); - case 'user2': return $this->getTestUser('user2'); - case 'user3': return $this->getTestUser('user3'); - case 'user33': return $this->getTestUser('user33'); - case 'user333': return $this->getTestUser('user333'); - default: - return null; - } - }); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', 'user3', 1, 1); - $this->assertCount(1, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertFalse(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertFalse(isset($users['user33'])); - $this->assertTrue(isset($users['user333'])); - } - - public function testDisplayNamesInGroupWithOneUserBackendAndSearchEmpty(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject|\OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->once()) - ->method('searchInGroup') - ->with('testgroup', '', -1, 0) - ->willReturn(['user2' => $this->getTestUser('user2'), 'user33' => $this->getTestUser('user33')]); - - $this->userManager->expects($this->never())->method('get'); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', ''); - $this->assertCount(2, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertTrue(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertTrue(isset($users['user33'])); - } - - public function testDisplayNamesInGroupWithOneUserBackendAndSearchEmptyAndLimitSpecified(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->once()) - ->method('searchInGroup') - ->with('testgroup', '', 1, 0) - ->willReturn([new User('user2', null, $this->dispatcher)]); - - $this->userManager->expects($this->never())->method('get'); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', '', 1); - $this->assertCount(1, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertTrue(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertFalse(isset($users['user33'])); - } - - public function testDisplayNamesInGroupWithOneUserBackendAndSearchEmptyAndLimitAndOffsetSpecified(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->exactly(1)) - ->method('groupExists') - ->with('testgroup') - ->willReturn(true); - - $backend->expects($this->once()) - ->method('searchInGroup') - ->with('testgroup', '', 1, 1) - ->willReturn(['user33' => $this->getTestUser('user33')]); - - $this->userManager->expects($this->never())->method('get'); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $users = $manager->displayNamesInGroup('testgroup', '', 1, 1); - $this->assertCount(1, $users); - $this->assertFalse(isset($users['user1'])); - $this->assertFalse(isset($users['user2'])); - $this->assertFalse(isset($users['user3'])); - $this->assertTrue(isset($users['user33'])); - } - - public function testGetUserGroupsWithAddUser(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $expectedGroups = []; - $backend->expects($this->any()) - ->method('getUserGroups') - ->with('user1') - ->willReturnCallback(function () use (&$expectedGroups) { - return $expectedGroups; - }); - $backend->expects($this->any()) - ->method('groupExists') - ->with('group1') - ->willReturn(true); - $backend->expects($this->any()) - ->method('getGroupDetails') - ->with('group1') - ->willReturn(['displayName' => 'group1']); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - // prime cache - $user1 = $this->getTestUser('user1'); - $groups = $manager->getUserGroups($user1); - $this->assertEquals([], $groups); - - // add user - $group = $manager->get('group1'); - $group->addUser($user1); - $expectedGroups[] = 'group1'; - - // check result - $groups = $manager->getUserGroups($user1); - $this->assertCount(1, $groups); - $group1 = reset($groups); - $this->assertEquals('group1', $group1->getGID()); - } - - public function testGetUserGroupsWithRemoveUser(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $expectedGroups = ['group1']; - $backend->expects($this->any()) - ->method('getUserGroups') - ->with('user1') - ->willReturnCallback(function () use (&$expectedGroups) { - return $expectedGroups; - }); - $backend->expects($this->any()) - ->method('getGroupDetails') - ->with('group1') - ->willReturn(['displayName' => 'group1']); - $backend->expects($this->once()) - ->method('inGroup') - ->willReturn(true); - $backend->expects($this->once()) - ->method('removeFromGroup') - ->willReturn(true); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - // prime cache - $user1 = $this->getTestUser('user1'); - $groups = $manager->getUserGroups($user1); - $this->assertCount(1, $groups); - $group1 = reset($groups); - $this->assertEquals('group1', $group1->getGID()); - - // remove user - $group = $manager->get('group1'); - $group->removeUser($user1); - $expectedGroups = []; - - // check result - $groups = $manager->getUserGroups($user1); - $this->assertEquals($expectedGroups, $groups); - } - - public function testGetUserIdGroups(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend(); - $backend->expects($this->any()) - ->method('getUserGroups') - ->with('user1') - ->willReturn(null); - - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - $groups = $manager->getUserIdGroups('user1'); - $this->assertEquals([], $groups); - } - - public function testGroupDisplayName(): void { - /** - * @var \PHPUnit\Framework\MockObject\MockObject | \OC\Group\Backend $backend - */ - $backend = $this->getTestBackend( - GroupInterface::ADD_TO_GROUP - | GroupInterface::REMOVE_FROM_GOUP - | GroupInterface::COUNT_USERS - | GroupInterface::CREATE_GROUP - | GroupInterface::DELETE_GROUP - | GroupInterface::GROUP_DETAILS - ); - $backend->expects($this->any()) - ->method('getGroupDetails') - ->willReturnMap([ - ['group1', ['gid' => 'group1', 'displayName' => 'Group One']], - ['group2', ['gid' => 'group2']], - ]); - $manager = new \OC\Group\Manager($this->userManager, $this->dispatcher, $this->logger, $this->cache, $this->remoteIpAddress, $this->userSession); - $manager->addBackend($backend); - - // group with display name - $group = $manager->get('group1'); - $this->assertNotNull($group); - $this->assertEquals('group1', $group->getGID()); - $this->assertEquals('Group One', $group->getDisplayName()); - - // group without display name - $group = $manager->get('group2'); - $this->assertNotNull($group); - $this->assertEquals('group2', $group->getGID()); - $this->assertEquals('group2', $group->getDisplayName()); - } -} --- a/tests/lib/Group/MetaDataTest.php +++ /dev/null @@ -1,121 +0,0 @@ -groupManager = $this->getMockBuilder('\OC\Group\Manager') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession = $this->createMock(IUserSession::class); - $this->groupMetadata = new MetaData( - 'foo', - $this->isAdmin, - $this->isDelegatedAdmin, - $this->groupManager, - $this->userSession - ); - } - - private function getGroupMock($countCallCount = 0) { - $group = $this->getMockBuilder('\OC\Group\Group') - ->disableOriginalConstructor() - ->getMock(); - - $group->expects($this->exactly(6)) - ->method('getGID')->willReturnOnConsecutiveCalls('admin', 'admin', 'g2', 'g2', 'g3', 'g3'); - - $group->expects($this->exactly(3)) - ->method('getDisplayName')->willReturnOnConsecutiveCalls('admin', 'g2', 'g3'); - - $group->expects($this->exactly($countCallCount)) - ->method('count') - ->with('')->willReturnOnConsecutiveCalls(2, 3, 5); - - return $group; - } - - public function testGet(): void { - $group = $this->getGroupMock(); - $groups = array_fill(0, 3, $group); - - $this->groupManager->expects($this->once()) - ->method('search') - ->with('') - ->willReturn($groups); - - [$adminGroups, $ordinaryGroups] = $this->groupMetadata->get(); - - $this->assertSame(1, count($adminGroups)); - $this->assertSame(2, count($ordinaryGroups)); - - $this->assertSame('g2', $ordinaryGroups[0]['name']); - // user count is not loaded - $this->assertSame(0, $ordinaryGroups[0]['usercount']); - } - - public function testGetWithSorting(): void { - $this->groupMetadata->setSorting(1); - $group = $this->getGroupMock(3); - $groups = array_fill(0, 3, $group); - - $this->groupManager->expects($this->once()) - ->method('search') - ->with('') - ->willReturn($groups); - - [$adminGroups, $ordinaryGroups] = $this->groupMetadata->get(); - - $this->assertSame(1, count($adminGroups)); - $this->assertSame(2, count($ordinaryGroups)); - - $this->assertSame('g3', $ordinaryGroups[0]['name']); - $this->assertSame(5, $ordinaryGroups[0]['usercount']); - } - - public function testGetWithCache(): void { - $group = $this->getGroupMock(); - $groups = array_fill(0, 3, $group); - - $this->groupManager->expects($this->once()) - ->method('search') - ->with('') - ->willReturn($groups); - - //two calls, if caching fails call counts for group and groupmanager - //are exceeded - $this->groupMetadata->get(); - $this->groupMetadata->get(); - } - - //get() does not need to be tested with search parameters, because they are - //solely and only passed to GroupManager and Group. - - public function testGetGroupsAsAdmin(): void { - $this->groupManager - ->expects($this->once()) - ->method('search') - ->with('Foo') - ->willReturn(['DummyValue']); - - $expected = ['DummyValue']; - $this->assertSame($expected, $this->invokePrivate($this->groupMetadata, 'getGroups', ['Foo'])); - } -} --- a/tests/lib/HelperStorageTest.php +++ /dev/null @@ -1,252 +0,0 @@ -user = $this->getUniqueID('user_'); - $this->createUser($this->user, $this->user); - $this->savedQuotaIncludeExternalStorage = $this->getIncludeExternalStorage(); - - Filesystem::tearDown(); - \OC_User::setUserId($this->user); - Filesystem::init($this->user, '/' . $this->user . '/files'); - - /** @var IMountManager $manager */ - $manager = Server::get(IMountManager::class); - $manager->removeMount('/' . $this->user); - - $this->storageMock = null; - } - - #[\Override] - protected function tearDown(): void { - $this->setIncludeExternalStorage($this->savedQuotaIncludeExternalStorage); - $this->user = null; - - if ($this->storageMock) { - $this->storageMock->getCache()->clear(); - $this->storageMock = null; - } - Filesystem::tearDown(); - - \OC_User::setUserId(''); - Server::get(IConfig::class)->deleteAllUserValues($this->user); - - parent::tearDown(); - } - - /** - * Returns a storage mock that returns the given value as - * free space - * - * @param int $freeSpace free space value - * @return Storage - */ - private function getStorageMock($freeSpace = 12) { - $this->storageMock = $this->getMockBuilder(Temporary::class) - ->onlyMethods(['free_space']) - ->setConstructorArgs([[]]) - ->getMock(); - - $this->storageMock->expects($this->once()) - ->method('free_space') - ->willReturn($freeSpace); - return $this->storageMock; - } - - /** - * Test getting the storage info - */ - public function testGetStorageInfo(): void { - $homeStorage = $this->getStorageMock(12); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - $homeStorage->file_put_contents('test.txt', '01234'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(12, $storageInfo['free']); - $this->assertEquals(5, $storageInfo['used']); - $this->assertEquals(17, $storageInfo['total']); - } - - private function getIncludeExternalStorage(): bool { - $class = new \ReflectionClass(\OC_Helper::class); - $prop = $class->getProperty('quotaIncludeExternalStorage'); - return $prop->getValue(null) ?? false; - } - - private function setIncludeExternalStorage(bool $include) { - $class = new \ReflectionClass(\OC_Helper::class); - $prop = $class->getProperty('quotaIncludeExternalStorage'); - $prop->setValue(null, $include); - } - - /** - * Test getting the storage info, ignoring extra mount points - */ - public function testGetStorageInfoExcludingExtStorage(): void { - $homeStorage = $this->getStorageMock(12); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - $homeStorage->file_put_contents('test.txt', '01234'); - - $extStorage = new Temporary([]); - $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq'); - $extStorage->getScanner()->scan(''); // update root size - - $this->setIncludeExternalStorage(false); - - Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(12, $storageInfo['free']); - $this->assertEquals(5, $storageInfo['used']); - $this->assertEquals(17, $storageInfo['total']); - } - - /** - * Test getting the storage info, including extra mount points - */ - public function testGetStorageInfoIncludingExtStorage(): void { - $homeStorage = new Temporary([]); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - $homeStorage->file_put_contents('test.txt', '01234'); - - $extStorage = new Temporary([]); - $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq'); - $extStorage->getScanner()->scan(''); // update root size - - Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext'); - - $this->setIncludeExternalStorage(true); - - $config = Server::get(IConfig::class); - $config->setUserValue($this->user, 'files', 'quota', '25'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(3, $storageInfo['free']); - $this->assertEquals(22, $storageInfo['used']); - $this->assertEquals(25, $storageInfo['total']); - - $config->setUserValue($this->user, 'files', 'quota', 'default'); - } - - /** - * Test getting the storage info excluding extra mount points - * when user has no quota set, even when quota ext storage option - * was set - */ - public function testGetStorageInfoIncludingExtStorageWithNoUserQuota(): void { - $homeStorage = $this->getStorageMock(12); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - $homeStorage->file_put_contents('test.txt', '01234'); - - $extStorage = new Temporary([]); - $extStorage->file_put_contents('extfile.txt', 'abcdefghijklmnopq'); - $extStorage->getScanner()->scan(''); // update root size - - Filesystem::mount($extStorage, [], '/' . $this->user . '/files/ext'); - - $config = Server::get(IConfig::class); - $this->setIncludeExternalStorage(true); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(12, $storageInfo['free'], '12 bytes free in home storage'); - $this->assertEquals(22, $storageInfo['used'], '5 bytes of home storage and 17 bytes of the temporary storage are used'); - $this->assertEquals(34, $storageInfo['total'], '5 bytes used and 12 bytes free in home storage as well as 17 bytes used in temporary storage'); - } - - /** - * Test getting the storage info with quota enabled - */ - public function testGetStorageInfoWithQuota(): void { - $homeStorage = $this->getStorageMock(12); - $homeStorage->file_put_contents('test.txt', '01234'); - $homeStorage = new Quota( - [ - 'storage' => $homeStorage, - 'quota' => 7 - ] - ); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(2, $storageInfo['free']); - $this->assertEquals(5, $storageInfo['used']); - $this->assertEquals(7, $storageInfo['total']); - } - - /** - * Test getting the storage info when data exceeds quota - */ - public function testGetStorageInfoWhenSizeExceedsQuota(): void { - $homeStorage = $this->getStorageMock(12); - $homeStorage->file_put_contents('test.txt', '0123456789'); - $homeStorage = new Quota( - [ - 'storage' => $homeStorage, - 'quota' => 7 - ] - ); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(0, $storageInfo['free']); - $this->assertEquals(10, $storageInfo['used']); - // total = quota - $this->assertEquals(7, $storageInfo['total']); - } - - /** - * Test getting the storage info when the remaining - * free storage space is less than the quota - */ - public function testGetStorageInfoWhenFreeSpaceLessThanQuota(): void { - $homeStorage = $this->getStorageMock(12); - $homeStorage->file_put_contents('test.txt', '01234'); - $homeStorage = new Quota( - [ - 'storage' => $homeStorage, - 'quota' => 18 - ] - ); - Filesystem::mount($homeStorage, [], '/' . $this->user . '/files'); - - $storageInfo = \OC_Helper::getStorageInfo(''); - $this->assertEquals(12, $storageInfo['free']); - $this->assertEquals(5, $storageInfo['used']); - // total = free + used (because quota > total) - $this->assertEquals(17, $storageInfo['total']); - } -} --- a/tests/lib/HookHelper.php +++ /dev/null @@ -1,115 +0,0 @@ - Filesystem::signal_create, - 'params' => $params - ]; - } - - public static function updateCallback($params) { - self::$hookCalls[] = [ - 'signal' => Filesystem::signal_update, - 'params' => $params - ]; - } - - public static function writeCallback($params) { - self::$hookCalls[] = [ - 'signal' => Filesystem::signal_write, - 'params' => $params - ]; - } - - public static function postCreateCallback($params) { - self::$hookCalls[] = [ - 'signal' => Filesystem::signal_post_create, - 'params' => $params - ]; - } - - public static function postUpdateCallback($params) { - self::$hookCalls[] = [ - 'signal' => Filesystem::signal_post_update, - 'params' => $params - ]; - } - - public static function postWriteCallback($params) { - self::$hookCalls[] = [ - 'signal' => Filesystem::signal_post_write, - 'params' => $params - ]; - } - - /** - * Callback that sets the run parameter to false - */ - public static function cancellingCallback($params) { - $params[Filesystem::signal_param_run] = false; - } -} --- a/tests/lib/Hooks/BasicEmitterTest.php +++ /dev/null @@ -1,275 +0,0 @@ -emit($scope, $method, $arguments); - } -} - -/** - * Class EmittedException - * - * a dummy exception so we can check if an event is emitted - * - * @package Test\Hooks - */ -class EmittedException extends \Exception { -} - -class BasicEmitterTest extends \Test\TestCase { - /** - * @var Emitter $emitter - */ - protected $emitter; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->emitter = new DummyEmitter(); - } - - public function nonStaticCallBack() { - throw new EmittedException; - } - - public static function staticCallBack() { - throw new EmittedException; - } - - public function testAnonymousFunction(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $this->emitter->listen('Test', 'test', function (): void { - throw new EmittedException; - }); - $this->emitter->emitEvent('Test', 'test'); - } - - public function testStaticCallback(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $this->emitter->listen('Test', 'test', ['\Test\Hooks\BasicEmitterTest', 'staticCallBack']); - $this->emitter->emitEvent('Test', 'test'); - } - - public function testNonStaticCallback(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $this->emitter->listen('Test', 'test', [$this, 'nonStaticCallBack']); - $this->emitter->emitEvent('Test', 'test'); - } - - public function testOnlyCallOnce(): void { - $count = 0; - $listener = function () use (&$count): void { - $count++; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->assertEquals(1, $count, 'Listener called an invalid number of times (' . $count . ') expected 1'); - } - - public function testDifferentMethods(): void { - $count = 0; - $listener = function () use (&$count): void { - $count++; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Test', 'foo', $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->emitter->emitEvent('Test', 'foo'); - $this->assertEquals(2, $count, 'Listener called an invalid number of times (' . $count . ') expected 2'); - } - - public function testDifferentScopes(): void { - $count = 0; - $listener = function () use (&$count): void { - $count++; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Bar', 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->emitter->emitEvent('Bar', 'test'); - $this->assertEquals(2, $count, 'Listener called an invalid number of times (' . $count . ') expected 2'); - } - - public function testDifferentCallbacks(): void { - $count = 0; - $listener1 = function () use (&$count): void { - $count++; - }; - $listener2 = function () use (&$count): void { - $count++; - }; - $this->emitter->listen('Test', 'test', $listener1); - $this->emitter->listen('Test', 'test', $listener2); - $this->emitter->emitEvent('Test', 'test'); - $this->assertEquals(2, $count, 'Listener called an invalid number of times (' . $count . ') expected 2'); - } - - public function testArguments(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $this->emitter->listen('Test', 'test', function ($foo, $bar): void { - if ($foo === 'foo' && $bar === 'bar') { - throw new EmittedException; - } - }); - $this->emitter->emitEvent('Test', 'test', ['foo', 'bar']); - } - - public function testNamedArguments(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $this->emitter->listen('Test', 'test', function ($foo, $bar): void { - if ($foo === 'foo' && $bar === 'bar') { - throw new EmittedException; - } - }); - $this->emitter->emitEvent('Test', 'test', ['foo' => 'foo', 'bar' => 'bar']); - } - - public function testRemoveAllSpecified(): void { - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->removeListener('Test', 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveWildcardListener(): void { - $listener1 = function (): void { - throw new EmittedException; - }; - $listener2 = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener1); - $this->emitter->listen('Test', 'test', $listener2); - $this->emitter->removeListener('Test', 'test'); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveWildcardMethod(): void { - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Test', 'foo', $listener); - $this->emitter->removeListener('Test', null, $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->emitter->emitEvent('Test', 'foo'); - - $this->addToAssertionCount(1); - } - - public function testRemoveWildcardScope(): void { - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Bar', 'test', $listener); - $this->emitter->removeListener(null, 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->emitter->emitEvent('Bar', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveWildcardScopeAndMethod(): void { - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Test', 'foo', $listener); - $this->emitter->listen('Bar', 'foo', $listener); - $this->emitter->removeListener(null, null, $listener); - $this->emitter->emitEvent('Test', 'test'); - $this->emitter->emitEvent('Test', 'foo'); - $this->emitter->emitEvent('Bar', 'foo'); - - $this->addToAssertionCount(1); - } - - public function testRemoveKeepOtherCallback(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $listener1 = function (): void { - throw new EmittedException; - }; - $listener2 = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener1); - $this->emitter->listen('Test', 'test', $listener2); - $this->emitter->removeListener('Test', 'test', $listener1); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveKeepOtherMethod(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Test', 'foo', $listener); - $this->emitter->removeListener('Test', 'foo', $listener); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveKeepOtherScope(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->listen('Bar', 'test', $listener); - $this->emitter->removeListener('Bar', 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } - - public function testRemoveNonExistingName(): void { - $this->expectException(\Test\Hooks\EmittedException::class); - - $listener = function (): void { - throw new EmittedException; - }; - $this->emitter->listen('Test', 'test', $listener); - $this->emitter->removeListener('Bar', 'test', $listener); - $this->emitter->emitEvent('Test', 'test'); - - $this->addToAssertionCount(1); - } -} --- a/tests/lib/Http/Client/ClientServiceTest.php +++ /dev/null @@ -1,134 +0,0 @@ -createMock(IConfig::class); - $config->method('getSystemValueBool') - ->with('dns_pinning', true) - ->willReturn(true); - /** @var ICertificateManager $certificateManager */ - $certificateManager = $this->createMock(ICertificateManager::class); - $dnsPinMiddleware = $this->createMock(DnsPinMiddleware::class); - $dnsPinMiddleware - ->expects($this->atLeastOnce()) - ->method('addDnsPinning') - ->willReturn(function (): void { - }); - $remoteHostValidator = $this->createMock(IRemoteHostValidator::class); - $eventLogger = $this->createMock(IEventLogger::class); - $logger = $this->createMock(LoggerInterface::class); - $serverVersion = $this->createMock(ServerVersion::class); - - $clientService = new ClientService( - $config, - $certificateManager, - $dnsPinMiddleware, - $remoteHostValidator, - $eventLogger, - $logger, - $serverVersion, - ); - - $handler = new CurlHandler(); - $stack = HandlerStack::create($handler); - $stack->push($dnsPinMiddleware->addDnsPinning()); - $stack->push(Middleware::tap(function (RequestInterface $request) use ($eventLogger): void { - $eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget()); - }, function () use ($eventLogger): void { - $eventLogger->end('http:request'); - }), 'event logger'); - $guzzleClient = new GuzzleClient(['handler' => $stack]); - - $this->assertEquals( - new Client( - $config, - $certificateManager, - $guzzleClient, - $remoteHostValidator, - $logger, - $serverVersion, - ), - $clientService->newClient() - ); - } - - public function testDisableDnsPinning(): void { - /** @var IConfig $config */ - $config = $this->createMock(IConfig::class); - $config->method('getSystemValueBool') - ->with('dns_pinning', true) - ->willReturn(false); - /** @var ICertificateManager $certificateManager */ - $certificateManager = $this->createMock(ICertificateManager::class); - $dnsPinMiddleware = $this->createMock(DnsPinMiddleware::class); - $dnsPinMiddleware - ->expects($this->never()) - ->method('addDnsPinning') - ->willReturn(function (): void { - }); - $remoteHostValidator = $this->createMock(IRemoteHostValidator::class); - $eventLogger = $this->createMock(IEventLogger::class); - $logger = $this->createMock(LoggerInterface::class); - $serverVersion = $this->createMock(ServerVersion::class); - - $clientService = new ClientService( - $config, - $certificateManager, - $dnsPinMiddleware, - $remoteHostValidator, - $eventLogger, - $logger, - $serverVersion, - ); - - $handler = new CurlHandler(); - $stack = HandlerStack::create($handler); - $stack->push(Middleware::tap(function (RequestInterface $request) use ($eventLogger): void { - $eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget()); - }, function () use ($eventLogger): void { - $eventLogger->end('http:request'); - }), 'event logger'); - $guzzleClient = new GuzzleClient(['handler' => $stack]); - - $this->assertEquals( - new Client( - $config, - $certificateManager, - $guzzleClient, - $remoteHostValidator, - $logger, - $serverVersion, - ), - $clientService->newClient() - ); - } -} --- a/tests/lib/Http/Client/ClientTest.php +++ /dev/null @@ -1,706 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class); - $this->certificateManager = $this->createMock(ICertificateManager::class); - $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->serverVersion = $this->createMock(ServerVersion::class); - - $this->client = new Client( - $this->config, - $this->certificateManager, - $this->guzzleClient, - $this->remoteHostValidator, - $this->logger, - $this->serverVersion, - ); - } - - public function testGetProxyUri(): void { - $this->config - ->method('getSystemValueString') - ->with('proxy', '') - ->willReturn(''); - $this->assertNull(self::invokePrivate($this->client, 'getProxyUri')); - } - - public function testGetProxyUriProxyHostEmptyPassword(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['proxyexclude', [], []], - ]); - - $this->config - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', ''], - ]); - - $this->assertEquals([ - 'http' => 'foo', - 'https' => 'foo' - ], self::invokePrivate($this->client, 'getProxyUri')); - } - - public function testGetProxyUriProxyHostWithPassword(): void { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('proxyexclude', []) - ->willReturn([]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', 'username:password'], - ]); - $this->assertEquals([ - 'http' => 'username:password@foo', - 'https' => 'username:password@foo' - ], self::invokePrivate($this->client, 'getProxyUri')); - } - - public function testGetProxyUriProxyHostWithPasswordAndExclude(): void { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('proxyexclude', []) - ->willReturn(['bar']); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', 'username:password'], - ]); - $this->assertEquals([ - 'http' => 'username:password@foo', - 'https' => 'username:password@foo', - 'no' => ['bar'] - ], self::invokePrivate($this->client, 'getProxyUri')); - } - - public function testPreventLocalAddressThrowOnInvalidUri(): void { - $this->expectException(LocalServerException::class); - $this->expectExceptionMessage('Could not detect any host'); - - self::invokePrivate($this->client, 'preventLocalAddress', ['!@#$', []]); - } - - public static function dataPreventLocalAddress(): array { - return [ - ['https://localhost/foo.bar'], - ['https://localHost/foo.bar'], - ['https://random-host/foo.bar'], - ['https://[::1]/bla.blub'], - ['https://[::]/bla.blub'], - ['https://192.168.0.1'], - ['https://172.16.42.1'], - ['https://[fdf8:f53b:82e4::53]/secret.ics'], - ['https://[fe80::200:5aee:feaa:20a2]/secret.ics'], - ['https://[0:0:0:0:0:0:10.0.0.1]/secret.ics'], - ['https://[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'], - ['https://10.0.0.1'], - ['https://another-host.local'], - ['https://service.localhost'], - ['https://normal.host.com'], - ['https://com.one-.nextcloud-one.com'], - ]; - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressDisabledByGlobalConfig(string $uri): void { - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('allow_local_remote_servers', false) - ->willReturn(true); - - self::invokePrivate($this->client, 'preventLocalAddress', [$uri, []]); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressDisabledByOption(string $uri): void { - $this->config->expects($this->never()) - ->method('getSystemValueBool'); - - self::invokePrivate($this->client, 'preventLocalAddress', [$uri, [ - 'nextcloud' => ['allow_local_address' => true], - ]]); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressOnGet(string $uri): void { - $host = parse_url($uri, PHP_URL_HOST); - $this->expectException(LocalServerException::class); - $this->remoteHostValidator - ->method('isValid') - ->with($host) - ->willReturn(false); - - $this->client->get($uri); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressOnHead(string $uri): void { - $host = parse_url($uri, PHP_URL_HOST); - $this->expectException(LocalServerException::class); - $this->remoteHostValidator - ->method('isValid') - ->with($host) - ->willReturn(false); - - $this->client->head($uri); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressOnPost(string $uri): void { - $host = parse_url($uri, PHP_URL_HOST); - $this->expectException(LocalServerException::class); - $this->remoteHostValidator - ->method('isValid') - ->with($host) - ->willReturn(false); - - $this->client->post($uri); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressOnPut(string $uri): void { - $host = parse_url($uri, PHP_URL_HOST); - $this->expectException(LocalServerException::class); - $this->remoteHostValidator - ->method('isValid') - ->with($host) - ->willReturn(false); - - $this->client->put($uri); - } - - /** - * @param string $uri - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] - public function testPreventLocalAddressOnDelete(string $uri): void { - $host = parse_url($uri, PHP_URL_HOST); - $this->expectException(LocalServerException::class); - $this->remoteHostValidator - ->method('isValid') - ->with($host) - ->willReturn(false); - - $this->client->delete($uri); - } - - private function setUpDefaultRequestOptions(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['proxyexclude', [], []], - ]); - $this->config - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', ''], - ['overwrite.cli.url', '', ''] - ]); - $this->config - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, true], - ['allow_local_remote_servers', false, true], - ['http_client_add_user_agent_url', false, false] - ]); - - $this->certificateManager - ->expects($this->once()) - ->method('getAbsoluteBundlePath') - ->with() - ->willReturn('/my/path.crt'); - - $this->serverVersion->method('getVersionString') - ->willReturn('123.45.6'); - - $acceptEnc = (((curl_version()['features'] ?? 0) & CURL_VERSION_BROTLI) === CURL_VERSION_BROTLI) ? 'br, gzip' : 'gzip'; - $this->defaultRequestOptions = [ - 'verify' => '/my/path.crt', - 'proxy' => [ - 'http' => 'foo', - 'https' => 'foo' - ], - 'headers' => [ - 'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6', - 'Accept-Encoding' => $acceptEnc, - ], - 'timeout' => 30, - 'nextcloud' => [ - 'allow_local_address' => true, - ], - 'version' => '2.0', - 'curl' => [ - \CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2TLS, - ], - ]; - } - - public function testGet(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('get', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->get('http://localhost/', [])->getStatusCode()); - } - - public function testGetWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('get', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->get('http://localhost/', $options)->getStatusCode()); - } - - public function testPost(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('post', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->post('http://localhost/', [])->getStatusCode()); - } - - public function testPostWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('post', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->post('http://localhost/', $options)->getStatusCode()); - } - - public function testPut(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('put', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->put('http://localhost/', [])->getStatusCode()); - } - - public function testPutWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('put', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->put('http://localhost/', $options)->getStatusCode()); - } - - public function testDelete(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('delete', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->delete('http://localhost/', [])->getStatusCode()); - } - - public function testDeleteWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('delete', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->delete('http://localhost/', $options)->getStatusCode()); - } - - public function testOptions(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('options', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->options('http://localhost/', [])->getStatusCode()); - } - - public function testOptionsWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('options', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->options('http://localhost/', $options)->getStatusCode()); - } - - public function testHead(): void { - $this->setUpDefaultRequestOptions(); - - $this->guzzleClient->method('request') - ->with('head', 'http://localhost/', $this->defaultRequestOptions) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->head('http://localhost/', [])->getStatusCode()); - } - - public function testHeadWithOptions(): void { - $this->setUpDefaultRequestOptions(); - - $options = array_merge($this->defaultRequestOptions, [ - 'verify' => false, - 'proxy' => [ - 'http' => 'bar', - 'https' => 'bar' - ], - ]); - - $this->guzzleClient->method('request') - ->with('head', 'http://localhost/', $options) - ->willReturn(new Response(418)); - $this->assertEquals(418, $this->client->head('http://localhost/', $options)->getStatusCode()); - } - - public function testSetDefaultOptionsWithNotInstalled(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, false], - ['allow_local_remote_servers', false, false], - ['http_client_add_user_agent_url', false, false], - ]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', ''], - ['overwrite.cli.url', '', ''], - ]); - $this->certificateManager - ->expects($this->never()) - ->method('listCertificates'); - $this->certificateManager - ->expects($this->once()) - ->method('getDefaultCertificatesBundlePath') - ->willReturn(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'); - - $this->serverVersion->method('getVersionString') - ->willReturn('123.45.6'); - - $acceptEnc = (((curl_version()['features'] ?? 0) & CURL_VERSION_BROTLI) === CURL_VERSION_BROTLI) ? 'br, gzip' : 'gzip'; - - $this->assertEquals([ - 'verify' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt', - 'headers' => [ - 'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6', - 'Accept-Encoding' => $acceptEnc, - ], - 'timeout' => 30, - 'nextcloud' => [ - 'allow_local_address' => false, - ], - 'allow_redirects' => [ - 'on_redirect' => function ( - \Psr\Http\Message\RequestInterface $request, - \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri, - ): void { - }, - ], - 'version' => '2.0', - 'curl' => [ - \CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2TLS, - ], - ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); - } - - public function testSetDefaultOptionsWithProxy(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, true], - ['allow_local_remote_servers', false, false], - ['http_client_add_user_agent_url', false, false], - ]); - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('proxyexclude', []) - ->willReturn([]); - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', ''], - ['overwrite.cli.url', '', ''], - ]); - $this->certificateManager - ->expects($this->once()) - ->method('getAbsoluteBundlePath') - ->with() - ->willReturn('/my/path.crt'); - - $this->serverVersion->method('getVersionString') - ->willReturn('123.45.6'); - - $acceptEnc = (((curl_version()['features'] ?? 0) & CURL_VERSION_BROTLI) === CURL_VERSION_BROTLI) ? 'br, gzip' : 'gzip'; - - $this->assertEquals([ - 'verify' => '/my/path.crt', - 'proxy' => [ - 'http' => 'foo', - 'https' => 'foo' - ], - 'headers' => [ - 'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6', - 'Accept-Encoding' => $acceptEnc, - ], - 'timeout' => 30, - 'nextcloud' => [ - 'allow_local_address' => false, - ], - 'allow_redirects' => [ - 'on_redirect' => function ( - \Psr\Http\Message\RequestInterface $request, - \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri, - ): void { - }, - ], - 'version' => '2.0', - 'curl' => [ - \CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2TLS, - ], - ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); - } - - public function testSetDefaultOptionsWithProxyAndExclude(): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, true], - ['allow_local_remote_servers', false, false], - ['http_client_add_user_agent_url', false, false], - ]); - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('proxyexclude', []) - ->willReturn(['bar']); - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', ''], - ['overwrite.cli.url', '', ''], - ]); - $this->certificateManager - ->expects($this->once()) - ->method('getAbsoluteBundlePath') - ->with() - ->willReturn('/my/path.crt'); - - $this->serverVersion->method('getVersionString') - ->willReturn('123.45.6'); - - $acceptEnc = (((curl_version()['features'] ?? 0) & CURL_VERSION_BROTLI) === CURL_VERSION_BROTLI) ? 'br, gzip' : 'gzip'; - - $this->assertEquals([ - 'verify' => '/my/path.crt', - 'proxy' => [ - 'http' => 'foo', - 'https' => 'foo', - 'no' => ['bar'] - ], - 'headers' => [ - 'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6', - 'Accept-Encoding' => $acceptEnc, - ], - 'timeout' => 30, - 'nextcloud' => [ - 'allow_local_address' => false, - ], - 'allow_redirects' => [ - 'on_redirect' => function ( - \Psr\Http\Message\RequestInterface $request, - \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri, - ): void { - }, - ], - 'version' => '2.0', - 'curl' => [ - \CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2TLS, - ], - ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); - } - - public static function dataForTestSetServerUrlInUserAgent(): array { - return [ - ['https://example.com/', 'Nextcloud-Server-Crawler/123.45.6; +https://example.com'], - ['', 'Nextcloud-Server-Crawler/123.45.6'], - ]; - } - - #[DataProvider('dataForTestSetServerUrlInUserAgent')] - public function testSetServerUrlInUserAgent(string $url, string $userAgent): void { - $this->config - ->expects($this->exactly(3)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, true], - ['allow_local_remote_servers', false, false], - ['http_client_add_user_agent_url', false, true], - ]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['proxy', '', ''], - ['overwrite.cli.url', '', $url], - ]); - $this->certificateManager - ->expects($this->once()) - ->method('getAbsoluteBundlePath') - ->with() - ->willReturn('/my/path.crt'); - - $this->serverVersion->method('getVersionString') - ->willReturn('123.45.6'); - - $acceptEnc = (((curl_version()['features'] ?? 0) & CURL_VERSION_BROTLI) === CURL_VERSION_BROTLI) ? 'br, gzip' : 'gzip'; - $this->assertEquals([ - 'verify' => '/my/path.crt', - 'headers' => [ - 'User-Agent' => $userAgent, - 'Accept-Encoding' => $acceptEnc, - ], - 'timeout' => 30, - 'nextcloud' => [ - 'allow_local_address' => false, - ], - 'allow_redirects' => [ - 'on_redirect' => function ( - \Psr\Http\Message\RequestInterface $request, - \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri, - ): void { - }, - ], - 'version' => '2.0', - 'curl' => [ - \CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2TLS, - ], - ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); - } -} --- a/tests/lib/Http/Client/DnsPinMiddlewareTest.php +++ /dev/null @@ -1,550 +0,0 @@ -createMock(ICacheFactory::class); - $cacheFactory - ->method('createLocal') - ->willReturn(new NullCache()); - - $ipAddressClassifier = new IpAddressClassifier(); - $negativeDnsCache = new NegativeDnsCache($cacheFactory); - $logger = new NullLogger(); - - $this->dnsPinMiddleware = $this->getMockBuilder(DnsPinMiddleware::class) - ->setConstructorArgs([$negativeDnsCache, $ipAddressClassifier, $logger]) - ->onlyMethods(['dnsGetRecord']) - ->getMock(); - } - - public function testPopulateDnsCacheIPv4(): void { - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options) { - self::arrayHasKey('curl', $options); - self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); - self::assertEquals([ - 'www.example.com:80:1.1.1.1', - 'www.example.com:443:1.1.1.1' - ], $options['curl'][CURLOPT_RESOLVE]); - return new Response(200); - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - // example.com SOA - if ($hostname === 'example.com.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.com A, AAAA, CNAME - if ($hostname === 'www.example.com.') { - return match ($type) { - DNS_A => [], - DNS_AAAA => [], - DNS_CNAME => [ - [ - 'host' => 'www.example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'target' => 'www.example.net' - ] - ], - }; - } - - // example.net SOA - if ($hostname === 'example.net.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.net', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.net A, AAAA, CNAME - if ($hostname === 'www.example.net.') { - return match ($type) { - DNS_A => [ - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '1.1.1.1' - ] - ], - DNS_AAAA => [], - DNS_CNAME => [], - }; - } - - return false; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testPopulateDnsCacheIPv6(): void { - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options) { - self::arrayHasKey('curl', $options); - self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); - self::assertEquals([ - 'www.example.com:80:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001', - 'www.example.com:443:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001' - ], $options['curl'][CURLOPT_RESOLVE]); - return new Response(200); - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - // example.com SOA - if ($hostname === 'example.com.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.com A, AAAA, CNAME - if ($hostname === 'www.example.com.') { - return match ($type) { - DNS_A => [], - DNS_AAAA => [], - DNS_CNAME => [ - [ - 'host' => 'www.example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'target' => 'www.example.net' - ] - ], - }; - } - - // example.net SOA - if ($hostname === 'example.net.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.net', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.net A, AAAA, CNAME - if ($hostname === 'www.example.net.') { - return match ($type) { - DNS_A => [ - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '1.1.1.1' - ], - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '1.0.0.1' - ], - ], - DNS_AAAA => [ - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'AAAA', - 'ip' => '2606:4700:4700::1111' - ], - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'AAAA', - 'ip' => '2606:4700:4700::1001' - ], - ], - DNS_CNAME => [], - }; - } - - return false; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testAllowLocalAddress(): void { - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options) { - self::assertArrayNotHasKey('curl', $options); - return new Response(200); - }, - ]); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => true]] - ); - } - - public function testRejectIPv4(): void { - $this->expectException(LocalServerException::class); - $this->expectExceptionMessage('violates local access rules'); - - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options): void { - // The handler should not be called - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - DNS_A => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '192.168.0.1' - ] - ], - DNS_AAAA => [], - DNS_CNAME => [], - }; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testRejectIPv6(): void { - $this->expectException(LocalServerException::class); - $this->expectExceptionMessage('violates local access rules'); - - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options): void { - // The handler should not be called - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - DNS_A => [], - DNS_AAAA => [ - [ - 'host' => 'ipv6.example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'AAAA', - 'ipv6' => 'fd12:3456:789a:1::1' - ] - ], - DNS_CNAME => [], - }; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://ipv6.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testRejectCanonicalName(): void { - $this->expectException(LocalServerException::class); - $this->expectExceptionMessage('violates local access rules'); - - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options): void { - // The handler should not be called - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - // example.com SOA - if ($hostname === 'example.com.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.com A, AAAA, CNAME - if ($hostname === 'www.example.com.') { - return match ($type) { - DNS_A => [], - DNS_AAAA => [], - DNS_CNAME => [ - [ - 'host' => 'www.example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'target' => 'www.example.net' - ] - ], - }; - } - - // example.net SOA - if ($hostname === 'example.net.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.net', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.net A, AAAA, CNAME - if ($hostname === 'www.example.net.') { - return match ($type) { - DNS_A => [ - [ - 'host' => 'www.example.net', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '192.168.0.2' - ] - ], - DNS_AAAA => [], - DNS_CNAME => [], - }; - } - - return false; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testRejectFaultyResponse(): void { - $this->expectException(LocalServerException::class); - $this->expectExceptionMessage('No DNS record found for www.example.com'); - - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options): void { - // The handler should not be called - }, - ]); - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) { - return false; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://www.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - } - - public function testIgnoreSubdomainForSoaQuery(): void { - $mockHandler = new MockHandler([ - static function (RequestInterface $request, array $options): void { - // The handler should not be called - }, - ]); - - $dnsQueries = []; - - $this->dnsPinMiddleware - ->method('dnsGetRecord') - ->willReturnCallback(function (string $hostname, int $type) use (&$dnsQueries) { - // log query - $dnsQueries[] = $hostname . $type; - - // example.com SOA - if ($hostname === 'example.com.') { - return match ($type) { - DNS_SOA => [ - [ - 'host' => 'example.com', - 'class' => 'IN', - 'ttl' => 7079, - 'type' => 'SOA', - 'minimum-ttl' => 3600, - ] - ], - }; - } - - // example.net A, AAAA, CNAME - if ($hostname === 'subsubdomain.subdomain.example.com.') { - return match ($type) { - DNS_A => [ - [ - 'host' => 'subsubdomain.subdomain.example.com', - 'class' => 'IN', - 'ttl' => 1800, - 'type' => 'A', - 'ip' => '1.1.1.1' - ] - ], - DNS_AAAA => [], - DNS_CNAME => [], - }; - } - - return false; - }); - - $stack = new HandlerStack($mockHandler); - $stack->push($this->dnsPinMiddleware->addDnsPinning()); - $handler = $stack->resolve(); - - $handler( - new Request('GET', 'https://subsubdomain.subdomain.example.com'), - ['nextcloud' => ['allow_local_address' => false]] - ); - - $this->assertCount(3, $dnsQueries); - $this->assertContains('example.com.' . DNS_SOA, $dnsQueries); - $this->assertContains('subsubdomain.subdomain.example.com.' . DNS_A, $dnsQueries); - $this->assertContains('subsubdomain.subdomain.example.com.' . DNS_AAAA, $dnsQueries); - // CNAME should not be queried if A or AAAA succeeded already - $this->assertNotContains('subsubdomain.subdomain.example.com.' . DNS_CNAME, $dnsQueries); - } -} --- a/tests/lib/Http/Client/NegativeDnsCacheTest.php +++ /dev/null @@ -1,56 +0,0 @@ -cache = $this->createMock(ICache::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cacheFactory - ->method('createLocal') - ->with('NegativeDnsCache') - ->willReturn($this->cache); - - $this->negativeDnsCache = new NegativeDnsCache($this->cacheFactory); - } - - public function testSetNegativeCacheForDnsType() : void { - $this->cache - ->expects($this->once()) - ->method('set') - ->with('www.example.com-1', 'true', 3600); - - $this->negativeDnsCache->setNegativeCacheForDnsType('www.example.com', DNS_A, 3600); - } - - public function testIsNegativeCached(): void { - $this->cache - ->expects($this->once()) - ->method('hasKey') - ->with('www.example.com-1') - ->willReturn(true); - - $this->assertTrue($this->negativeDnsCache->isNegativeCached('www.example.com', DNS_A)); - } -} --- a/tests/lib/Http/Client/ResponseTest.php +++ /dev/null @@ -1,60 +0,0 @@ -guzzleResponse = new GuzzleResponse(418); - } - - public function testGetBody(): void { - $response = new Response($this->guzzleResponse->withBody(Utils::streamFor('MyResponse'))); - $this->assertSame('MyResponse', $response->getBody()); - } - - public function testGetStatusCode(): void { - $response = new Response($this->guzzleResponse); - $this->assertSame(418, $response->getStatusCode()); - } - - public function testGetHeader(): void { - $response = new Response($this->guzzleResponse->withHeader('bar', 'foo')); - $this->assertSame('foo', $response->getHeader('bar')); - } - - public function testGetHeaders(): void { - $response = new Response($this->guzzleResponse - ->withHeader('bar', 'foo') - ->withHeader('x-awesome', 'yes') - ); - - $expected = [ - 'bar' => [ - 0 => 'foo', - ], - 'x-awesome' => [ - 0 => 'yes', - ], - ]; - $this->assertSame($expected, $response->getHeaders()); - $this->assertSame('yes', $response->getHeader('x-awesome')); - } -} --- a/tests/lib/Http/WellKnown/GenericResponseTest.php +++ /dev/null @@ -1,24 +0,0 @@ -createMock(JSONResponse::class); - - $response = new GenericResponse($httpResponse); - - self::assertSame($httpResponse, $response->toHttpResponse()); - } -} --- a/tests/lib/Http/WellKnown/JrdResponseTest.php +++ /dev/null @@ -1,92 +0,0 @@ -toHttpResponse(); - - self::assertTrue($response->isEmpty()); - self::assertInstanceOf(JSONResponse::class, $httpResponse); - /** @var JSONResponse $httpResponse */ - self::assertEquals( - [ - 'subject' => 'subject', - ], - $httpResponse->getData() - ); - } - - public function testComplexToHttpResponse(): void { - $response = new JrdResponse('subject'); - $response->addAlias('alias'); - $response->addAlias('blias'); - $response->addProperty('propa', 'a'); - $response->addProperty('propb', null); - $response->setExpires('tomorrow'); - $response->addLink('rel', null, null); - $response->addLink('rel', 'type', null); - $response->addLink('rel', 'type', 'href', ['title' => 'titlevalue']); - $response->addLink('rel', 'type', 'href', ['title' => 'titlevalue'], ['propx' => 'valx']); - $httpResponse = $response->toHttpResponse(); - - self::assertFalse($response->isEmpty()); - self::assertInstanceOf(JSONResponse::class, $httpResponse); - /** @var JSONResponse $httpResponse */ - self::assertEquals( - [ - 'subject' => 'subject', - 'aliases' => [ - 'alias', - 'blias', - ], - 'properties' => [ - 'propa' => 'a', - 'propb' => null, - ], - 'expires' => 'tomorrow', - 'links' => [ - [ - 'rel' => 'rel', - ], - [ - 'rel' => 'rel', - 'type' => 'type', - ], - [ - 'rel' => 'rel', - 'type' => 'type', - 'href' => 'href', - 'titles' => [ - 'title' => 'titlevalue', - ], - ], - [ - 'rel' => 'rel', - 'type' => 'type', - 'href' => 'href', - 'titles' => [ - 'title' => 'titlevalue', - ], - 'properties' => [ - 'propx' => 'valx', - ], - ], - ] - ], - $httpResponse->getData() - ); - } -} --- a/tests/lib/Http/WellKnown/RequestManagerTest.php +++ /dev/null @@ -1,149 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->manager = new RequestManager( - $this->coordinator, - $this->container, - $this->logger, - ); - } - - public function testProcessAppsNotRegistered(): void { - $request = $this->createMock(IRequest::class); - $this->expectException(RuntimeException::class); - - $this->manager->process('webfinger', $request); - } - - public function testProcessNoHandlersRegistered(): void { - $request = $this->createMock(IRequest::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects(self::once()) - ->method('getWellKnownHandlers') - ->willReturn([]); - - $response = $this->manager->process('webfinger', $request); - - self::assertNull($response); - } - - public function testProcessHandlerNotLoadable(): void { - $request = $this->createMock(IRequest::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $handler = new class { - }; - $registrationContext->expects(self::once()) - ->method('getWellKnownHandlers') - ->willReturn([ - new ServiceRegistration('test', get_class($handler)), - ]); - $this->container->expects(self::once()) - ->method('get') - ->with(get_class($handler)) - ->willThrowException(new QueryException('')); - $this->logger->expects(self::once()) - ->method('error'); - - $response = $this->manager->process('webfinger', $request); - - self::assertNull($response); - } - - public function testProcessHandlerOfWrongType(): void { - $request = $this->createMock(IRequest::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $handler = new class { - }; - $registrationContext->expects(self::once()) - ->method('getWellKnownHandlers') - ->willReturn([ - new ServiceRegistration('test', get_class($handler)), - ]); - $this->container->expects(self::once()) - ->method('get') - ->with(get_class($handler)) - ->willReturn($handler); - $this->logger->expects(self::once()) - ->method('error'); - - $response = $this->manager->process('webfinger', $request); - - self::assertNull($response); - } - - public function testProcess(): void { - $request = $this->createMock(IRequest::class); - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects(self::once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $handler = new class implements IHandler { - #[\Override] - public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse { - return (new JrdResponse($service))->addAlias('alias'); - } - }; - $registrationContext->expects(self::once()) - ->method('getWellKnownHandlers') - ->willReturn([ - new ServiceRegistration('test', get_class($handler)), - ]); - $this->container->expects(self::once()) - ->method('get') - ->with(get_class($handler)) - ->willReturn($handler); - - $response = $this->manager->process('webfinger', $request); - - self::assertNotNull($response); - self::assertInstanceOf(JrdResponse::class, $response); - } -} --- a/tests/lib/HttpRequestLogger.php +++ /dev/null @@ -1,106 +0,0 @@ -registerService(IClientService::class, static fn (): IClientService - => new self(Server::get(ClientService::class), $logFile)); - - // A test running in a separate process re-runs the bootstrap, so only the - // process that owns the run may truncate. Children inherit the marker. - if (getenv(self::RUN_MARKER) === false) { - putenv(self::RUN_MARKER . '=' . getmypid()); - file_put_contents($logFile, ''); - } - } - - #[\Override] - public function newClient(?callable $handler = null): IClient { - $next = $handler ?? new CurlHandler(); - - return $this->inner->newClient( - function (RequestInterface $request, array $options) use ($next): PromiseInterface { - $start = microtime(true); - - return $next($request, $options)->then( - function (ResponseInterface $response) use ($request, $start): ResponseInterface { - $this->record($request, $start, (string)$response->getStatusCode()); - return $response; - }, - function (mixed $reason) use ($request, $start): PromiseInterface { - $this->record($request, $start, 'error'); - return Create::rejectionFor($reason); - }, - ); - }, - ); - } - - private function record(RequestInterface $request, float $start, string $outcome): void { - $line = json_encode([ - 'test' => self::currentTest(), - 'method' => $request->getMethod(), - 'uri' => (string)$request->getUri(), - 'outcome' => $outcome, - 'duration' => round(microtime(true) - $start, 6), - ], JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_SLASHES); - - if ($line !== false) { - file_put_contents($this->logFile, $line . "\n", FILE_APPEND); - } - } - - /** PHPUnit exposes no global for the running test, so walk the stack for it. */ - private static function currentTest(): string { - $test = '(unknown)'; - - foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { - $class = $frame['class'] ?? ''; - $function = $frame['function'] ?? ''; - - if ($class !== '' - && str_starts_with($function, 'test') - && is_subclass_of($class, \PHPUnit\Framework\TestCase::class) - ) { - $test = $class . '::' . $function; - } - } - - return $test; - } -} --- a/tests/lib/ImageTest.php +++ /dev/null @@ -1,384 +0,0 @@ -loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertInstanceOf('\OC\Image', $img); - $this->assertInstanceOf('\OCP\IImage', $img); - unset($img); - - $imgcreate = imagecreatefromjpeg(OC::$SERVERROOT . '/tests/data/testimage.jpg'); - $img = new Image(); - $img->setResource($imgcreate); - $this->assertInstanceOf('\OC\Image', $img); - $this->assertInstanceOf('\OCP\IImage', $img); - unset($img); - - $base64 = base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif')); - $img = new Image(); - $img->loadFromBase64($base64); - $this->assertInstanceOf('\OC\Image', $img); - $this->assertInstanceOf('\OCP\IImage', $img); - unset($img); - - $img = new Image(); - $this->assertInstanceOf('\OC\Image', $img); - $this->assertInstanceOf('\OCP\IImage', $img); - unset($img); - } - - public function testValid(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertTrue($img->valid()); - - $text = base64_encode('Lorem ipsum dolor sir amet …'); - $img = new Image(); - $img->loadFromBase64($text); - $this->assertFalse($img->valid()); - - $img = new Image(); - $this->assertFalse($img->valid()); - } - - public function testMimeType(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertEquals('image/png', $img->mimeType()); - - $img = new Image(); - $this->assertEquals('', $img->mimeType()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertEquals('image/jpeg', $img->mimeType()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertEquals('image/gif', $img->mimeType()); - } - - public function testWidth(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertEquals(128, $img->width()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertEquals(1680, $img->width()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertEquals(64, $img->width()); - - $img = new Image(); - $this->assertEquals(-1, $img->width()); - } - - public function testHeight(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertEquals(128, $img->height()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertEquals(1050, $img->height()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertEquals(64, $img->height()); - - $img = new Image(); - $this->assertEquals(-1, $img->height()); - } - - public function testSave(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $img->resize(16); - $img->save(OC::$SERVERROOT . '/tests/data/testimage2.png'); - $this->assertEquals(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage2.png'), $img->data()); - - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.jpg'); - $img->resize(128); - $img->save(OC::$SERVERROOT . '/tests/data/testimage2.jpg'); - $this->assertEquals(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage2.jpg'), $img->data()); - } - - public function testData(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $raw = imagecreatefromstring(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.png')); - // Preserve transparency - imagealphablending($raw, true); - imagesavealpha($raw, true); - ob_start(); - imagepng($raw); - $expected = ob_get_clean(); - $this->assertEquals($expected, $img->data()); - - $appConfig = $this->createMock(IAppConfig::class); - $appConfig->expects($this->once()) - ->method('getValueInt') - ->with('preview', 'jpeg_quality', 80) - ->willReturn(80); - $config = $this->createMock(IConfig::class); - $config->expects($this->once()) - ->method('getSystemValueInt') - ->with('preview_max_memory', 256) - ->willReturn(256); - $img = new Image(null, $appConfig, $config); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.jpg'); - $raw = imagecreatefromstring(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - imageinterlace($raw, true); - ob_start(); - imagejpeg($raw, null, 80); - $expected = ob_get_clean(); - $this->assertEquals($expected, $img->data()); - - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.gif'); - $raw = imagecreatefromstring(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif')); - ob_start(); - imagegif($raw); - $expected = ob_get_clean(); - $this->assertEquals($expected, $img->data()); - } - - public function testDataNoResource(): void { - $img = new Image(); - $this->assertNull($img->data()); - } - - #[\PHPUnit\Framework\Attributes\Depends('testData')] - public function testToString(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $expected = base64_encode($img->data()); - $this->assertEquals($expected, (string)$img); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $expected = base64_encode($img->data()); - $this->assertEquals($expected, (string)$img); - - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.gif'); - $expected = base64_encode($img->data()); - $this->assertEquals($expected, (string)$img); - } - - public function testResize(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertTrue($img->resize(32)); - $this->assertEquals(32, $img->width()); - $this->assertEquals(32, $img->height()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertTrue($img->resize(840)); - $this->assertEquals(840, $img->width()); - $this->assertEquals(525, $img->height()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertTrue($img->resize(100)); - $this->assertEquals(100, $img->width()); - $this->assertEquals(100, $img->height()); - } - - public function testPreciseResize(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertTrue($img->preciseResize(128, 512)); - $this->assertEquals(128, $img->width()); - $this->assertEquals(512, $img->height()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertTrue($img->preciseResize(64, 840)); - $this->assertEquals(64, $img->width()); - $this->assertEquals(840, $img->height()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertTrue($img->preciseResize(1000, 1337)); - $this->assertEquals(1000, $img->width()); - $this->assertEquals(1337, $img->height()); - } - - public function testCenterCrop(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $img->centerCrop(); - $this->assertEquals(128, $img->width()); - $this->assertEquals(128, $img->height()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $img->centerCrop(); - $this->assertEquals(1050, $img->width()); - $this->assertEquals(1050, $img->height()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $img->centerCrop(512); - $this->assertEquals(512, $img->width()); - $this->assertEquals(512, $img->height()); - } - - public function testCrop(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $this->assertTrue($img->crop(0, 0, 50, 20)); - $this->assertEquals(50, $img->width()); - $this->assertEquals(20, $img->height()); - - $img = new Image(); - $img->loadFromData(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.jpg')); - $this->assertTrue($img->crop(500, 700, 550, 300)); - $this->assertEquals(550, $img->width()); - $this->assertEquals(300, $img->height()); - - $img = new Image(); - $img->loadFromBase64(base64_encode(file_get_contents(OC::$SERVERROOT . '/tests/data/testimage.gif'))); - $this->assertTrue($img->crop(10, 10, 15, 15)); - $this->assertEquals(15, $img->width()); - $this->assertEquals(15, $img->height()); - } - - public static function sampleProvider(): array { - return [ - ['testimage.png', [200, 100], [100, 100]], - ['testimage.jpg', [840, 840], [840, 525]], - ['testimage.gif', [200, 250], [200, 200]] - ]; - } - - /** - * - * @param string $filename - * @param int[] $asked - * @param int[] $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('sampleProvider')] - public function testFitIn($filename, $asked, $expected): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/' . $filename); - $this->assertTrue($img->fitIn($asked[0], $asked[1])); - $this->assertEquals($expected[0], $img->width()); - $this->assertEquals($expected[1], $img->height()); - } - - public static function sampleFilenamesProvider(): array { - return [ - ['testimage.png'], - ['testimage.jpg'], - ['testimage.gif'] - ]; - } - - /** - * Image should not be resized if it's already smaller than what is required - * - * - * @param string $filename - */ - #[\PHPUnit\Framework\Attributes\DataProvider('sampleFilenamesProvider')] - public function testScaleDownToFitWhenSmallerAlready($filename): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/' . $filename); - $currentWidth = $img->width(); - $currentHeight = $img->height(); - // We pick something larger than the image we want to scale down - $this->assertFalse($img->scaleDownToFit(4000, 4000)); - // The dimensions of the image should not have changed since it's smaller already - $resizedWidth = $img->width(); - $resizedHeight = $img->height(); - $this->assertEquals( - $currentWidth, $img->width(), "currentWidth $currentWidth resizedWidth $resizedWidth \n" - ); - $this->assertEquals( - $currentHeight, $img->height(), - "currentHeight $currentHeight resizedHeight $resizedHeight \n" - ); - } - - public static function largeSampleProvider(): array { - return [ - ['testimage.png', [200, 100], [100, 100]], - ['testimage.jpg', [840, 840], [840, 525]], - ]; - } - - /** - * - * @param string $filename - * @param int[] $asked - * @param int[] $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('largeSampleProvider')] - public function testScaleDownWhenBigger($filename, $asked, $expected): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/' . $filename); - //$this->assertTrue($img->scaleDownToFit($asked[0], $asked[1])); - $img->scaleDownToFit($asked[0], $asked[1]); - $this->assertEquals($expected[0], $img->width()); - $this->assertEquals($expected[1], $img->height()); - } - - public static function convertDataProvider(): array { - return [ - [ 'image/gif'], - [ 'image/jpeg'], - [ 'image/png'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('convertDataProvider')] - public function testConvert($mimeType): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.png'); - $tempFile = tempnam(sys_get_temp_dir(), 'img-test'); - - $img->save($tempFile, $mimeType); - $this->assertEquals($mimeType, image_type_to_mime_type(exif_imagetype($tempFile))); - } - - public function testMemoryLimitFromFile(): void { - $img = new Image(); - $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-badheader.jpg'); - $this->assertFalse($img->valid()); - } - - public function testMemoryLimitFromData(): void { - $data = file_get_contents(OC::$SERVERROOT . '/tests/data/testimage-badheader.jpg'); - $img = new Image(); - $img->loadFromData($data); - $this->assertFalse($img->valid()); - } -} --- a/tests/lib/InfoXmlTest.php +++ /dev/null @@ -1,145 +0,0 @@ -appManager = Server::get(AppManager::class); - } - - public static function dataApps(): array { - return [ - ['admin_audit'], - ['comments'], - ['dav'], - ['encryption'], - ['federatedfilesharing'], - ['federation'], - ['files'], - ['files_external'], - ['files_sharing'], - ['files_trashbin'], - ['files_versions'], - ['provisioning_api'], - ['systemtags'], - ['theming'], - ['settings'], - ['twofactor_backupcodes'], - ['updatenotification'], - ['user_ldap'], - ['workflowengine'], - ]; - } - - /** - * @param string $app - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataApps')] - public function testClasses($app): void { - $appInfo = $this->appManager->getAppInfo($app); - $appPath = $this->appManager->getAppPath($app); - $this->appManager->registerAutoloading($app, $appPath); - - //Add the appcontainer - $applicationClassName = $this->appManager->getAppNamespace($app) . '\\AppInfo\\Application'; - if (class_exists($applicationClassName)) { - $application = new $applicationClassName(); - $this->addToAssertionCount(1); - } else { - $application = new App($app); - $this->addToAssertionCount(1); - } - - if (isset($appInfo['background-jobs'])) { - foreach ($appInfo['background-jobs'] as $job) { - $this->assertTrue(class_exists($job), 'Asserting background job "' . $job . '" exists'); - $this->assertInstanceOf($job, Server::get($job)); - } - } - - if (isset($appInfo['two-factor-providers'])) { - foreach ($appInfo['two-factor-providers'] as $provider) { - $this->assertTrue(class_exists($provider), 'Asserting two-factor providers "' . $provider . '" exists'); - $this->assertInstanceOf($provider, Server::get($provider)); - } - } - - if (isset($appInfo['commands'])) { - foreach ($appInfo['commands'] as $command) { - $this->assertTrue(class_exists($command), 'Asserting command "' . $command . '" exists'); - $this->assertInstanceOf($command, Server::get($command)); - } - } - - if (isset($appInfo['repair-steps']['pre-migration'])) { - foreach ($appInfo['repair-steps']['pre-migration'] as $migration) { - $this->assertTrue(class_exists($migration), 'Asserting pre-migration "' . $migration . '" exists'); - $this->assertInstanceOf($migration, Server::get($migration)); - } - } - - if (isset($appInfo['repair-steps']['post-migration'])) { - foreach ($appInfo['repair-steps']['post-migration'] as $migration) { - $this->assertTrue(class_exists($migration), 'Asserting post-migration "' . $migration . '" exists'); - $this->assertInstanceOf($migration, Server::get($migration)); - } - } - - if (isset($appInfo['repair-steps']['live-migration'])) { - foreach ($appInfo['repair-steps']['live-migration'] as $migration) { - $this->assertTrue(class_exists($migration), 'Asserting live-migration "' . $migration . '" exists'); - $this->assertInstanceOf($migration, Server::get($migration)); - } - } - - if (isset($appInfo['repair-steps']['install'])) { - foreach ($appInfo['repair-steps']['install'] as $migration) { - $this->assertTrue(class_exists($migration), 'Asserting install-migration "' . $migration . '" exists'); - $this->assertInstanceOf($migration, Server::get($migration)); - } - } - - if (isset($appInfo['repair-steps']['uninstall'])) { - foreach ($appInfo['repair-steps']['uninstall'] as $migration) { - $this->assertTrue(class_exists($migration), 'Asserting uninstall-migration "' . $migration . '" exists'); - $this->assertInstanceOf($migration, Server::get($migration)); - } - } - - if (isset($appInfo['commands'])) { - foreach ($appInfo['commands'] as $command) { - $this->assertTrue(class_exists($command), 'Asserting command "' . $command . '"exists'); - $this->assertInstanceOf($command, Server::get($command)); - } - } - - if (isset($appInfo['openmetrics']['exporter'])) { - foreach ($appInfo['openmetrics']['exporter'] as $class) { - $this->assertTrue(class_exists($class), 'Asserting exporter "' . $class . '"exists'); - $exporter = Server::get($class); - $this->assertInstanceOf($class, $exporter); - $this->assertInstanceOf(IMetricFamily::class, $exporter); - } - } - } -} --- a/tests/lib/InitialStateServiceTest.php +++ /dev/null @@ -1,101 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - - $this->service = new InitialStateService( - $this->logger, - $this->createMock(Coordinator::class), - $this->createMock(ContainerInterface::class) - ); - } - - public static function staticData(): array { - return [ - ['string'], - [23], - [2.3], - [new class implements JsonSerializable { - #[\Override] - public function jsonSerialize(): int { - return 3; - } - }], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('staticData')] - public function testStaticData(mixed $value): void { - $this->service->provideInitialState('test', 'key', $value); - $data = $this->service->getInitialStates(); - - $this->assertEquals( - ['test-key' => json_encode($value)], - $data - ); - } - - public function testValidDataButFailsToJSONEncode(): void { - $this->logger->expects($this->once()) - ->method('error'); - - $this->service->provideInitialState('test', 'key', ['upload' => INF]); - $data = $this->service->getInitialStates(); - - $this->assertEquals( - [], - $data - ); - } - - public function testStaticButInvalidData(): void { - $this->logger->expects($this->once()) - ->method('warning'); - - $this->service->provideInitialState('test', 'key', new stdClass()); - $data = $this->service->getInitialStates(); - - $this->assertEquals( - [], - $data - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('staticData')] - public function testLazyData(mixed $value): void { - $this->service->provideLazyInitialState('test', 'key', function () use ($value) { - return $value; - }); - $data = $this->service->getInitialStates(); - - $this->assertEquals( - ['test-key' => json_encode($value)], - $data - ); - } -} --- a/tests/lib/Install/Events/InstallationCompletedEventTest.php +++ /dev/null @@ -1,89 +0,0 @@ -assertEquals($dataDir, $event->getDataDirectory()); - $this->assertEquals($adminUsername, $event->getAdminUsername()); - $this->assertEquals($adminEmail, $event->getAdminEmail()); - $this->assertTrue($event->hasAdminUser()); - } - - public function testConstructorWithMinimalParameters(): void { - $dataDir = '/path/to/data'; - - $event = new InstallationCompletedEvent($dataDir); - - $this->assertEquals($dataDir, $event->getDataDirectory()); - $this->assertNull($event->getAdminUsername()); - $this->assertNull($event->getAdminEmail()); - $this->assertFalse($event->hasAdminUser()); - } - - public function testConstructorWithUsernameOnly(): void { - $dataDir = '/path/to/data'; - $adminUsername = 'admin'; - - $event = new InstallationCompletedEvent($dataDir, $adminUsername); - - $this->assertEquals($dataDir, $event->getDataDirectory()); - $this->assertEquals($adminUsername, $event->getAdminUsername()); - $this->assertNull($event->getAdminEmail()); - $this->assertTrue($event->hasAdminUser()); - } - - public function testConstructorWithUsernameAndEmail(): void { - $dataDir = '/path/to/data'; - $adminUsername = 'admin'; - $adminEmail = 'admin@example.com'; - - $event = new InstallationCompletedEvent($dataDir, $adminUsername, $adminEmail); - - $this->assertEquals($dataDir, $event->getDataDirectory()); - $this->assertEquals($adminUsername, $event->getAdminUsername()); - $this->assertEquals($adminEmail, $event->getAdminEmail()); - $this->assertTrue($event->hasAdminUser()); - } - - public function testHasAdminUserReturnsFalseWhenUsernameIsNull(): void { - $event = new InstallationCompletedEvent('/path/to/data', null, 'admin@example.com'); - - $this->assertFalse($event->hasAdminUser()); - $this->assertNull($event->getAdminUsername()); - $this->assertEquals('admin@example.com', $event->getAdminEmail()); - } - - public function testDataDirectoryCanBeAnyString(): void { - $customPath = '/custom/data/directory'; - $event = new InstallationCompletedEvent($customPath); - - $this->assertEquals($customPath, $event->getDataDirectory()); - } - - public function testEmailCanBeSetWithoutUsername(): void { - $dataDir = '/path/to/data'; - $email = 'admin@example.com'; - - $event = new InstallationCompletedEvent($dataDir, null, $email); - - $this->assertNull($event->getAdminUsername()); - $this->assertEquals($email, $event->getAdminEmail()); - $this->assertFalse($event->hasAdminUser()); - } -} --- a/tests/lib/InstallerTest.php +++ /dev/null @@ -1,689 +0,0 @@ -appFetcher = $this->createMock(AppFetcher::class); - $this->clientService = $this->createMock(IClientService::class); - $this->tempManager = $this->createMock(ITempManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->config = $this->createMock(IConfig::class); - $this->appManager = $this->createMock(AppManager::class); - $this->l10nFactory = $this->createMock(IFactory::class); - - $installer = Server::get(Installer::class); - $installer->removeApp(self::$appid); - } - - protected function getInstaller() { - return new Installer( - $this->appFetcher, - $this->clientService, - $this->tempManager, - $this->logger, - $this->config, - $this->appManager, - $this->l10nFactory, - false - ); - } - - #[\Override] - protected function tearDown(): void { - $installer = Server::get(Installer::class); - $installer->removeApp(self::$appid); - - parent::tearDown(); - } - - public function testInstallApp(): void { - // Read the current version of the app to check for bug #2572 - Server::get(IAppManager::class)->getAppVersion('testapp', true); - - // Build installer - $installer = Server::get(Installer::class); - - // Extract app - $pathOfTestApp = __DIR__ . '/../data/testapp.zip'; - $tar = new ZIP($pathOfTestApp); - $tar->extract($installer->getInstallPath()); - - // Install app - $this->assertNull(Server::get(IConfig::class)->getAppValue('testapp', 'enabled', null), 'Check that the app is not listed before installation'); - $this->assertSame('testapp', $installer->installApp(self::$appid)); - $this->assertSame('no', Server::get(IConfig::class)->getAppValue('testapp', 'enabled', null), 'Check that the app is listed after installation'); - $this->assertSame('0.9', Server::get(IConfig::class)->getAppValue('testapp', 'installed_version')); - $installer->removeApp(self::$appid); - } - - public static function updateArrayProvider(): array { - return [ - // Update available - [ - [ - [ - 'id' => 'files', - 'releases' => [ - [ - 'version' => '1111.0' - ], - ], - ], - ], - '1111.0', - ], - // No update available - [ - [ - [ - 'id' => 'files', - 'releases' => [ - [ - 'version' => '1.0' - ], - ], - ], - ], - false, - ], - ]; - } - - /** - * @param array $appArray - * @param string|bool $updateAvailable - */ - #[\PHPUnit\Framework\Attributes\DataProvider('updateArrayProvider')] - public function testIsUpdateAvailable(array $appArray, $updateAvailable): void { - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $this->appManager - ->expects($this->exactly(2)) - ->method('getAppVersion') - ->willReturn('1.0'); - - $installer = $this->getInstaller(); - $this->assertSame($updateAvailable, $installer->isUpdateAvailable('files')); - $this->assertSame($updateAvailable, $installer->isUpdateAvailable('files'), 'Cached result should be returned and fetcher should be only called once'); - } - - public function testDownloadAppWithRevokedCertificate(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Certificate "4112" has been revoked'); - - $appArray = [ - [ - 'id' => 'news', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAQMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDAzMTMyNDM3WhcNMjcwMTA5MTMyNDM3WjASMRAwDgYD -VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApEt+ -KZGs+WqdZkHZflzqk+ophYWB8qB47XCzy+xdTGPFM84/9wXltRPbcQQWJJl5aOx0 -FPbsyTGhIt/IYZ2Vl0XrDRJjsaxzPcrofrwpJ2tqforXjGohl6mZUBA0ESzFiPzT -SAZe8E14+Jk8rbF/ecrkqcWf2cTMV3Qfu9YvJo8WVs4lHc95r1F+Nalh/OLkHkzb -fYPno2Z5cco6U7BXunFQG2gqy3wWQwmlhDxh5fwrCoFzPWm7WhwSyK+eMoSDz+Vp -3kmtyijsqnda0zA9bfNzgW26czbJaObbnkdtDC2nfoAWXndlS/5YRI8yHd9miB5C -u1OC8LUWToDGNa9+FOxBSj7Nk6iyjbVfRXcTqThdkVZdOOPaBRMsL9R4UYywCbhA -yGNiQ0ahfXD8MZSb08rlQg8tAtcUZW1sYQcbtMGnu8OyC5J7N1efzv5mys4+9hBS -5ECeyCuQTuOkF4H/XS2BMSFZWF2xh7wzhMLca+5yauDW4i8baFEv74QTeY1DADgI -Lz29NJ6z9xYzEnPesjNrwIcJwIjV52EkdLTi+EIf83UjXLQdwDbLxu76qxqP7K0I -oMmwbl7UNA0wzq7nmgRhvqhow5RoCaSJjTz0EYQVSa1xelwiKeJiSKj2G9Mgt5Ms -Miuy3C3VAGvQJ2ocILPGOt54oVeNRFLpnCo1e3sCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEAkGYtg21rGpUVT/AokGUfI0PeyYAkcXKy2yuBAzfRk+uIXnRR0vK+OMpx -shBoYGR3JEGUHZcMTRh8wjAZ0wuyYlQONtJbFFF3bCfODXxCsw0Vm8/Ms+KCmE4Z -SyQafWEQf1sdqNw4VS4DYS2mlpDgAl+U9UY6HQKuT3+GFIxCsQSdS0GTaiYVKPVE -p/eKou739h+5dM4FEhIYZX+7PWlHmX6wPCFAjgNu3kiRGmF6LKmCNNXTySATEP86 -tczQMzLtVdTg5z8XMi//6TkAPxRPjYi8Vef/s2mLo7KystTmofxI/HZePSieJ9tj -gLgK8d8sKL60JMmKHN3boHrsThKBVA== ------END CERTIFICATE-----', - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - - $installer = $this->getInstaller(); - $installer->downloadApp('news'); - } - - public function testDownloadAppWithNotNextcloudCertificate(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App with id news has a certificate not issued by a trusted Code Signing Authority'); - - $appArray = [ - [ - 'id' => 'news', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIID8TCCAdkCAhAAMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ8wDQYD -VQQIDAZCb3N0b24xFjAUBgNVBAoMDW93bkNsb3VkIEluYy4xNTAzBgNVBAMMLG93 -bkNsb3VkIENvZGUgU2lnbmluZyBJbnRlcm1lZGlhdGUgQXV0aG9yaXR5MB4XDTE2 -MDIwMzE3NTE0OVoXDTI2MDEzMTE3NTE0OVowDzENMAsGA1UEAwwEY29yZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPHdSljnHI+ueQd27UyWPO9n4Lqt -bK0kdekiC3si7Mee7uXXJaGuqXJozHEZYB1LIFLdCU/itCxEk9hyLcyNzeT+nRT/ -zDuOYdbLgCj7/A5bX+u3jc29UlCYybSFchfMdvn7a0njCna4dE+73b4yEj16tS2h -S1EUygSzgicWlJqMD3Z9Qc+zLEpdhq9oDdDB8HURi2NW4KzIraVncSH+zF1QduOh -nERDnF8x48D3FLdTxGA0W/Kg4gYsq4NRvU6g3DJNdp4YfqRSFMmLFDCgzDuhan7D -wgRlI9NAeHbnyoUPtrDBUceI7shIbC/i87xk9ptqV0AyFonkJtK6lWwZjNkCAwEA -ATANBgkqhkiG9w0BAQsFAAOCAgEAAMgymqZE1YaHYlRGwvTE7gGDY3gmFOMaxQL4 -E5m0CnkBz4BdIPRsQFFdOv3l/MIWkw5ED3vUB925VpQZYFSiEuv5NbnlPaHZlIMI -n8AV/sTP5jue3LhtAN4EM63xNBhudAT6wVsvGwOuQOx9Xv+ptO8Po7sTuNYP0CMH -EOQN+/q8tYlSm2VW+dAlaJ+zVZwZldhVjL+lSH4E9ktWn3PmgNQeKfcnJISUbus6 -ZtsYDF/X96/Z2ZQvMXOKksgvU6XlvIxllcyebC9Bxe/h0D63GCO2tqN5CWQzIIqn -apUynPX8BlLaaExqYGERwlUi/yOGaUVPUjEPVehviOQYgAqxlrkJk1dWeCrwUori -CXpi+IUYkidfgiJ9F88M3ElpwqIaXp7G3/4oHBuE2u6M+L+1/vqPJeTCAWUxxpJE -yYmM+db6D4TySFpQPENNzPS8bpR6T8w2hRumkldC42HrnyJJbpjOieTXhXzjdPvZ -IEP9JGtkhB2du6nBF2MNAq2TqRXpcfQrQEbnQ13aV9bl+roTwwO+SOWK/wgvdOMI -STQ0Xk0sTGlmQjPYPkibVceaWMR3sX4cNt5c33YhJys5jxHoAh42km4nN9tfykR5 -crl5lBlKjXh2GP0+omSO3x1jX4+iQPCW2TWoyKkUdLu/hGHG2w8RrTeme+kATECH -YSu356M= ------END CERTIFICATE-----', - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - - $installer = $this->getInstaller(); - $installer->downloadApp('news'); - } - - public function testDownloadAppWithDifferentCN(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App with id news has a cert issued to passman'); - - $appArray = [ - [ - 'id' => 'news', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhDUMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTkwMTMwMTMwMjA5WhcNMjkwNTA3MTMwMjA5WjASMRAwDgYD -VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwT1/ -hO/RSUkj0T9a879LPjy4c5V7FBDffEh6H/n1aiOEzofr8vqP3wJ4ZLwIvpZIZNFC -CY4HjBTIgk+QOlAv2oV2w/8XxkSQ708H3m99GFNRQg9EztjiIeKn7y1HhFOeiVaF -Eq6R1Tnre8cjzv6/yf1f1EFpPY3ptCefUjdLfpU/YrPhFxGLS+n5hyr8b6EszqKm -6NhGI09sd1Wb1y8o+dtQIQr24gWeo3l3QGLxjcJQqHCxE38rGdTNd04qDEm69BMD -Kjk4/JmUBBOn0svg9IAks+4nDnpr3IABfcnKYlmAucVEMNMYfA6kXXWEALsE2yo9 -8Y7GeV8En5Ztn4w3Pt2HMNpJV2m7MWWocSbF+ocp8oJ0cIEcthBubiE2kJhdPi5a -Yo5Bwh54hx53an+XfiDn+bfidvNz5TsJtmZykB84gLcdBQyMHrZcDcD6g74KdW3X -du/AnNUlJujhIU0fsw3UUPB845Q8XjbsIK5WhgaQeXJum8HXnXWkEfh6mE4j9l2Z -6FJVe8fQlF5lspn6z3qYsWlYRalD3N9Qxy3vpRgCAYTPc3D+fbVP9KJw1cWux1+O -0X/hNWMDOTenhgyQS+mqhRvdWq71mFjP/RXjOSfED3ipFamPofCz9QC7EERtvZPE -Pr8tBnbxnyTbAOQrN9N2mA9UJbOhk2l5kCSkDpMCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEAdVaizjIBQLGFo3uzOVAjh5IoFt/DBiSjVYOOt4HacWDDkRKOmQp3AJaA -Zecbd+tF7OsIqrBzbbfK7wp1+y1pLrPNFQghhk7gVxBP9LscLQ1JmDK2vHv9rLAg -d/gv90gf3HVeRQWntAhGTqtiSZkafcXJIRCM4GygoPy2zxeWeCTt85yEbQnuGZ4Z -qtWS/yyJR5ZQEwYG7jFWRxaZicuUdWBhlUMafRUxzjxBhsQplGKSI66eFQ5VtB7L -u/spPSSVhaun5BA1FlphB2TkgnzlCmxJa63nFY045e/Jq+IKMcqqZl/092gbI2EQ -5EpZaQ1l6H5DBXwrz58a8WTPC2Mu8g== ------END CERTIFICATE-----', - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - - $installer = $this->getInstaller(); - $installer->downloadApp('news'); - } - - public function testDownloadAppWithInvalidSignature(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App with id passman has invalid signature'); - - $appArray = [ - [ - 'id' => 'passman', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhDUMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTkwMTMwMTMwMjA5WhcNMjkwNTA3MTMwMjA5WjASMRAwDgYD -VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwT1/ -hO/RSUkj0T9a879LPjy4c5V7FBDffEh6H/n1aiOEzofr8vqP3wJ4ZLwIvpZIZNFC -CY4HjBTIgk+QOlAv2oV2w/8XxkSQ708H3m99GFNRQg9EztjiIeKn7y1HhFOeiVaF -Eq6R1Tnre8cjzv6/yf1f1EFpPY3ptCefUjdLfpU/YrPhFxGLS+n5hyr8b6EszqKm -6NhGI09sd1Wb1y8o+dtQIQr24gWeo3l3QGLxjcJQqHCxE38rGdTNd04qDEm69BMD -Kjk4/JmUBBOn0svg9IAks+4nDnpr3IABfcnKYlmAucVEMNMYfA6kXXWEALsE2yo9 -8Y7GeV8En5Ztn4w3Pt2HMNpJV2m7MWWocSbF+ocp8oJ0cIEcthBubiE2kJhdPi5a -Yo5Bwh54hx53an+XfiDn+bfidvNz5TsJtmZykB84gLcdBQyMHrZcDcD6g74KdW3X -du/AnNUlJujhIU0fsw3UUPB845Q8XjbsIK5WhgaQeXJum8HXnXWkEfh6mE4j9l2Z -6FJVe8fQlF5lspn6z3qYsWlYRalD3N9Qxy3vpRgCAYTPc3D+fbVP9KJw1cWux1+O -0X/hNWMDOTenhgyQS+mqhRvdWq71mFjP/RXjOSfED3ipFamPofCz9QC7EERtvZPE -Pr8tBnbxnyTbAOQrN9N2mA9UJbOhk2l5kCSkDpMCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEAdVaizjIBQLGFo3uzOVAjh5IoFt/DBiSjVYOOt4HacWDDkRKOmQp3AJaA -Zecbd+tF7OsIqrBzbbfK7wp1+y1pLrPNFQghhk7gVxBP9LscLQ1JmDK2vHv9rLAg -d/gv90gf3HVeRQWntAhGTqtiSZkafcXJIRCM4GygoPy2zxeWeCTt85yEbQnuGZ4Z -qtWS/yyJR5ZQEwYG7jFWRxaZicuUdWBhlUMafRUxzjxBhsQplGKSI66eFQ5VtB7L -u/spPSSVhaun5BA1FlphB2TkgnzlCmxJa63nFY045e/Jq+IKMcqqZl/092gbI2EQ -5EpZaQ1l6H5DBXwrz58a8WTPC2Mu8g== ------END CERTIFICATE-----', - 'releases' => [ - [ - 'download' => 'https://example.com', - 'signature' => 'MySignature', - ], - [ - 'download' => 'https://nextcloud.com', - ], - ], - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $realTmpFile = Server::get(ITempManager::class)->getTemporaryFile('.tar.gz'); - copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFile') - ->with('.tar.gz') - ->willReturn($realTmpFile); - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://example.com', ['sink' => $realTmpFile, 'timeout' => 120]); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - - $installer = $this->getInstaller(); - $installer->downloadApp('passman'); - } - - public function testDownloadAppWithMoreThanOneFolderDownloaded(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Extracted app testapp has more than 1 folder'); - - $appArray = [ - [ - 'id' => 'testapp', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD -VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x -FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j -ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe -svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr -l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 -6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ -me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 -KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ -l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z -yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 -dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg -gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj -nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G -z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx -89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ -Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 -cUg0V1uAxqUvKytKkMfcyPWsz/AINA== ------END CERTIFICATE-----', - 'releases' => [ - [ - 'download' => 'https://example.com', - 'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz -VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ -jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt -Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU -bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg -ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB -D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC -oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a -GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67 -0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/ -YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=', - ], - [ - 'download' => 'https://nextcloud.com', - ], - ], - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $realTmpFile = Server::get(ITempManager::class)->getTemporaryFile('.tar.gz'); - copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFile') - ->with('.tar.gz') - ->willReturn($realTmpFile); - $realTmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); - mkdir($realTmpFolder . '/testfolder'); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFolder') - ->willReturn($realTmpFolder); - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://example.com', ['sink' => $realTmpFile, 'timeout' => 120]); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - - $installer = $this->getInstaller(); - $installer->downloadApp('testapp'); - } - - public function testDownloadAppWithMismatchingIdentifier(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App for id testapp has a wrong app ID in info.xml: testapp1'); - - $appArray = [ - [ - 'id' => 'testapp', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD -VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x -FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j -ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe -svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr -l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 -6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ -me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 -KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ -l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z -yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 -dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg -gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj -nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G -z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx -89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ -Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 -cUg0V1uAxqUvKytKkMfcyPWsz/AINA== ------END CERTIFICATE-----', - 'releases' => [ - [ - 'download' => 'https://example.com', - 'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz -VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ -jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt -Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU -bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg -ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB -D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC -oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a -GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67 -0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/ -YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=', - ], - [ - 'download' => 'https://nextcloud.com', - ], - ], - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $realTmpFile = Server::get(ITempManager::class)->getTemporaryFile('.tar.gz'); - copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFile') - ->with('.tar.gz') - ->willReturn($realTmpFile); - $realTmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFolder') - ->willReturn($realTmpFolder); - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://example.com', ['sink' => $realTmpFile, 'timeout' => 120]); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - - $installer = $this->getInstaller(); - $installer->downloadApp('testapp'); - } - - public function testDownloadAppSuccessful(): void { - $appArray = [ - [ - 'id' => 'testapp', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD -VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x -FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j -ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe -svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr -l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 -6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ -me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 -KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ -l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z -yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 -dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg -gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj -nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G -z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx -89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ -Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 -cUg0V1uAxqUvKytKkMfcyPWsz/AINA== ------END CERTIFICATE-----', - 'releases' => [ - [ - 'download' => 'https://example.com', - 'signature' => 'O5UWFRnSx4mSdEX83Uh9u7KW+Gl1OWU4uaFg6aYY19zc+lWP4rKCbAUH7Jo1Bohf -qxQbhXs4cMqGmoL8dW4zeFUqSJCRk52LA+ciLezjPFv275q+BxEgyWOylLnbhBaz -+v6lXLaeG0J/ry8wEdg+rwP8FCYPsvKlXSVbFjgubvCR/owKJJf5iL0B93noBwBN -jfbcxi7Kh16HAKy6f/gVZ6hf/4Uo7iEFMCPEHjidope+ejUpqbd8XhQg5/yh7TQ7 -VKR7pkdDG2eFr5c3CpaECdNg5ZIGRbQNJHBXHT/wliorWpYJtwtNAQJ4xC635gLP -4klkKN4XtSj8bJUaJC6aaksLFgRSeKXaYAHai/XP6BkeyNzlSbsmyZk8cZbySx8F -gVOzPok1c94UGT57FjeW5eqRjtmzbYivQdP89Ouz6et7PY69yOCqiRFQanrqzwoX -MPLX6f5V9tCJtlH6ztmEcDROfvuVc0U3rEhqx2hphoyo+MZrPFpdcJL8KkIdMKbY -7yQWrsV7QvAzygAOFsC0TlSNJbmMCljouUk9di4CUZ+xsQ6n6TZtE7gsdljlKjPS -3Ys+e3V1HUaVzv8SaSmKwjRoQxQxHWLtXpJS2Yq+i+gq7LuC+aStzxAzV/h2plDW -358picx/PobNDi71Q97+/CAOq+4wDOwhKwls7lwudIs=', - ], - [ - 'download' => 'https://nextcloud.com', - ], - ], - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $realTmpFile = Server::get(ITempManager::class)->getTemporaryFile('.tar.gz'); - copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFile') - ->with('.tar.gz') - ->willReturn($realTmpFile); - $realTmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFolder') - ->willReturn($realTmpFolder); - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://example.com', ['sink' => $realTmpFile, 'timeout' => 120]); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - - $installer = $this->getInstaller(); - $installer->downloadApp('testapp'); - - $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); - $this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); - } - - public function testDownloadAppWithDowngrade(): void { - // Use previous test to download the application in version 0.9 - $this->testDownloadAppSuccessful(); - - // Reset mocks - $this->appFetcher = $this->createMock(AppFetcher::class); - $this->clientService = $this->createMock(IClientService::class); - $this->tempManager = $this->createMock(ITempManager::class); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App for id testapp has version 0.9 and tried to update to lower version 0.8'); - - $appArray = [ - [ - 'id' => 'testapp', - 'certificate' => '-----BEGIN CERTIFICATE----- -MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD -VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI -MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB -dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD -VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x -FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j -ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe -svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr -l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 -6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ -me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 -KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ -l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z -yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 -dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg -gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj -nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G -z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx -89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ -Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 -cUg0V1uAxqUvKytKkMfcyPWsz/AINA== ------END CERTIFICATE-----', - 'releases' => [ - [ - 'download' => 'https://example.com', - 'signature' => 'KMSao4cKdMIYxeT8Bm4lrmSeIQnk7YzJZh+Vz+4LVSBwF+OMmcujryQuWLXmbPfg -4hGI9zS025469VNjUoCprn01H8NBq3O1cXz+ewG1oxYWMMQFZDkOtUQ+XZ27b91t -y0l45H6C8j0sTeSrUb/LCjrdm+buUygkhC2RZxCI6tLi4rYWj0MiqDz98XkbB3te -pW3ZND6mG6Jxn1fnd35paqZ/+URMftoLQ4K+6vJoBVGnug9nk1RpGLouICI0zCrz -YPTsBHo0s2mPvQQ/ASacWYmSe5R6r5JCzNeGMpViGCqCYPbwuebgqK079s2zvSF9 -mSLAm2Tk6gCM29N8Vdfr6ppCvIbuNzlLU/dGdYHAILgxEsm/odZjt1Fhs4lOo3A5 -9ToaNl5+qOEkggwfE/QqceHAY2soW9V5d9izhTCDgXmxpPpPXkwPPTz04ZUpi1Yc -OdZZOswbEcc2jUC5T7a7Tnp0uBOkdqat6jB4oMGwU1ldYLCGRyy546cPPTXJw5kH -9WfeKJ/mavrSLVa7QqZ4RCcMigmijT1kdqbaEh05IZNrzs6VDcS2EIrbDX8SGXUk -uDDkPXZEXqNDEjyONfDXVRLiqDa52Gg+I4vW/l/4ZOFgAWdZkqPPuZFaqzZpsJXm -JXhrdaWDZ8fzpUjugrtC3qslsqL0dzgU37anS3HwrT8=', - ], - [ - 'download' => 'https://nextcloud.com', - ], - ], - ], - ]; - $this->appFetcher - ->expects($this->once()) - ->method('get') - ->willReturn($appArray); - $realTmpFile = Server::get(ITempManager::class)->getTemporaryFile('.tar.gz'); - copy(__DIR__ . '/../data/testapp.0.8.tar.gz', $realTmpFile); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFile') - ->with('.tar.gz') - ->willReturn($realTmpFile); - $realTmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); - $this->tempManager - ->expects($this->once()) - ->method('getTemporaryFolder') - ->willReturn($realTmpFolder); - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with('https://example.com', ['sink' => $realTmpFile, 'timeout' => 120]); - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->willReturn($client); - $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); - $this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); - - $this->appManager - ->expects($this->once()) - ->method('getAppVersion') - ->willReturn('0.9'); - - $installer = $this->getInstaller(); - $installer->downloadApp('testapp'); - $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); - $this->assertEquals('0.8', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); - } -} --- a/tests/lib/IntegrityCheck/CheckerTest.php +++ /dev/null @@ -1,1158 +0,0 @@ -serverVersion = $this->createMock(ServerVersion::class); - $this->environmentHelper = $this->createMock(EnvironmentHelper::class); - $this->fileAccessHelper = $this->createMock(FileAccessHelper::class); - $this->config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->mimeTypeDetector = $this->createMock(Detection::class); - - $this->config->method('getAppValue') - ->willReturnArgument(2); - - $this->cacheFactory - ->expects($this->any()) - ->method('createDistributed') - ->with('oc.integritycheck.checker') - ->willReturn(new NullCache()); - - $this->checker = new Checker( - $this->serverVersion, - $this->environmentHelper, - $this->fileAccessHelper, - $this->config, - $this->appConfig, - $this->cacheFactory, - $this->appManager, - $this->mimeTypeDetector - ); - } - - public function testWriteAppSignatureOfNotExistingApp(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Exception message'); - - $this->fileAccessHelper - ->expects($this->once()) - ->method('assertDirectoryExists') - ->with('NotExistingApp/appinfo') - ->willThrowException(new \Exception('Exception message')); - $this->fileAccessHelper - ->expects($this->once()) - ->method('is_writable') - ->with('NotExistingApp/appinfo') - ->willReturn(true); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeAppSignature('NotExistingApp', $x509, $rsa); - } - - public function testWriteAppSignatureWrongPermissions(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches('/[a-zA-Z\\/_-]+ is not writable/'); - - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->willThrowException(new \Exception('Exception message')); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeAppSignature(\OC::$SERVERROOT . '/tests/data/integritycheck/app/', $x509, $rsa); - } - - public function testWriteAppSignature(): void { - $expectedSignatureFileData = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "Y5yvXvcGHVPuRRatKVDUONWq1FpLXugZd6Km\/+aEHsQj7coVl9FeMj9OsWamBf7yRIw3dtNLguTLlAA9QAv\/b0uHN3JnbNZN+dwFOve4NMtqXfSDlWftqKN00VS+RJXpG1S2IIx9Poyp2NoghL\/5AuTv4GHiNb7zU\/DT\/kt71pUGPgPR6IIFaE+zHOD96vjYkrH+GfWZzKR0FCdLib9yyNvk+EGrcjKM6qjs2GKfS\/XFjj\/\/neDnh\/0kcPuKE3ZbofnI4TIDTv0CGqvOp7PtqVNc3Vy\/UKa7uF1PT0MAUKMww6EiMUSFZdUVP4WWF0Y72W53Qdtf1hrAZa2kfKyoK5kd7sQmCSKUPSU8978AUVZlBtTRlyT803IKwMV0iHMkw+xYB1sN2FlHup\/DESADqxhdgYuK35bCPvgkb4SBe4B8Voz\/izTvcP7VT5UvkYdAO+05\/jzdaHEmzmsD92CFfvX0q8O\/Y\/29ubftUJsqcHeMDKgcR4eZOE8+\/QVc\/89QO6WnKNuNuV+5bybO6g6PAdC9ZPsCvnihS61O2mwRXHLR3jv2UleFWm+lZEquPKtkhi6SLtDiijA4GV6dmS+dzujSLb7hGeD5o1plZcZ94uhWljl+QIp82+zU\/lYB1Zfr4Mb4e+V7r2gv7Fbv7y6YtjE2GIQwRhC5jq56bD0ZB+I=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEwTCCAqmgAwIBAgIUWv0iujufs5lUr0svCf\/qTQvoyKAwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIyNDk1M1oXDTE2MTEwMzIyNDk1M1owEjEQMA4GA1UEAwwHU29tZUFwcDCCAiIw\r\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK8q0x62agGSRBqeWsaeEwFfepMk\r\nF8cAobMMi50qHCv9IrOn\/ZH9l52xBrbIkErVmRjmly0d4JhD8Ymhidsh9ONKYl\/j\r\n+ishsZDM8eNNdp3Ew+fEYVvY1W7mR1qU24NWj0bzVsClI7hvPVIuw7AjfBDq1C5+\r\nA+ZSLSXYvOK2cEWjdxQfuNZwEZSjmA63DUllBIrm35IaTvfuyhU6BW9yHZxmb8+M\r\nw0xDv30D5UkE\/2N7Pa\/HQJLxCR+3zKibRK3nUyRDLSXxMkU9PnFNaPNX59VPgyj4\r\nGB1CFSToldJVPF4pzh7p36uGXZVxs8m3LFD4Ol8mhi7jkxDZjqFN46gzR0r23Py6\r\ndol9vfawGIoUwp9LvL0S7MvdRY0oazLXwClLP4OQ17zpSMAiCj7fgNT661JamPGj\r\nt5O7Zn2wA7I4ddDS\/HDTWCu98Zwc9fHIpsJPgCZ9awoqxi4Mnf7Pk9g5nnXhszGC\r\ncxxIASQKM+GhdzoRxKknax2RzUCwCzcPRtCj8AQT\/x\/mqN3PfRmlnFBNACUw9bpZ\r\nSOoNq2pCF9igftDWpSIXQ38pVpKLWowjjg3DVRmVKBgivHnUnVLyzYBahHPj0vaz\r\ntFtUFRaqXDnt+4qyUGyrT5h5pjZaTcHIcSB4PiarYwdVvgslgwnQzOUcGAzRWBD4\r\n6jV2brP5vFY3g6iPAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACTY3CCHC+Z28gCf\r\nFWGKQ3wAKs+k4+0yoti0qm2EKX7rSGQ0PHSas6uW79WstC4Rj+DYkDtIhGMSg8FS\r\nHVGZHGBCc0HwdX+BOAt3zi4p7Sf3oQef70\/4imPoKxbAVCpd\/cveVcFyDC19j1yB\r\nBapwu87oh+muoeaZxOlqQI4UxjBlR\/uRSMhOn2UGauIr3dWJgAF4pGt7TtIzt+1v\r\n0uA6FtN1Y4R5O8AaJPh1bIG0CVvFBE58esGzjEYLhOydgKFnEP94kVPgJD5ds9C3\r\npPhEpo1dRpiXaF7WGIV1X6DI\/ipWvfrF7CEy6I\/kP1InY\/vMDjQjeDnJ\/VrXIWXO\r\nyZvHXVaN\/m+1RlETsH7YO\/QmxRue9ZHN3gvvWtmpCeA95sfpepOk7UcHxHZYyQbF\r\n49\/au8j+5tsr4A83xzsT1JbcKRxkAaQ7WDJpOnE5O1+H0fB+BaLakTg6XX9d4Fo7\r\n7Gin7hVWX7pL+JIyxMzME3LhfI61+CRcqZQIrpyaafUziPQbWIPfEs7h8tCOWyvW\r\nUO8ZLervYCB3j44ivkrxPlcBklDCqqKKBzDP9dYOtS\/P4RB1NkHA9+NTvmBpTonS\r\nSFXdg9fFMD7VfjDE3Vnk+8DWkVH5wBYowTAD7w9Wuzr7DumiAULexnP\/Y7xwxLv7\r\n4B+pXTAcRK0zECDEaX3npS8xWzrB\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects(self::any()) - ->method('is_writable') - ->with( - $this->equalTo(\OC::$SERVERROOT . '/tests/data/integritycheck/app//appinfo'), - ) - ->willReturn(true); - - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->with( - $this->equalTo(\OC::$SERVERROOT . '/tests/data/integritycheck/app//appinfo/signature.json'), - $this->callback(function ($signature) use ($expectedSignatureFileData) { - $expectedArray = json_decode($expectedSignatureFileData, true); - $actualArray = json_decode($signature, true); - $this->assertEquals($expectedArray, $actualArray); - return true; - }) - ); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeAppSignature(\OC::$SERVERROOT . '/tests/data/integritycheck/app/', $x509, $rsa); - } - - public function testVerifyAppSignatureWithoutSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\IntegrityCheck\Exceptions\InvalidSignatureException', - 'message' => 'Signature data not found.', - ], - ]; - $this->assertSame($expected, $this->checker->verifyAppSignature('SomeApp')); - } - - public function testVerifyAppSignatureWithValidSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->once()) - ->method('getAppPath') - ->with('SomeApp') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyAppSignature('SomeApp')); - } - - public function testVerifyAppSignatureWithTamperedSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->once()) - ->method('getAppPath') - ->with('SomeApp') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "tampered", - "subfolder\/file.txt": "tampered" - }, - "signature": "EL49UaSeyMAqyMtqId+tgOhhwgOevPZsRLX4j2blnybAB6fN07z0936JqZV7+eMPiE30Idx+UCY6rCFN531Kqe9vAOCdgtHUSOjjKyKc+lvULESlMb6YQcrZrvDlEMMjzjH49ewG7Ai8sNN6HrRUd9U8ws+ewSkW2DOOBItj\/21RBnkrSt+2AtGXGigEvuTm57HrCYDj8\/lSkumC2GVkjLUHeLOKYo4PRNOr6yP5mED5v7zo66AWvXl2fKv54InZcdxsAk35lyK9DGZbk\/027ZRd0AOHT3LImRLvQ+8EAg3XLlRUy0hOFGgPC+jYonMzgYvsAXAXi2j8LnLJlsLwpFwu1k1B+kZVPMumKZvP9OvJb70EirecXmz62V+Jiyuaq7ne4y7Kp5gKZT\/T8SeZ0lFtCmPfYyzBB0y8s5ldmTTmdVYHs54t\/OCCW82HzQZxnFNPzDTRa8HglsaMKrqPtW59+R4UvRKSWhB8M\/Ah57qgzycvPV4KMz\/FbD4l\/\/9chRKSlCfc2k3b8ZSHNmi+EzCKgJjWIoKdgN1yax94puU8jfn8UW+G7H9Y1Jsf\/jox6QLyYEgtV1vOHY2xLT7fVs2vhyvkN2MNjJnmQ70gFG5Qz2lBz5wi6ZpB+tOfCcpbLxWAkoWoIrmC\/Ilqh7mfmRZ43g5upjkepHNd93ONuY8=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEwTCCAqmgAwIBAgIUWv0iujufs5lUr0svCf\/qTQvoyKAwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIyNDk1M1oXDTE2MTEwMzIyNDk1M1owEjEQMA4GA1UEAwwHU29tZUFwcDCCAiIw\r\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK8q0x62agGSRBqeWsaeEwFfepMk\r\nF8cAobMMi50qHCv9IrOn\/ZH9l52xBrbIkErVmRjmly0d4JhD8Ymhidsh9ONKYl\/j\r\n+ishsZDM8eNNdp3Ew+fEYVvY1W7mR1qU24NWj0bzVsClI7hvPVIuw7AjfBDq1C5+\r\nA+ZSLSXYvOK2cEWjdxQfuNZwEZSjmA63DUllBIrm35IaTvfuyhU6BW9yHZxmb8+M\r\nw0xDv30D5UkE\/2N7Pa\/HQJLxCR+3zKibRK3nUyRDLSXxMkU9PnFNaPNX59VPgyj4\r\nGB1CFSToldJVPF4pzh7p36uGXZVxs8m3LFD4Ol8mhi7jkxDZjqFN46gzR0r23Py6\r\ndol9vfawGIoUwp9LvL0S7MvdRY0oazLXwClLP4OQ17zpSMAiCj7fgNT661JamPGj\r\nt5O7Zn2wA7I4ddDS\/HDTWCu98Zwc9fHIpsJPgCZ9awoqxi4Mnf7Pk9g5nnXhszGC\r\ncxxIASQKM+GhdzoRxKknax2RzUCwCzcPRtCj8AQT\/x\/mqN3PfRmlnFBNACUw9bpZ\r\nSOoNq2pCF9igftDWpSIXQ38pVpKLWowjjg3DVRmVKBgivHnUnVLyzYBahHPj0vaz\r\ntFtUFRaqXDnt+4qyUGyrT5h5pjZaTcHIcSB4PiarYwdVvgslgwnQzOUcGAzRWBD4\r\n6jV2brP5vFY3g6iPAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACTY3CCHC+Z28gCf\r\nFWGKQ3wAKs+k4+0yoti0qm2EKX7rSGQ0PHSas6uW79WstC4Rj+DYkDtIhGMSg8FS\r\nHVGZHGBCc0HwdX+BOAt3zi4p7Sf3oQef70\/4imPoKxbAVCpd\/cveVcFyDC19j1yB\r\nBapwu87oh+muoeaZxOlqQI4UxjBlR\/uRSMhOn2UGauIr3dWJgAF4pGt7TtIzt+1v\r\n0uA6FtN1Y4R5O8AaJPh1bIG0CVvFBE58esGzjEYLhOydgKFnEP94kVPgJD5ds9C3\r\npPhEpo1dRpiXaF7WGIV1X6DI\/ipWvfrF7CEy6I\/kP1InY\/vMDjQjeDnJ\/VrXIWXO\r\nyZvHXVaN\/m+1RlETsH7YO\/QmxRue9ZHN3gvvWtmpCeA95sfpepOk7UcHxHZYyQbF\r\n49\/au8j+5tsr4A83xzsT1JbcKRxkAaQ7WDJpOnE5O1+H0fB+BaLakTg6XX9d4Fo7\r\n7Gin7hVWX7pL+JIyxMzME3LhfI61+CRcqZQIrpyaafUziPQbWIPfEs7h8tCOWyvW\r\nUO8ZLervYCB3j44ivkrxPlcBklDCqqKKBzDP9dYOtS\/P4RB1NkHA9+NTvmBpTonS\r\nSFXdg9fFMD7VfjDE3Vnk+8DWkVH5wBYowTAD7w9Wuzr7DumiAULexnP\/Y7xwxLv7\r\n4B+pXTAcRK0zECDEaX3npS8xWzrB\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Signature could not get verified.', - ], - ]; - $this->assertEquals($expected, $this->checker->verifyAppSignature('SomeApp')); - } - - public function testVerifyAppSignatureWithTamperedFiles(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->once()) - ->method('getAppPath') - ->with('SomeApp') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'INVALID_HASH' => [ - 'AnotherFile.txt' => [ - 'expected' => '1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112', - 'current' => '7322348ba269c6d5522efe02f424fa3a0da319a7cd9c33142a5afe32a2d9af2da3a411f086fcfc96ff4301ea566f481dba0960c2abeef3594c4d930462f6584c', - ], - ], - 'FILE_MISSING' => [ - 'subfolder/file.txt' => [ - 'expected' => '410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b', - 'current' => '', - ], - ], - 'EXTRA_FILE' => [ - 'UnecessaryFile' => [ - 'expected' => '', - 'current' => 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', - ], - ], - ]; - $this->assertSame($expected, $this->checker->verifyAppSignature('SomeApp')); - } - - public function testVerifyAppSignatureWithTamperedFilesAndAlternatePath(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->never()) - ->method('getAppPath') - ->with('SomeApp'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'INVALID_HASH' => [ - 'AnotherFile.txt' => [ - 'expected' => '1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112', - 'current' => '7322348ba269c6d5522efe02f424fa3a0da319a7cd9c33142a5afe32a2d9af2da3a411f086fcfc96ff4301ea566f481dba0960c2abeef3594c4d930462f6584c', - ], - ], - 'FILE_MISSING' => [ - 'subfolder/file.txt' => [ - 'expected' => '410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b', - 'current' => '', - ], - ], - 'EXTRA_FILE' => [ - 'UnecessaryFile' => [ - 'expected' => '', - 'current' => 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', - ], - ], - ]; - $this->assertSame($expected, $this->checker->verifyAppSignature('SomeApp', \OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData/')); - } - - public function testVerifyAppWithDifferentScope(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->once()) - ->method('getAppPath') - ->with('SomeApp') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "eXesvDm3pkek12xSwMG10y9suRES79Nye3jYNe5KYq1tTUPqRRNgxmMGAfcUro0zpLeAr2YgHeSMWtglblGOW7pmwGVPZ0O1Y4r1fE6jnep0kW+35PLIaqCorIOnCAtSzDNKBhwd1ow3zW2wC0DFouuEkIO8u5Fw28g8E8dp8zEk1xMblNPy+xtWkmYHrVJ\/dQgun1bYOF2ZFtAzatwndTI\/bGsy1i3Wsl+x6HyWKQdq8y8VObtOqKDH7uERBEpB9DHVyKflj1v1gQuEH6BhaRdATc7ee0MiQdGblraIySwYRdfo2d8i82OVKrenMB3SLwyCvDPyQ9iKpTOnSF52ZBqaqSXKM2N\/RAkweeBFQQCwcHhqxvB0cfbyHcbkOLeCZe\/tsh68IxwTiYgzvLfl7sOZ5arnZbzrPpZmB+hfV2omkoJ1tDwOWz9hEmLLNtfo2OxyUH1m0+XFaC+Gbn4WkVDgf7YZkwUcG+Qoa3oKDNMss8MEyZxewl2iDGZcf402dlidHRprlfmXbAYuVQ08\/a0HxIKYPGh\/nsMGmwnO15CWtFpAbhUA\/D5oRjsIxnvXaMDg0iAFpdu\/5Ffsj7g3EPdBkiQHNYK7YU1RRx609eH0bZyiIYHdUPw7ikLupvrebZmELqi3mqDFO99u4eISlxFJlUbUND3L4BtmWTWrKwI=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIExjCCAq6gAwIBAgIUHSJjhJqMwr+3TkoiQFg4SVVYQ1gwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIzMjc1NVoXDTE2MTEwMzIzMjc1NVowFzEVMBMGA1UEAwwMQW5vdGhlclNjb3Bl\r\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA33npb5RmUkXrDT+TbwMf\r\n0zQ33SlzsjoGxCrbSwJOn6leGGInJ6ZrdzLL0WTi\/dTpg+Y\/JS+72XWm5NSjaTxo\r\n7OHc3cQBwXQj4tN6j\/y5qqY0GDLYufEkx2rpazqt9lBSJ72u1bGl2yoOXzYCz5i0\r\n60KsJXC9K44LKzGsarzbwAgskSVNkjAsPgjnCWZmcl6icpLi5Fz9rs2UMOWbdvdI\r\nAROsn0eC9E\/akmXTy5YMu6bAIGpvjZFHzyA83FQRbvv5o1V5Gsye\/VQLEgh7rqfz\r\nT\/jgWifP+JgoeB6otzuRZ3fFsmbBiyCIRtIOzQQflozhUlWtmiEGwg4GySuMUjEH\r\nA1LF86LO+ZzDQgd2oYNKmrQ8O+EcLqx9BpV4AFhEvqdk7uycJYPHs6yl+yfbzTeJ\r\n2Xd0yVAfd9r\/iDr36clLj2bzEObdl9xzKjcCIXE4Q0G4Pur41\/BJUDK9PI390ccQ\r\nnFjjVYBMsC859OwW64tMP0zkM9Vv72LCaEzaR8jqH0j11catqxunr+StfMcmxLTN\r\nbqBJbSEq4ER3mJxCTI2UrIVmdQ7+wRxgv3QTDNOZyqrz2L8A1Rpb3h0APxtQv+oA\r\n8KIZYID5\/qsS2V2jITkMQ8Nd1W3b0cZhZ600z+znh3jLJ0TYLvwN6\/qBQTUDaM2o\r\ng1+icMqXIXIeKuoPCVVsG7cCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAHc4F\/kOV\r\nHc8In5MmGg2YtjwZzjdeoC5TIPZczRqz0B+wRbJzN6aYryKZKLmP+wKpgRnJWDzp\r\nrgKGyyEQIAfK63DEv4B9p4N1B+B3aeMKsSpVcw7wbFTD57V5A7pURGoo31d0mw5L\r\nUIXZ2u+TUfGbzucMxLdFhTwjGpz9M6Kkm\/POxmV0tvLija5LdbdKnYR9BFmyu4IX\r\nqyoIAtComATNLl+3URu3SZxhE3NxhzMz+eAeNfh1KuIf2gWIIeDCXalVSJLym+OQ\r\nHFDpqRhJqfTMprrRlmmU7Zntgbj8\/RRZuXnBvH9cQ2KykLOb4UoCPlGUqOqKyP9m\r\nDJSFRiMJfpgMQUaJk1TLhKF+IR6FnmwURLEtkONJumtDQju9KaWPlhueONdyGi0p\r\nqxLVUo1Vb52XnPhk2GEEduxpDc9V5ePJ+pdcEdMifY\/uPNBRuBj2c87yq1DLH+U4\r\n3XzP1MlwjnBWZYuoFo0j6Jq0r\/MG6HjGdmkGIsRoheRi8Z8Scz5AW5QRkNz8pKop\r\nTELFqQy9g6TyQzzC8t6HZcpNe842ZUk4raEAbCZe\/XqxWMw5svPgNceBqM3fh7sZ\r\nBSykOHLaL8kiRO\/IS3y1yZEAuiWBvtxcTNLzBb+hdRpm2y8\/qH\/pKo+CMj1VzjNT\r\nD8YRQg0cjmDytJzHDrtV\/aTc9W1aPHun0vw=\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Certificate is not valid for required scope. (Requested: SomeApp, current: CN=AnotherScope)', - ], - ]; - $this->assertSame($expected, $this->checker->verifyAppSignature('SomeApp')); - } - - public function testVerifyAppWithDifferentScopeAndAlwaysTrustedCore(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->appManager - ->expects($this->once()) - ->method('getAppPath') - ->with('SomeApp') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//appinfo/signature.json', $signatureDataFile], - ['/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyAppSignature('SomeApp')); - } - - public function testWriteCoreSignatureWithException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Exception message'); - - $this->fileAccessHelper - ->expects($this->once()) - ->method('assertDirectoryExists') - ->willThrowException(new \Exception('Exception message')); - $this->fileAccessHelper - ->expects($this->once()) - ->method('is_writable') - ->with(__DIR__ . '/core') - ->willReturn(true); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, __DIR__); - } - - public function testWriteCoreSignatureWrongPermissions(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches('/[a-zA-Z\\/_-]+ is not writable/'); - - $this->fileAccessHelper - ->expects($this->once()) - ->method('assertDirectoryExists') - ->willThrowException(new \Exception('Exception message')); - $this->fileAccessHelper - ->expects($this->once()) - ->method('is_writable') - ->with(__DIR__ . '/core') - ->willReturn(false); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/SomeApp.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, __DIR__); - } - - public function testWriteCoreSignature(): void { - $expectedSignatureFileData = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->with( - \OC::$SERVERROOT . '/tests/data/integritycheck/app//core/signature.json', - $this->callback(function ($signature) use ($expectedSignatureFileData) { - $expectedArray = json_decode($expectedSignatureFileData, true); - $actualArray = json_decode($signature, true); - $this->assertEquals($expectedArray, $actualArray); - return true; - }) - ); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/core.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/core.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, \OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - } - - public function testWriteCoreSignatureWithUnmodifiedHtaccess(): void { - $expectedSignatureFileData = '{ - "hashes": { - ".htaccess": "dc479770a6232061e04a768ee1f9133fdb3aea7b3a99f7105b0e0b6197474733e8d14b5b2bbad054e6b62a410fe5d0b3d790242dee1e0f11274af2100f5289e2", - "subfolder\/.htaccess": "2c57b1e25050e11dc3ae975832f378c452159f7b69f818e47eeeafadd6ba568517461dcb4d843b90b906cd7c89d161bc1b89dff8e3ae0eb6f5088508c47befd1" - }, - "signature": "nRtR377DB\/I\/4hmh9q3elMQYfSHnQFlNtjchNgrdfmUQqVmgkU\/4qgGyxDqYkV8mSMbH2gYysfP42nx\/3zSo7n0dBYDfU87Q6f96Cv597vEV27do8CaBkEk8Xjn2SxhHw8hVxracvE2OBAPxk0H3sRp\/cQBgjoXpju4kQin0N5E+DEJMh7Sp+u8aKoFpb+2FaAZJFn\/hnqxLTlVi2nyDxGL3U0eobWY+jWH9XPt52v3Hyh8TDhcAnQ1cN30B8Jn2+jkrm8ib+buchaCXHk0cPX72xuPECdwOEKLCBNrJa3FGSvO1zWiecnCgxCXgt+R8hUgsVPTsbrdFY2YRJGIhHndYZL98XzgG7cw85SnnMMe2SulzeL7xANGF8qiEVyiC7x83bbj5xOkeM\/CUTajrLBO3vyZ23KKOxvskjgI0t+Zw1zFsl+sYW0\/O\/V5WzPOwMwV8+iApQ8k9gEMiYQg98QLEMYnSohncmp0Z9qx2qFcQuHLcKJVa1J6wGtE\/EHR\/4d0aYPd6IRjg+qshCJmdzud\/12xjpGTl+BT0Hi0VsU5o7ZMi7WhmukZmmv8u0uZsvKREQNATm4cO4WCkYySt5O9gZEJOF+jjgeynDoAh09lyrNXIgMpM9ufm\/XEG\/I\/f2zIwbAUc6J6qks5OuYlJzW5vscTiOKhwcGZU9WBLgh0=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/htaccessUnmodified/'); - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->with( - \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessUnmodified//core/signature.json', - $this->callback(function ($signature) use ($expectedSignatureFileData) { - $expectedArray = json_decode($expectedSignatureFileData, true); - $actualArray = json_decode($signature, true); - $this->assertEquals($expectedArray, $actualArray); - return true; - }) - ); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/core.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/core.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessUnmodified/'); - } - - public function testWriteCoreSignatureWithInvalidModifiedHtaccess(): void { - $expectedSignatureFileData = '{ - "hashes": { - ".htaccess": "4a54273dc8d697b2ca615acf2ae2c1ee3c1c643492cb04f42b10984fa9aacff1420dc829fd82f93ad3476fbd0cdab0251142c887dc8f872d03e39a3a3eb6d381" - }, - "signature": "qpDddYGgAKNR3TszOgjPXRphUl2P9Ym5OQaetltocgZASGDkOun5D64+1D0QJRKb4SG2+48muxGOHyL2Ngos4NUrrSR+SIkywZacay82YQBCEdr7\/4MjW1WHRPjvboLwEJwViw0EdAjsWRpD68aPnzUGrGsy2BsCo06P5iwjk9cXcHxdjC9R39npvoC3QNvQ2jmNIbh1Lc4U97dbb+CsXEQCLU1OSa9p3q6cEFV98Easwt7uF\/DzHK+CbeZlxVZ0DwLh2\/ylT1PyGou8QC1b3vKAnPjLWMO+UsCPpCKhk3C5pV+5etQ8puGd+0x2t5tEU+qXxLzek91zWNC+rqgC\/WlqLKbwPb\/BCHs4zLGV55Q2fEQmT21x0KCUELdPs4dBnYP4Ox5tEDugtJujWFzOHzoY6gGa\/BY\/78pSZXmq9o8dWkBEtioWWvaNZ1rM0ddE83GBlBTgjigi9Ay1D++bUW\/FCBB7CMk6qyNlV81H+cBuIEODw2aymmkM9LLDD2Qbmvo8gHEPRjiQxPC5OpDlcdSNiL+zcxVxeuX4FpT+9xzz\/\/DRONhufxRpsbuCOMxd96RW7y9U2N2Uxb3Bzn\/BIqEayUUsdgZjfaGcXXYKR+chu\/LOwNYN6RlnLsgqL\/dhGKwlRVKXw1RA2\/af\/CpqyR7uVP6al1YJo\/YJ+5XJ6zE=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->with( - \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithInvalidModifiedContent//core/signature.json', - $this->callback(function ($signature) use ($expectedSignatureFileData) { - $expectedArray = json_decode($expectedSignatureFileData, true); - $actualArray = json_decode($signature, true); - $this->assertEquals($expectedArray, $actualArray); - return true; - }) - ); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/core.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/core.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithInvalidModifiedContent/'); - } - - public function testWriteCoreSignatureWithValidModifiedHtaccess(): void { - $expectedSignatureFileData = '{ - "hashes": { - ".htaccess": "ee900c70c32f5475f301648f790f0836f330fe77af94154dfec7d290327c14b505aaea27945eb6862eaa104bbf8346c011752767af455b77b6676e382bcd5344", - "subfolder\/.htaccess": "2c57b1e25050e11dc3ae975832f378c452159f7b69f818e47eeeafadd6ba568517461dcb4d843b90b906cd7c89d161bc1b89dff8e3ae0eb6f5088508c47befd1" - }, - "signature": "LhHHUjQlsNacPWGO7dmnDYNU4pvuVuaXAG01w41A6ijwTB1ii3khtaAJcsT4HYoDWLZ9KnmAYPwmYWjEh9xvzvC7arOFsZFixSjaEjNQADqwUwacyNCyJ4an3JRw/nZPJqAgCDOtr3pJWixf2v7qOWbJrlu7yB/SF3wiSXRfE1s6Y8jAMERZJ5bfMjiLC1yXJ4VpIzn5ed3RnWfl0MWtPr/XEtOLkh+eoSExhmDw91w/gyJo4/+iCnnLjKDsIEPRcoe2/t4azaxdvCIwlStLsQXygXNUySeO2m2HnjZOCHy2E4MfTbrV8XuT3wPEYHWQTc3QAMehbjEsctTaI4RAoKbNtnjWrBqP+Z10cpIfiIENFMvMYu/mSzhZH84rjoywWaZ+MyQ+LxeMiugVZKLEEAWQ7rCKjBEILnJ1ssrTOkBf7tmom52FvCSTNkieEBNzupR5GjePRXfzrSXw56Yg8veq9ALkQH7yXviEiHlRDLa0F1MfOjp4QgJzH0yMVDZAnYjqWnHq05/VNxUJsMAtXHym9REekueo0WMTgqIAbV4ODSE5MBDGVSFGf+GkcpYaW6kl+lAF7UgoLTKCibNZNeeZzSy4h3aD3XGlQD3YGgN3smQRRNrfPjdgyy69x2wRarXZCsigvCDIlW/90Ubbaa9tbqPpODEkGR8M2Wbybzs=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $expectedArray = json_decode($expectedSignatureFileData, true); - $actualArray = ''; - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent'); - $this->fileAccessHelper - ->expects($this->once()) - ->method('file_put_contents') - ->with( - \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent/core/signature.json', - $this->callback(function ($signature) use (&$actualArray) { - $actualArray = json_decode($signature, true); - return true; - }) - ); - - $keyBundle = file_get_contents(__DIR__ . '/../../data/integritycheck/core.crt'); - $rsaPrivateKey = file_get_contents(__DIR__ . '/../../data/integritycheck/core.key'); - $rsa = RSA::loadPrivateKey($rsaPrivateKey); - // After loading the key, always set the PSS padding and options: - $rsa = $rsa - ->withPadding(RSA::SIGNATURE_PSS) - ->withMGFHash('sha512') - ->withSaltLength(0); - $x509 = new X509(); - $x509->loadX509($keyBundle); - $this->checker->writeCoreSignature($x509, $rsa, \OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent'); - - // now check that the actual signature file content matches the expected content - // we cannot do in the callback because throwing an assertion error there would not be properly reported by PHPUnit - $this->assertEquals($expectedArray, $actualArray); - } - - public function testVerifyCoreSignatureWithoutSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Signature data not found.', - ], - ]; - $this->assertSame($expected, $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreSignatureWithValidSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyCoreSignature()); - } - - /** - * When updated use this to generate new signature data: - * `occ integrity:sign-core --privateKey=./tests/data/integritycheck/core.key --certificate=./tests/data/integritycheck/core.crt --path=tests/data/integritycheck/htaccessWithValidModifiedContent` - */ - public function testVerifyCoreSignatureWithValidModifiedHtaccessSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent'); - $signatureDataFile = '{ - "hashes": { - ".htaccess": "ee900c70c32f5475f301648f790f0836f330fe77af94154dfec7d290327c14b505aaea27945eb6862eaa104bbf8346c011752767af455b77b6676e382bcd5344", - "subfolder\/.htaccess": "2c57b1e25050e11dc3ae975832f378c452159f7b69f818e47eeeafadd6ba568517461dcb4d843b90b906cd7c89d161bc1b89dff8e3ae0eb6f5088508c47befd1" - }, - "signature": "LhHHUjQlsNacPWGO7dmnDYNU4pvuVuaXAG01w41A6ijwTB1ii3khtaAJcsT4HYoDWLZ9KnmAYPwmYWjEh9xvzvC7arOFsZFixSjaEjNQADqwUwacyNCyJ4an3JRw\/nZPJqAgCDOtr3pJWixf2v7qOWbJrlu7yB\/SF3wiSXRfE1s6Y8jAMERZJ5bfMjiLC1yXJ4VpIzn5ed3RnWfl0MWtPr\/XEtOLkh+eoSExhmDw91w\/gyJo4\/+iCnnLjKDsIEPRcoe2\/t4azaxdvCIwlStLsQXygXNUySeO2m2HnjZOCHy2E4MfTbrV8XuT3wPEYHWQTc3QAMehbjEsctTaI4RAoKbNtnjWrBqP+Z10cpIfiIENFMvMYu\/mSzhZH84rjoywWaZ+MyQ+LxeMiugVZKLEEAWQ7rCKjBEILnJ1ssrTOkBf7tmom52FvCSTNkieEBNzupR5GjePRXfzrSXw56Yg8veq9ALkQH7yXviEiHlRDLa0F1MfOjp4QgJzH0yMVDZAnYjqWnHq05\/VNxUJsMAtXHym9REekueo0WMTgqIAbV4ODSE5MBDGVSFGf+GkcpYaW6kl+lAF7UgoLTKCibNZNeeZzSy4h3aD3XGlQD3YGgN3smQRRNrfPjdgyy69x2wRarXZCsigvCDIlW\/90Ubbaa9tbqPpODEkGR8M2Wbybzs=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent/core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/htaccessWithValidModifiedContent/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyCoreSignature()); - } - - /** - * See inline instruction on how to update the test assets when changing mimetypealiases.dist.json - */ - public function testVerifyCoreSignatureWithModifiedMimetypelistSignatureData(): void { - $shippedMimetypeAliases = (array)json_decode(file_get_contents(\OC::$SERVERROOT . '/resources/config/mimetypealiases.dist.json')); - $shippedMimetypeNames = (array)json_decode(file_get_contents(\OC::$SERVERROOT . '/resources/config/mimetypenames.dist.json')); - $allAliases = array_merge($shippedMimetypeAliases, ['my-custom/mimetype' => 'custom']); - $allMimetypeNames = array_merge($shippedMimetypeNames, ['my-custom/mimetype' => 'Custom Document']); - - $this->mimeTypeDetector - ->method('getOnlyDefaultAliases') - ->willReturn($shippedMimetypeAliases); - - $this->mimeTypeDetector - ->method('getAllAliases') - ->willReturn($allAliases); - - $this->mimeTypeDetector - ->method('getAllNamings') - ->willReturn($allMimetypeNames); - - $oldMimetypeList = new GenerateMimetypeFileBuilder(); - $all = $this->mimeTypeDetector->getAllAliases(); - $namings = $this->mimeTypeDetector->getAllNamings(); - $newFile = $oldMimetypeList->generateFile($all, $namings); - - // When updating the mimetype list the test assets need to be updated as well - // 1. Update core/js/mimetypelist.js with the new generated js by running the test with the next line uncommented: - // file_put_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js', $newFile); - // 2. Update signature.json using the following occ command: - // occ integrity:sign-core --privateKey=./tests/data/integritycheck/core.key --certificate=./tests/data/integritycheck/core.crt --path=./tests/data/integritycheck/mimetypeListModified - self::assertEquals($newFile, file_get_contents(\OC::$SERVERROOT . '/tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js')); - - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/mimetypeListModified'); - - $signatureDataFile = file_get_contents(__DIR__ . '/../../data/integritycheck/mimetypeListModified/core/signature.json'); - $this->fileAccessHelper - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/mimetypeListModified/core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/mimetypeListModified/resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreSignatureWithValidSignatureDataAndNotAlphabeticOrder(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $this->assertSame([], $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreSignatureWithTamperedSignatureData(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "tampered", - "subfolder\/file.txt": "tampered" - }, - "signature": "eXesvDm3pkek12xSwMG10y9suRES79Nye3jYNe5KYq1tTUPqRRNgxmMGAfcUro0zpLeAr2YgHeSMWtglblGOW7pmwGVPZ0O1Y4r1fE6jnep0kW+35PLIaqCorIOnCAtSzDNKBhwd1ow3zW2wC0DFouuEkIO8u5Fw28g8E8dp8zEk1xMblNPy+xtWkmYHrVJ\/dQgun1bYOF2ZFtAzatwndTI\/bGsy1i3Wsl+x6HyWKQdq8y8VObtOqKDH7uERBEpB9DHVyKflj1v1gQuEH6BhaRdATc7ee0MiQdGblraIySwYRdfo2d8i82OVKrenMB3SLwyCvDPyQ9iKpTOnSF52ZBqaqSXKM2N\/RAkweeBFQQCwcHhqxvB0cfbyHcbkOLeCZe\/tsh68IxwTiYgzvLfl7sOZ5arnZbzrPpZmB+hfV2omkoJ1tDwOWz9hEmLLNtfo2OxyUH1m0+XFaC+Gbn4WkVDgf7YZkwUcG+Qoa3oKDNMss8MEyZxewl2iDGZcf402dlidHRprlfmXbAYuVQ08\/a0HxIKYPGh\/nsMGmwnO15CWtFpAbhUA\/D5oRjsIxnvXaMDg0iAFpdu\/5Ffsj7g3EPdBkiQHNYK7YU1RRx609eH0bZyiIYHdUPw7ikLupvrebZmELqi3mqDFO99u4eISlxFJlUbUND3L4BtmWTWrKwI=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Signature could not get verified.', - ] - ]; - $this->assertSame($expected, $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreSignatureWithTamperedFiles(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "dYoohBaWIFR\/To1FXEbMQB5apUhVYlEauBGSPo12nq84wxWkBx2EM3KDRgkB5Sub2tr0CgmAc2EVjPhKIEzAam26cyUb48bJziz1V6wvW7z4GZAfaJpzLkyHdSfV5117VSf5w1rDcAeZDXfGUaaNEJPWytaF4ZIxVge7f3NGshHy4odFVPADy\/u6c43BWvaOtJ4m3aJQbP6sxCO9dxwcm5yJJJR3n36jfh229sdWBxyl8BhwhH1e1DEv78\/aiL6ckKFPVNzx01R6yDFt3TgEMR97YZ\/R6lWiXG+dsJ305jNFlusLu518zBUvl7g5yjzGN778H29b2C8VLZKmi\/h1CH9jGdD72fCqCYdenD2uZKzb6dsUtXtvBmVcVT6BUGz41W1pkkEEB+YJpMrHILIxAiHRGv1+aZa9\/Oz8LWFd+BEUQjC2LJgojPnpzaG\/msw1nBkX16NNVDWWtJ25Bc\/r\/mG46rwjWB\/cmV6Lwt6KODiqlxgrC4lm9ALOCEWw+23OcYhLwNfQTYevXqHqsFfXOkhUnM8z5vDUb\/HBraB1DjFXN8iLK+1YewD4P495e+SRzrR79Oi3F8SEqRIzRLfN2rnW1BTms\/wYsz0p67cup1Slk1XlNmHwbWX25NVd2PPlLOvZRGoqcKFpIjC5few8THiZfyjiNFwt3RM0AFdZcXY=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUc\/0FxYrsgSs9rDxp03EJmbjN0NwwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIxMDMzM1oXDTE2MTEwMzIxMDMzM1owDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBALb6EgHpkAqZbO5vRO8XSh7G7XGWHw5s\r\niOf4RwPXR6SE9bWZEm\/b72SfWk\/\/J6AbrD8WiOzBuT\/ODy6k5T1arEdHO+Pux0W1\r\nMxYJJI4kH74KKgMpC0SB0Rt+8WrMqV1r3hhJ46df6Xr\/xolP3oD+eLbShPcblhdS\r\nVtkZEkoev8Sh6L2wDCeHDyPxzvj1w2dTdGVO9Kztn0xIlyfEBakqvBWtcxyi3Ln0\r\nklnxlMx3tPDUE4kqvpia9qNiB1AN2PV93eNr5\/2riAzIssMFSCarWCx0AKYb54+d\r\nxLpcYFyqPJ0ydBCkF78DD45RCZet6PNYkdzgbqlUWEGGomkuDoJbBg4wzgzO0D77\r\nH87KFhYW8tKFFvF1V3AHl\/sFQ9tDHaxM9Y0pZ2jPp\/ccdiqnmdkBxBDqsiRvHvVB\r\nCn6qpb4vWGFC7vHOBfYspmEL1zLlKXZv3ezMZEZw7O9ZvUP3VO\/wAtd2vUW8UFiq\r\ns2v1QnNLN6jNh51obcwmrBvWhJy9vQIdtIjQbDxqWTHh1zUSrw9wrlklCBZ\/zrM0\r\ni8nfCFwTxWRxp3H9KoECzO\/zS5R5KIS7s3\/wq\/w9T2Ie4rcecgXwDizwnn0C\/aKc\r\nbDIjujpL1s9HO05pcD\/V3wKcPZ1izymBkmMyIbL52iRVN5FTVHeZdXPpFuq+CTQJ\r\nQ238lC+A\/KOVAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAGoKTnh8RfJV4sQItVC2\r\nAvfJagkrIqZ3iiQTUBQGTKBsTnAqE1H7QgUSV9vSd+8rgvHkyZsRjmtyR1e3A6Ji\r\noNCXUbExC\/0iCPUqdHZIVb+Lc\/vWuv4ByFMybGPydgtLoEUX2ZrKFWmcgZFDUSRd\r\n9Uj26vtUhCC4bU4jgu6hIrR9IuxOBLQUxGTRZyAcXvj7obqRAEZwFAKQgFpfpqTb\r\nH+kjcbZSaAlLVSF7vBc1syyI8RGYbqpwvtREqJtl5IEIwe6huEqJ3zPnlP2th\/55\r\ncf3Fovj6JJgbb9XFxrdnsOsDOu\/tpnaRWlvv5ib4+SzG5wWFT5UUEo4Wg2STQiiX\r\nuVSRQxK1LE1yg84bs3NZk9FSQh4B8vZVuRr5FaJsZZkwlFlhRO\/\/+TJtXRbyNgsf\r\noMRZGi8DLGU2SGEAHcRH\/QZHq\/XDUWVzdxrSBYcy7GSpT7UDVzGv1rEJUrn5veP1\r\n0KmauAqtiIaYRm4f6YBsn0INcZxzIPZ0p8qFtVZBPeHhvQtvOt0iXI\/XUxEWOa2F\r\nK2EqhErgMK\/N07U1JJJay5tYZRtvkGq46oP\/5kQG8hYST0MDK6VihJoPpvCmAm4E\r\npEYKQ96x6A4EH9Y9mZlYozH\/eqmxPbTK8n89\/p7Ydun4rI+B2iiLnY8REWWy6+UQ\r\nV204fGUkJqW5CrKy3P3XvY9X\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/appWithInvalidData//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'INVALID_HASH' => [ - 'AnotherFile.txt' => [ - 'expected' => '1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112', - 'current' => '7322348ba269c6d5522efe02f424fa3a0da319a7cd9c33142a5afe32a2d9af2da3a411f086fcfc96ff4301ea566f481dba0960c2abeef3594c4d930462f6584c', - ], - ], - 'FILE_MISSING' => [ - 'subfolder/file.txt' => [ - 'expected' => '410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b', - 'current' => '', - ], - ], - 'EXTRA_FILE' => [ - 'UnecessaryFile' => [ - 'expected' => '', - 'current' => 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', - ], - ], - ]; - $this->assertSame($expected, $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreWithInvalidCertificate(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "eXesvDm3pkek12xSwMG10y9suRES79Nye3jYNe5KYq1tTUPqRRNgxmMGAfcUro0zpLeAr2YgHeSMWtglblGOW7pmwGVPZ0O1Y4r1fE6jnep0kW+35PLIaqCorIOnCAtSzDNKBhwd1ow3zW2wC0DFouuEkIO8u5Fw28g8E8dp8zEk1xMblNPy+xtWkmYHrVJ\/dQgun1bYOF2ZFtAzatwndTI\/bGsy1i3Wsl+x6HyWKQdq8y8VObtOqKDH7uERBEpB9DHVyKflj1v1gQuEH6BhaRdATc7ee0MiQdGblraIySwYRdfo2d8i82OVKrenMB3SLwyCvDPyQ9iKpTOnSF52ZBqaqSXKM2N\/RAkweeBFQQCwcHhqxvB0cfbyHcbkOLeCZe\/tsh68IxwTiYgzvLfl7sOZ5arnZbzrPpZmB+hfV2omkoJ1tDwOWz9hEmLLNtfo2OxyUH1m0+XFaC+Gbn4WkVDgf7YZkwUcG+Qoa3oKDNMss8MEyZxewl2iDGZcf402dlidHRprlfmXbAYuVQ08\/a0HxIKYPGh\/nsMGmwnO15CWtFpAbhUA\/D5oRjsIxnvXaMDg0iAFpdu\/5Ffsj7g3EPdBkiQHNYK7YU1RRx609eH0bZyiIYHdUPw7ikLupvrebZmELqi3mqDFO99u4eISlxFJlUbUND3L4BtmWTWrKwI=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEvjCCAqagAwIBAgIUPYoweUxCPqbDW4ntuh7QvgyqSrgwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIyNDIwNloXDTE2MTEwMzIyNDIwNlowDzENMAsGA1UEAwwEY29yZTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJui3nDbjOIjxNnthdBZplphujsN6u8K\r\nQ\/62zAuSwzXVp0+3IMgM\/2sepklVE8YfCyVJ5+SUJqnqHoUWVRVfs8jL0wW6nrHM\r\n\/lsscAguWCee4iAdNOqI9kq4+DUau8J45e62XA9mrAo\/8\/NKzFE2y2WduDoQZcm+\r\n8+dwcUUHXw2jl8dfrmvEMYSqTNDdb4rGmQpeV+dr9BLqr+x03U1Q08qCG9j7mSOz\r\ncvJENjOvC5uzAh5LCuCgxqG4o+mPzB0FtNnwoRRu6IsF3Y3KacRqPc30fB\/iXDn5\r\nBPr14uNxTTYWoZJ1F0tZrLzRbXdjJJOC+dnQurTtXWZ8WjPB1BWQYK7fW6t82mkN\r\n2Qe2xen99gs9nX5yY\/sHM3TKSJdM7AVCEv\/emW3gNjkvWTtRlN\/Nc7X2ckNwXcvo\r\n0yi3fSPjzXpDgLbhp1FzrMlHDn1VzmRT3r8wLByWa\/hsxrJDsBzwunMJYhXhmeKb\r\n3wX0tN\/EUJTWBntpwVOIGnRPD51oBoQUOMaEAq\/kz8PgN181bWZkJbRuf+FWkijQ\r\no+HR2lVF1jWXXst5Uc+s9HN81Uly7X4O9MMg0QxT4+wymtGDs6AOkwMi9rgBTrRB\r\n3tLU3XL2UIwRXgmd8cPtTu\/I6Bm7LdyaYtZ3yJTxRewq3nZdWypqBhD8uhpIYVkf\r\no4bxmGkVAQVTAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAKKAX5EHgU1grODnJ0of\r\nspFpgB1K67YvclNUyuU6NQ6zBJx1\/w1RnM7uxLcxiiWj1BbUhwZQ0ojmEHeUyi6O\r\nGrDVajwhTccDMmja3u5adhEncx65\/H+lD85IPRRkS2qBDssMDdJHhZ0uI+40nI7M\r\nMq1kFjl+6wiuqZXqps66DuLbk45g\/ZlrFIrIo3Ix5vj0OVqwT+gO4LYirJK6KgVS\r\nUttbcEsc\/yKU9ThnM8\/n4m2jstZXfzKPgOsJrQcZrFOtpj+CWmBzVElBSPlDT3Nh\r\nHSgOeTFJ8bQBxj2iG5dLA+JZJQKxyJ1gy2ZtxIJ2GyvLtSe8NUSqvfPWOaAKEUV2\r\ngniytnEFLr+PcD+9EGux6jZNuj6HmtWVThTfD5VGFmtlVU2z71ZRYY0kn6J3mmFc\r\nS2ecEcCUwqG5YNLncEUCyZhC2klWql2SHyGctCEyWWY7ikIDjVzYt2EbcFvLNBnP\r\ntybN1TYHRRZxlug00CCoOE9EZfk46FkZpDvU6KmqJRofkNZ5sj+SffyGcwYwNrDH\r\nKqe8m+9lHf3CRTIDeMu8r2xl1I6M6ZZfjabbmVP9Jd6WN4s6f1FlXDWzhlT1N0Qw\r\nGzJj6xB+SPtS3UV05tBlvbfA4e06D5G9uD7Q8ONcINtMS0xsSJ2oo82AqlpvlF\/q\r\noj7YKHsaTVGA+FxBktZHfoxD\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Certificate is not valid.', - ] - ]; - $this->assertSame($expected, $this->checker->verifyCoreSignature()); - } - - public function testVerifyCoreWithDifferentScope(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->environmentHelper - ->expects($this->any()) - ->method('getServerRoot') - ->willReturn(\OC::$SERVERROOT . '/tests/data/integritycheck/app/'); - $signatureDataFile = '{ - "hashes": { - "AnotherFile.txt": "1570ca9420e37629de4328f48c51da29840ddeaa03ae733da4bf1d854b8364f594aac560601270f9e1797ed4cd57c1aea87bf44cf4245295c94f2e935a2f0112", - "subfolder\/file.txt": "410738545fb623c0a5c8a71f561e48ea69e3ada0981a455e920a5ae9bf17c6831ae654df324f9328ff8453de179276ae51931cca0fa71fe8ccde6c083ca0574b" - }, - "signature": "EL49UaSeyMAqyMtqId+tgOhhwgOevPZsRLX4j2blnybAB6fN07z0936JqZV7+eMPiE30Idx+UCY6rCFN531Kqe9vAOCdgtHUSOjjKyKc+lvULESlMb6YQcrZrvDlEMMjzjH49ewG7Ai8sNN6HrRUd9U8ws+ewSkW2DOOBItj\/21RBnkrSt+2AtGXGigEvuTm57HrCYDj8\/lSkumC2GVkjLUHeLOKYo4PRNOr6yP5mED5v7zo66AWvXl2fKv54InZcdxsAk35lyK9DGZbk\/027ZRd0AOHT3LImRLvQ+8EAg3XLlRUy0hOFGgPC+jYonMzgYvsAXAXi2j8LnLJlsLwpFwu1k1B+kZVPMumKZvP9OvJb70EirecXmz62V+Jiyuaq7ne4y7Kp5gKZT\/T8SeZ0lFtCmPfYyzBB0y8s5ldmTTmdVYHs54t\/OCCW82HzQZxnFNPzDTRa8HglsaMKrqPtW59+R4UvRKSWhB8M\/Ah57qgzycvPV4KMz\/FbD4l\/\/9chRKSlCfc2k3b8ZSHNmi+EzCKgJjWIoKdgN1yax94puU8jfn8UW+G7H9Y1Jsf\/jox6QLyYEgtV1vOHY2xLT7fVs2vhyvkN2MNjJnmQ70gFG5Qz2lBz5wi6ZpB+tOfCcpbLxWAkoWoIrmC\/Ilqh7mfmRZ43g5upjkepHNd93ONuY8=", - "certificate": "-----BEGIN CERTIFICATE-----\r\nMIIEwTCCAqmgAwIBAgIUWv0iujufs5lUr0svCf\/qTQvoyKAwDQYJKoZIhvcNAQEF\r\nBQAwIzEhMB8GA1UECgwYb3duQ2xvdWQgQ29kZSBTaWduaW5nIENBMB4XDTE1MTEw\r\nMzIyNDk1M1oXDTE2MTEwMzIyNDk1M1owEjEQMA4GA1UEAwwHU29tZUFwcDCCAiIw\r\nDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK8q0x62agGSRBqeWsaeEwFfepMk\r\nF8cAobMMi50qHCv9IrOn\/ZH9l52xBrbIkErVmRjmly0d4JhD8Ymhidsh9ONKYl\/j\r\n+ishsZDM8eNNdp3Ew+fEYVvY1W7mR1qU24NWj0bzVsClI7hvPVIuw7AjfBDq1C5+\r\nA+ZSLSXYvOK2cEWjdxQfuNZwEZSjmA63DUllBIrm35IaTvfuyhU6BW9yHZxmb8+M\r\nw0xDv30D5UkE\/2N7Pa\/HQJLxCR+3zKibRK3nUyRDLSXxMkU9PnFNaPNX59VPgyj4\r\nGB1CFSToldJVPF4pzh7p36uGXZVxs8m3LFD4Ol8mhi7jkxDZjqFN46gzR0r23Py6\r\ndol9vfawGIoUwp9LvL0S7MvdRY0oazLXwClLP4OQ17zpSMAiCj7fgNT661JamPGj\r\nt5O7Zn2wA7I4ddDS\/HDTWCu98Zwc9fHIpsJPgCZ9awoqxi4Mnf7Pk9g5nnXhszGC\r\ncxxIASQKM+GhdzoRxKknax2RzUCwCzcPRtCj8AQT\/x\/mqN3PfRmlnFBNACUw9bpZ\r\nSOoNq2pCF9igftDWpSIXQ38pVpKLWowjjg3DVRmVKBgivHnUnVLyzYBahHPj0vaz\r\ntFtUFRaqXDnt+4qyUGyrT5h5pjZaTcHIcSB4PiarYwdVvgslgwnQzOUcGAzRWBD4\r\n6jV2brP5vFY3g6iPAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACTY3CCHC+Z28gCf\r\nFWGKQ3wAKs+k4+0yoti0qm2EKX7rSGQ0PHSas6uW79WstC4Rj+DYkDtIhGMSg8FS\r\nHVGZHGBCc0HwdX+BOAt3zi4p7Sf3oQef70\/4imPoKxbAVCpd\/cveVcFyDC19j1yB\r\nBapwu87oh+muoeaZxOlqQI4UxjBlR\/uRSMhOn2UGauIr3dWJgAF4pGt7TtIzt+1v\r\n0uA6FtN1Y4R5O8AaJPh1bIG0CVvFBE58esGzjEYLhOydgKFnEP94kVPgJD5ds9C3\r\npPhEpo1dRpiXaF7WGIV1X6DI\/ipWvfrF7CEy6I\/kP1InY\/vMDjQjeDnJ\/VrXIWXO\r\nyZvHXVaN\/m+1RlETsH7YO\/QmxRue9ZHN3gvvWtmpCeA95sfpepOk7UcHxHZYyQbF\r\n49\/au8j+5tsr4A83xzsT1JbcKRxkAaQ7WDJpOnE5O1+H0fB+BaLakTg6XX9d4Fo7\r\n7Gin7hVWX7pL+JIyxMzME3LhfI61+CRcqZQIrpyaafUziPQbWIPfEs7h8tCOWyvW\r\nUO8ZLervYCB3j44ivkrxPlcBklDCqqKKBzDP9dYOtS\/P4RB1NkHA9+NTvmBpTonS\r\nSFXdg9fFMD7VfjDE3Vnk+8DWkVH5wBYowTAD7w9Wuzr7DumiAULexnP\/Y7xwxLv7\r\n4B+pXTAcRK0zECDEaX3npS8xWzrB\r\n-----END CERTIFICATE-----" -}'; - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_get_contents') - ->willReturnMap([ - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//core/signature.json', $signatureDataFile], - [\OC::$SERVERROOT . '/tests/data/integritycheck/app//resources/codesigning/root.crt', file_get_contents(__DIR__ . '/../../data/integritycheck/root.crt')], - ]); - - $expected = [ - 'EXCEPTION' => [ - 'class' => 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException', - 'message' => 'Certificate is not valid for required scope. (Requested: core, current: CN=SomeApp)', - ] - ]; - $this->assertSame($expected, $this->checker->verifyCoreSignature()); - } - - public function testRunInstanceVerification(): void { - /** @var Checker&MockObject */ - $checker = $this->getMockBuilder('\OC\IntegrityCheck\Checker') - ->setConstructorArgs([ - $this->serverVersion, - $this->environmentHelper, - $this->fileAccessHelper, - $this->config, - $this->appConfig, - $this->cacheFactory, - $this->appManager, - $this->mimeTypeDetector, - ]) - ->onlyMethods([ - 'verifyCoreSignature', - 'verifyAppSignature', - ]) - ->getMock(); - - $checker - ->expects($this->once()) - ->method('verifyCoreSignature'); - $this->appManager - ->expects($this->once()) - ->method('getAllAppsInAppsFolders') - ->willReturn([ - 'files', - 'calendar', - 'contacts', - 'dav', - ]); - $this->appManager - ->expects($this->exactly(4)) - ->method('isShipped') - ->willReturnMap([ - ['files', true], - ['calendar', false], - ['contacts', false], - ['dav', true], - ]); - - $calls = [ - 'files', - 'calendar', - 'dav', - ]; - $checker - ->expects($this->exactly(3)) - ->method('verifyAppSignature') - ->willReturnCallback(function ($app) use (&$calls) { - $expected = array_shift($calls); - $this->assertSame($expected, $app); - return []; - }); - $this->appManager - ->expects($this->exactly(2)) - ->method('getAppPath') - ->willReturnMap([ - ['calendar', '/apps/calendar'], - ['contacts', '/apps/contacts'], - ]); - $this->fileAccessHelper - ->expects($this->exactly(2)) - ->method('file_exists') - ->willReturnMap([ - ['/apps/calendar/appinfo/signature.json', true], - ['/apps/contacts/appinfo/signature.json', false], - ]); - $this->appConfig - ->expects($this->once()) - ->method('deleteKey') - ->with('core', 'oc.integritycheck.checker'); - - $checker->runInstanceVerification(); - } - - public function testVerifyAppSignatureWithoutSignatureDataAndCodeCheckerDisabled(): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn('stable'); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(true); - - $expected = []; - $this->assertSame($expected, $this->checker->verifyAppSignature('SomeApp')); - } - - public static function channelDataProvider(): array { - return [ - ['stable', true], - ['git', false], - ]; - } - - /** - * @param string $channel - * @param bool $isCodeSigningEnforced - */ - #[\PHPUnit\Framework\Attributes\DataProvider('channelDataProvider')] - public function testIsCodeCheckEnforced($channel, $isCodeSigningEnforced): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn($channel); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(false); - - $this->assertSame($isCodeSigningEnforced, $this->checker->isCodeCheckEnforced()); - } - - /** - * @param string $channel - */ - #[\PHPUnit\Framework\Attributes\DataProvider('channelDataProvider')] - public function testIsCodeCheckEnforcedWithDisabledConfigSwitch($channel): void { - $this->serverVersion - ->expects($this->once()) - ->method('getChannel') - ->willReturn($channel); - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('integrity.check.disabled', false) - ->willReturn(true); - - $this->assertFalse(self::invokePrivate($this->checker, 'isCodeCheckEnforced')); - } -} --- a/tests/lib/IntegrityCheck/Helpers/EnvironmentHelperTest.php +++ /dev/null @@ -1,27 +0,0 @@ -environmentHelper = new EnvironmentHelper(); - parent::setUp(); - } - - public function testGetServerRoot(): void { - $this->assertSame(\OC::$SERVERROOT, $this->environmentHelper->getServerRoot()); - } -} --- a/tests/lib/IntegrityCheck/Helpers/FileAccessHelperTest.php +++ /dev/null @@ -1,58 +0,0 @@ -fileAccessHelper = new FileAccessHelper(); - } - - public function testReadAndWrite(): void { - $tempManager = Server::get(ITempManager::class); - $filePath = $tempManager->getTemporaryFile(); - $data = 'SomeDataGeneratedByIntegrityCheck'; - - $this->fileAccessHelper->file_put_contents($filePath, $data); - $this->assertSame($data, $this->fileAccessHelper->file_get_contents($filePath)); - } - - public function testFile_put_contentsWithException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Failed to write into /anabsolutelynotexistingfolder/on/the/system.txt'); - - $this->fileAccessHelper->file_put_contents('/anabsolutelynotexistingfolder/on/the/system.txt', 'MyFiles'); - } - - public function testIs_writable(): void { - $this->assertFalse($this->fileAccessHelper->is_writable('/anabsolutelynotexistingfolder/on/the/system.txt')); - $this->assertTrue($this->fileAccessHelper->is_writable(Server::get(ITempManager::class)->getTemporaryFile('MyFile'))); - } - - public function testAssertDirectoryExistsWithException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Directory /anabsolutelynotexistingfolder/on/the/system does not exist.'); - - $this->fileAccessHelper->assertDirectoryExists('/anabsolutelynotexistingfolder/on/the/system'); - } - - public function testAssertDirectoryExists(): void { - $this->fileAccessHelper->assertDirectoryExists(Server::get(ITempManager::class)->getTemporaryFolder('/testfolder/')); - $this->addToAssertionCount(1); - } -} --- a/tests/lib/IntegrityCheck/Iterator/ExcludeFileByNameFilterIteratorTest.php +++ /dev/null @@ -1,83 +0,0 @@ -filter = $this->getMockBuilder(ExcludeFileByNameFilterIterator::class) - ->disableOriginalConstructor() - ->onlyMethods(['current']) - ->getMock(); - } - - public static function fileNameProvider(): array { - return [ - ['a file', true], - ['Thumbs.db', false], - ['another file', true], - ['.directory', false], - ['.webapp-nextcloud-15.0.2', false], - ['.webapp-nextcloud-14.0.5-r3', false], - ['wx.webapp-nextcloud-obee', true], - ['.rnd', false], - ]; - } - - /** - * @param string $fileName - * @param bool $expectedResult - */ - #[\PHPUnit\Framework\Attributes\DataProvider('fileNameProvider')] - public function testAcceptForFiles($fileName, $expectedResult): void { - $iteratorMock = $this->getMockBuilder(\RecursiveDirectoryIterator::class) - ->disableOriginalConstructor() - ->onlyMethods(['getFilename', 'isDir']) - ->getMock(); - - $iteratorMock->method('getFilename') - ->willReturn($fileName); - $iteratorMock->method('isDir') - ->willReturn(false); - $this->filter->method('current') - ->willReturn($iteratorMock); - - $actualResult = $this->filter->accept(); - $this->assertEquals($expectedResult, $actualResult); - } - - /** - * @param string $fileName - * @param bool $expectedResult - */ - #[\PHPUnit\Framework\Attributes\DataProvider('fileNameProvider')] - public function testAcceptForDirs($fileName, $expectedResult): void { - $iteratorMock = $this->getMockBuilder(\RecursiveDirectoryIterator::class) - ->disableOriginalConstructor() - ->onlyMethods(['getFilename', 'isDir']) - ->getMock(); - - $iteratorMock->method('getFilename') - ->willReturn($fileName); - $iteratorMock->method('isDir') - ->willReturn(true); - $this->filter->method('current') - ->willReturn($iteratorMock); - - $actualResult = $this->filter->accept(); - $this->assertTrue($actualResult); - } -} --- a/tests/lib/Interaction/RestrictInteractionEventTest.php +++ /dev/null @@ -1,111 +0,0 @@ - - */ - public static function dataIsInteractionRestricted(): array { - return [ - [true], - [false], - ]; - } - - #[DataProvider('dataIsInteractionRestricted')] - public function testIsInteractionRestricted(bool $isRestricted): void { - $eventDispatcher = Server::get(IEventDispatcher::class); - - $auditEvents = []; - $auditEventListener = function (CriticalActionPerformedEvent $event) use (&$auditEvents): void { - $auditEvents[] = $event; - }; - $eventDispatcher->addListener(CriticalActionPerformedEvent::class, $auditEventListener); - - /** @psalm-suppress UnusedClosureParam */ - $restrictInteractionEventListener = function (RestrictInteractionEvent $event) use ($isRestricted): void { - if ($isRestricted) { - throw new InteractionRestrictedException('my restriction message', 'my restriction hint'); - } - }; - $eventDispatcher->addListener(RestrictInteractionEvent::class, $restrictInteractionEventListener); - - $user = $this->createMock(IUser::class); - $user - ->method('getUID') - ->willReturn('my-uid'); - - $resource1 = $this->createMock(InteractionResource::class); - $resource1 - ->method('getID') - ->willReturn('my-resource1'); - - $resource2 = $this->createMock(InteractionResource::class); - $resource2 - ->method('getID') - ->willReturn('my-resource2'); - - $action = $this->createStub(InteractionAction::class); - - $receiver1 = $this->createMock(InteractionReceiver::class); - $receiver1 - ->method('getID') - ->willReturn('my-receiver1'); - - $receiver2 = $this->createMock(InteractionReceiver::class); - $receiver2 - ->method('getID') - ->willReturn('my-receiver2'); - - $event = new RestrictInteractionEvent( - $user->getUID(), - $user, - [$resource1, $resource2], - $action, - [$receiver1, $receiver2], - ); - - if ($isRestricted) { - $this->assertEquals('my restriction hint', $event->isInteractionRestricted()); - } else { - $this->assertFalse($event->isInteractionRestricted()); - } - - $this->assertEquals([ - new CriticalActionPerformedEvent( - $isRestricted - ? 'Interaction %s from user %s on %s to %s is restricted: my restriction message' - : 'Interaction %s from user %s on %s to %s is allowed.', - [ - $action::class, - 'my-uid', - 'my-resource1, my-resource2', - 'my-receiver1, my-receiver2', - ], - ), - ], $auditEvents); - - $eventDispatcher->removeListener(CriticalActionPerformedEvent::class, $auditEventListener); - $eventDispatcher->removeListener(RestrictInteractionEvent::class, $restrictInteractionEventListener); - } -} --- a/tests/lib/L10N/FactoryTest.php +++ /dev/null @@ -1,783 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->request = $this->createMock(IRequest::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->appManager = $this->createMock(IAppManager::class); - - $this->serverRoot = \OC::$SERVERROOT; - - $this->config - ->method('getSystemValueBool') - ->willReturnMap([ - ['installed', false, true], - ]); - } - - /** - * @param string[] $methods - * @param bool $mockRequestGetHeaderMethod - * - * @return Factory|MockObject - */ - protected function getFactory(array $methods = [], $mockRequestGetHeaderMethod = false) { - if ($mockRequestGetHeaderMethod) { - $this->request->expects(self::any()) - ->method('getHeader') - ->willReturn(''); - } - - if (!empty($methods)) { - return $this->getMockBuilder(Factory::class) - ->setConstructorArgs([ - $this->config, - $this->request, - $this->userSession, - $this->cacheFactory, - $this->serverRoot, - $this->appManager, - ]) - ->onlyMethods($methods) - ->getMock(); - } - - return new Factory($this->config, $this->request, $this->userSession, $this->cacheFactory, $this->serverRoot, $this->appManager); - } - - public static function dataCleanLanguage(): array { - return [ - 'null shortcut' => [null, null], - 'default language' => ['de', 'de'], - 'regional language' => ['de_DE', 'de_DE'], - 'malicious language' => ['de/../fr', 'defr'], - 'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'], - ]; - } - - #[DataProvider('dataCleanLanguage')] - public function testCleanLanguage(?string $lang, ?string $expected): void { - $factory = $this->getFactory(); - $this->assertSame($expected, self::invokePrivate($factory, 'cleanLanguage', [$lang])); - } - - public static function dataFindAvailableLanguages(): array { - return [ - [null], - ['files'], - ]; - } - - public function testFindLanguageWithExistingRequestLanguageAndNoApp(): void { - $factory = $this->getFactory(['languageExists']); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects(self::once()) - ->method('languageExists') - ->with(null, 'de') - ->willReturn(true); - - self::assertSame('de', $factory->findLanguage()); - } - - public function testFindLanguageWithExistingRequestLanguageAndApp(): void { - $factory = $this->getFactory(['languageExists']); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects(self::once()) - ->method('languageExists') - ->with('MyApp', 'de') - ->willReturn(true); - - self::assertSame('de', $factory->findLanguage('MyApp')); - } - - public function testFindLanguageWithNotExistingRequestLanguageAndExistingStoredUserLanguage(): void { - $factory = $this->getFactory(['languageExists']); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects($this->exactly(2)) - ->method('languageExists') - ->willReturnMap([ - ['MyApp', 'de', false], - ['MyApp', 'jp', true], - ]); - $this->config - ->expects($this->exactly(1)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false], - ]); - $user = $this->createMock(IUser::class); - $user->expects(self::once()) - ->method('getUID') - ->willReturn('MyUserUid'); - $this->userSession - ->expects(self::exactly(2)) - ->method('getUser') - ->willReturn($user); - $this->config - ->expects(self::once()) - ->method('getUserValue') - ->with('MyUserUid', 'core', 'lang', null) - ->willReturn('jp'); - - self::assertSame('jp', $factory->findLanguage('MyApp')); - } - - public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguage(): void { - $factory = $this->getFactory(['languageExists'], true); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects($this->exactly(3)) - ->method('languageExists') - ->willReturnMap([ - ['MyApp', 'de', false], - ['MyApp', 'jp', false], - ['MyApp', 'es', true], - ]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false], - ['default_language', false, 'es'] - ]); - $user = $this->createMock(IUser::class); - $user->expects(self::once()) - ->method('getUID') - ->willReturn('MyUserUid'); - $this->userSession - ->expects(self::exactly(2)) - ->method('getUser') - ->willReturn($user); - $this->config - ->expects(self::once()) - ->method('getUserValue') - ->with('MyUserUid', 'core', 'lang', null) - ->willReturn('jp'); - - self::assertSame('es', $factory->findLanguage('MyApp')); - } - - public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefault(): void { - $factory = $this->getFactory(['languageExists'], true); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects($this->exactly(3)) - ->method('languageExists') - ->willReturnMap([ - ['MyApp', 'de', false], - ['MyApp', 'jp', false], - ['MyApp', 'es', false], - ]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false], - ['default_language', false, 'es'] - ]); - $user = $this->createMock(IUser::class); - $user->expects(self::once()) - ->method('getUID') - ->willReturn('MyUserUid'); - $this->userSession - ->expects(self::exactly(2)) - ->method('getUser') - ->willReturn($user); - $this->config - ->expects(self::once()) - ->method('getUserValue') - ->with('MyUserUid', 'core', 'lang', null) - ->willReturn('jp'); - $this->config - ->expects(self::never()) - ->method('setUserValue'); - - self::assertSame('en', $factory->findLanguage('MyApp')); - } - - public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefaultAndNoAppInScope(): void { - $factory = $this->getFactory(['languageExists'], true); - $this->invokePrivate($factory, 'requestLanguage', ['de']); - $factory->expects($this->exactly(3)) - ->method('languageExists') - ->willReturnMap([ - ['MyApp', 'de', false], - ['MyApp', 'jp', false], - ['MyApp', 'es', false], - ]); - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false], - ['default_language', false, 'es'] - ]); - $user = $this->createMock(IUser::class); - $user->expects(self::once()) - ->method('getUID') - ->willReturn('MyUserUid'); - $this->userSession - ->expects(self::exactly(2)) - ->method('getUser') - ->willReturn($user); - $this->config - ->expects(self::once()) - ->method('getUserValue') - ->with('MyUserUid', 'core', 'lang', null) - ->willReturn('jp'); - $this->config - ->expects(self::never()) - ->method('setUserValue') - ->with('MyUserUid', 'core', 'lang', 'en'); - - self::assertSame('en', $factory->findLanguage('MyApp')); - } - - public function testFindLanguageWithForcedLanguage(): void { - $factory = $this->getFactory(['languageExists']); - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('force_language', false) - ->willReturn('de'); - - $factory->expects($this->once()) - ->method('languageExists') - ->with('MyApp', 'de') - ->willReturn(true); - - self::assertSame('de', $factory->findLanguage('MyApp')); - } - - /** - * @param string|null $app - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataFindAvailableLanguages')] - public function testFindAvailableLanguages($app): void { - $factory = $this->getFactory(['findL10nDir']); - $factory->expects(self::once()) - ->method('findL10nDir') - ->with($app) - ->willReturn(\OC::$SERVERROOT . '/tests/data/l10n/'); - - self::assertEqualsCanonicalizing(['cs', 'de', 'en', 'ru'], $factory->findAvailableLanguages($app)); - } - - public static function dataLanguageExists(): array { - return [ - [null, 'en', [], true], - [null, 'de', [], false], - [null, 'de', ['ru'], false], - [null, 'de', ['ru', 'de'], true], - ['files', 'en', [], true], - ['files', 'de', [], false], - ['files', 'de', ['ru'], false], - ['files', 'de', ['de', 'ru'], true], - ]; - } - - public function testFindAvailableLanguagesWithThemes(): void { - $this->serverRoot .= '/tests/data'; - $app = 'files'; - - $factory = $this->getFactory(['findL10nDir']); - $factory->expects(self::once()) - ->method('findL10nDir') - ->with($app) - ->willReturn($this->serverRoot . '/apps/files/l10n/'); - $this->config - ->expects(self::once()) - ->method('getSystemValueString') - ->with('theme') - ->willReturn('abc'); - - self::assertEqualsCanonicalizing(['en', 'zz'], $factory->findAvailableLanguages($app)); - } - - /** - * - * @param string|null $app - * @param string $lang - * @param string[] $availableLanguages - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataLanguageExists')] - public function testLanguageExists($app, $lang, array $availableLanguages, $expected): void { - $factory = $this->getFactory(['findAvailableLanguages']); - $factory->expects(($lang === 'en') ? self::never() : self::once()) - ->method('findAvailableLanguages') - ->with($app) - ->willReturn($availableLanguages); - - self::assertSame($expected, $factory->languageExists($app, $lang)); - } - - public static function dataSetLanguageFromRequest(): array { - return [ - // Language is available - [null, 'de', ['de'], 'de'], - [null, 'de,en', ['de'], 'de'], - [null, 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'], - // Language is not available - [null, 'de', ['ru'], new LanguageNotFoundException()], - [null, 'de,en', ['ru', 'en'], 'en'], - [null, 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'], - - // Don't fall back from kab (Kabyle) to ka (Georgian) - Unless specifically requested - [null, 'kab;q=0.8,en;q=0.6', ['ka', 'en'], 'en'], - [null, 'kab;q=0.8,de;q=0.6', ['ka', 'en', 'de'], 'de'], - [null, 'kab;q=0.8,de;q=0.7,ka;q=0.6', ['ka', 'en', 'de'], 'de'], - [null, 'kab;q=0.8,ka;q=0.7,de;q=0.6', ['ka', 'en', 'de'], 'ka'], - - // Language for app - ['files_pdfviewer', 'de', ['de'], 'de'], - ['files_pdfviewer', 'de,en', ['de'], 'de'], - ['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'], - // Language for app is not available - ['files_pdfviewer', 'de', ['ru'], new LanguageNotFoundException()], - ['files_pdfviewer', 'de,en', ['ru', 'en'], 'en'], - ['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'], - ]; - } - - /** - * - * @param string|null $app - * @param string $header - * @param string[] $availableLanguages - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLanguageFromRequest')] - public function testGetLanguageFromRequest($app, $header, array $availableLanguages, $expected): void { - $factory = $this->getFactory(['findAvailableLanguages', 'respectDefaultLanguage']); - $factory->expects(self::once()) - ->method('findAvailableLanguages') - ->with($app) - ->willReturn($availableLanguages); - - $factory->expects(self::any()) - ->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) { - return $lang; - }); - - $this->request->expects(self::once()) - ->method('getHeader') - ->with('ACCEPT_LANGUAGE') - ->willReturn($header); - - if ($expected instanceof LanguageNotFoundException) { - $this->expectException(LanguageNotFoundException::class); - self::invokePrivate($factory, 'getLanguageFromRequest', [$app]); - } else { - self::assertSame($expected, self::invokePrivate($factory, 'getLanguageFromRequest', [$app]), 'Asserting returned language'); - } - } - - public static function dataGetL10nFilesForApp(): array { - return [ - ['', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']], - ['core', 'ru', [\OC::$SERVERROOT . '/core/l10n/ru.json']], - ['lib', 'ru', [\OC::$SERVERROOT . '/lib/l10n/ru.json']], - ['settings', 'de', [\OC::$SERVERROOT . '/apps/settings/l10n/de.json']], - ['files', 'de', [\OC::$SERVERROOT . '/apps/files/l10n/de.json']], - ['files', '_lang_never_exists_', []], - ['_app_never_exists_', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']], - ]; - } - - /** - * - * @param string $app - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetL10nFilesForApp')] - public function testGetL10nFilesForApp($app, $lang, $expected): void { - $factory = $this->getFactory(); - if (in_array($app, ['settings','files'])) { - $this->appManager - ->method('getAppPath') - ->with($app) - ->willReturn(\OC::$SERVERROOT . '/apps/' . $app); - } else { - $this->appManager - ->method('getAppPath') - ->with($app) - ->willThrowException(new AppPathNotFoundException()); - } - self::assertSame($expected, $this->invokePrivate($factory, 'getL10nFilesForApp', [$app, $lang])); - } - - public static function dataFindL10NDir(): array { - return [ - ['', \OC::$SERVERROOT . '/core/l10n/'], - ['core', \OC::$SERVERROOT . '/core/l10n/'], - ['lib', \OC::$SERVERROOT . '/lib/l10n/'], - ['settings', \OC::$SERVERROOT . '/apps/settings/l10n/'], - ['files', \OC::$SERVERROOT . '/apps/files/l10n/'], - ['_app_never_exists_', \OC::$SERVERROOT . '/core/l10n/'], - ]; - } - - /** - * - * @param string $app - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataFindL10NDir')] - public function testFindL10NDir($app, $expected): void { - $factory = $this->getFactory(); - if (in_array($app, ['settings','files'])) { - $this->appManager - ->method('getAppPath') - ->with($app) - ->willReturn(\OC::$SERVERROOT . '/apps/' . $app); - } else { - $this->appManager - ->method('getAppPath') - ->with($app) - ->willThrowException(new AppPathNotFoundException()); - } - self::assertSame($expected, $this->invokePrivate($factory, 'findL10nDir', [$app])); - } - - public static function dataFindLanguage(): array { - return [ - // Not logged in - [false, [], 'en'], - [false, ['fr'], 'fr'], - [false, ['de', 'fr'], 'de'], - [false, ['nl', 'de', 'fr'], 'de'], - - [true, [], 'en'], - [true, ['fr'], 'fr'], - [true, ['de', 'fr'], 'de'], - [true, ['nl', 'de', 'fr'], 'nl'], - ]; - } - - /** - * - * @param bool $loggedIn - * @param array $availableLang - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataFindLanguage')] - public function testFindLanguage($loggedIn, $availableLang, $expected): void { - $userLang = 'nl'; - $browserLang = 'de'; - $defaultLang = 'fr'; - - $this->config->expects(self::any()) - ->method('getSystemValue') - ->willReturnCallback(function ($var, $default) use ($defaultLang) { - if ($var === 'default_language') { - return $defaultLang; - } else { - return $default; - } - }); - - if ($loggedIn) { - $user = $this->createMock(IUser::class); - $user->expects(self::any()) - ->method('getUID') - ->willReturn('MyUserUid'); - $this->userSession - ->expects(self::any()) - ->method('getUser') - ->willReturn($user); - $this->config->expects(self::any()) - ->method('getUserValue') - ->with('MyUserUid', 'core', 'lang', null) - ->willReturn($userLang); - } else { - $this->userSession - ->expects(self::any()) - ->method('getUser') - ->willReturn(null); - } - - $this->request->expects(self::any()) - ->method('getHeader') - ->with($this->equalTo('ACCEPT_LANGUAGE')) - ->willReturn($browserLang); - - $factory = $this->getFactory(['languageExists', 'findAvailableLanguages', 'respectDefaultLanguage']); - $factory->expects(self::any()) - ->method('languageExists') - ->willReturnCallback(function ($app, $lang) use ($availableLang) { - return in_array($lang, $availableLang); - }); - $factory->expects(self::any()) - ->method('findAvailableLanguages') - ->willReturnCallback(function ($app) use ($availableLang) { - return $availableLang; - }); - $factory->expects(self::any()) - ->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) { - return $lang; - }); - - $lang = $factory->findLanguage(); - - self::assertSame($expected, $lang); - } - - public function testFindGenericLanguageByRequestParam(): void { - $factory = $this->getFactory(); - $this->request->expects(self::once()) - ->method('getParam') - ->with('forceLanguage') - ->willReturn('cz'); - - $lang = $factory->findGenericLanguage(); - - self::assertSame('cz', $lang); - } - - public function testFindGenericLanguageByEnforcedLanguage(): void { - $factory = $this->getFactory(); - $this->request->expects(self::once()) - ->method('getParam') - ->with('forceLanguage') - ->willReturn(null); - $this->config->expects(self::once()) - ->method('getSystemValue') - ->with('force_language', false) - ->willReturn('cz'); - - $lang = $factory->findGenericLanguage(); - - self::assertSame('cz', $lang); - } - - public function testFindGenericLanguageByDefaultLanguage(): void { - $factory = $this->getFactory(['languageExists']); - $this->request->expects(self::once()) - ->method('getParam') - ->with('forceLanguage') - ->willReturn(null); - $this->config->expects(self::exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false,], - ['default_language', false, 'cz',], - ]); - $factory->expects(self::once()) - ->method('languageExists') - ->with(null, 'cz') - ->willReturn(true); - - $lang = $factory->findGenericLanguage(); - - self::assertSame('cz', $lang); - } - - public function testFindGenericLanguageByDefaultLanguageNotExists(): void { - $factory = $this->getFactory(['languageExists']); - $this->request->expects(self::once()) - ->method('getParam') - ->with('forceLanguage') - ->willReturn(null); - $this->config->expects(self::exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false,], - ['default_language', false, 'cz',], - ]); - $factory->expects(self::once()) - ->method('languageExists') - ->with(null, 'cz') - ->willReturn(false); - - $lang = $factory->findGenericLanguage(); - - self::assertSame('en', $lang); - } - - public function testFindGenericLanguageFallback(): void { - $factory = $this->getFactory(); - $this->request->expects(self::once()) - ->method('getParam') - ->with('forceLanguage') - ->willReturn(null); - $this->config->expects(self::exactly(2)) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false,], - ['default_language', false, false,], - ]); - - $lang = $factory->findGenericLanguage(); - - self::assertSame('en', $lang); - } - - public static function dataTestRespectDefaultLanguage(): array { - return [ - ['de', 'de_DE', true, 'de_DE'], - ['de', 'de', true, 'de'], - ['de', false, true, 'de'], - ['fr', 'de_DE', true, 'fr'], - ]; - } - - /** - * test if we respect default language if possible - * - * - * @param string $lang - * @param string $defaultLanguage - * @param bool $langExists - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRespectDefaultLanguage')] - public function testRespectDefaultLanguage($lang, $defaultLanguage, $langExists, $expected): void { - $factory = $this->getFactory(['languageExists']); - $factory->expects(self::any()) - ->method('languageExists')->willReturn($langExists); - $this->config->expects(self::any()) - ->method('getSystemValue')->with('default_language', false)->willReturn($defaultLanguage); - - $result = $this->invokePrivate($factory, 'respectDefaultLanguage', ['app', $lang]); - self::assertSame($expected, $result); - } - - public static function dataTestReduceToLanguages(): array { - return [ - ['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'fr', 'de'], ['en', 'fr', 'de']], - ['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'de'], ['en', 'de']], - ['en', ['en', 'de', 'fr', 'it', 'es'], [], ['de', 'en', 'es', 'fr', 'it']], - ]; - } - - /** - * test - * - if available languages set can be reduced by configuration - * - if available languages set is not reduced to an empty set if - * the reduce config is an empty set - * - * - * @param string $lang - * @param array $availableLanguages - * @param array $reducedLanguageSet - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestReduceToLanguages')] - public function testReduceLanguagesByConfiguration(string $lang, array $availableLanguages, array $reducedLanguageSet, array $expected): void { - $factory = $this->getFactory(['findAvailableLanguages', 'languageExists']); - $factory->expects(self::any()) - ->method('languageExists')->willReturn(true); - $factory->expects(self::any()) - ->method('findAvailableLanguages') - ->willReturnCallback(function ($app) use ($availableLanguages) { - return $availableLanguages; - }); - - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, false], - ['default_language', false, $lang], - ['reduce_to_languages', [], $reducedLanguageSet] - ]); - - $result = $this->invokePrivate($factory, 'getLanguages'); - $commonLanguagesCodes = array_map(function ($lang) { - return $lang['code']; - }, $result['commonLanguages']); - - self::assertEqualsCanonicalizing($expected, $commonLanguagesCodes); - } - - public static function languageIteratorRequestProvider(): array { - return [ - [ true, true], - [ false, true], - [ false, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('languageIteratorRequestProvider')] - public function testGetLanguageIterator(bool $hasSession, bool $mockUser): void { - $factory = $this->getFactory(); - $user = null; - - if (!$mockUser) { - $matcher = $this->userSession->expects(self::once()) - ->method('getUser'); - - if ($hasSession) { - $matcher->willReturn($this->createMock(IUser::class)); - } else { - $this->expectException(\RuntimeException::class); - } - } else { - $user = $this->createMock(IUser::class); - } - - $iterator = $factory->getLanguageIterator($user); - self::assertInstanceOf(ILanguageIterator::class, $iterator); - } - - public static function dataGetLanguageDirection(): array { - return [ - ['en', 'ltr'], - ['de', 'ltr'], - ['fa', 'rtl'], - ['ar', 'rtl'] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetLanguageDirection')] - public function testGetLanguageDirection(string $language, string $expectedDirection) { - $factory = $this->getFactory(); - - self::assertEquals($expectedDirection, $factory->getLanguageDirection($language)); - } -} --- a/tests/lib/L10N/L10nTest.php +++ /dev/null @@ -1,248 +0,0 @@ -createMock(IConfig::class); - /** @var IRequest $request */ - $request = $this->createMock(IRequest::class); - /** @var IUserSession $userSession */ - $userSession = $this->createMock(IUserSession::class); - $cacheFactory = $this->createMock(ICacheFactory::class); - $appManager = $this->createMock(IAppManager::class); - return new Factory($config, $request, $userSession, $cacheFactory, \OC::$SERVERROOT, $appManager); - } - - public function testSimpleTranslationWithTrailingColon(): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/de.json'; - $l = new L10N($this->getFactory(), 'test', 'de', 'de_AT', [$transFile]); - - $this->assertEquals('Files:', $l->t('Files:')); - } - - public function testGermanPluralTranslations(): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/de.json'; - $l = new L10N($this->getFactory(), 'test', 'de', 'de_AT', [$transFile]); - - $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1)); - $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2)); - } - - public function testRussianPluralTranslations(): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/ru.json'; - $l = new L10N($this->getFactory(), 'test', 'ru', 'ru_UA', [$transFile]); - - $this->assertEquals('1 файл', (string)$l->n('%n file', '%n files', 1)); - $this->assertEquals('2 файла', (string)$l->n('%n file', '%n files', 2)); - $this->assertEquals('6 файлов', (string)$l->n('%n file', '%n files', 6)); - $this->assertEquals('21 файл', (string)$l->n('%n file', '%n files', 21)); - $this->assertEquals('22 файла', (string)$l->n('%n file', '%n files', 22)); - $this->assertEquals('26 файлов', (string)$l->n('%n file', '%n files', 26)); - - /* - 1 file 1 файл 1 папка - 2-4 files 2-4 файла 2-4 папки - 5-20 files 5-20 файлов 5-20 папок - 21 files 21 файл 21 папка - 22-24 files 22-24 файла 22-24 папки - 25-30 files 25-30 файлов 25-30 папок - etc - 100 files 100 файлов, 100 папок - 1000 files 1000 файлов 1000 папок - */ - } - - public function testCzechPluralTranslations(): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/cs.json'; - $l = new L10N($this->getFactory(), 'test', 'cs', 'cs_CZ', [$transFile]); - - $this->assertEquals('1 okno', (string)$l->n('%n window', '%n windows', 1)); - $this->assertEquals('2 okna', (string)$l->n('%n window', '%n windows', 2)); - $this->assertEquals('5 oken', (string)$l->n('%n window', '%n windows', 5)); - } - - public function testGermanPluralWithCzechLocaleTranslations(): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/de.json'; - $l = new L10N($this->getFactory(), 'test', 'de', 'cs_CZ', [$transFile]); - - $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1)); - $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2)); - $this->assertEquals('5 Dateien', (string)$l->n('%n file', '%n files', 5)); - } - - public static function dataPlaceholders(): array { - return [ - ['Ordered placeholders one %s two %s', 'Placeholder one 1 two 2'], - ['Reordered placeholders one %s two %s', 'Placeholder two 2 one 1'], - ['Reordered placeholders one %1$s two %2$s', 'Placeholder two 2 one 1'], - ]; - } - - /** - * - * @param $string - * @param $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataPlaceholders')] - public function testPlaceholders($string, $expected): void { - $transFile = \OC::$SERVERROOT . '/tests/data/l10n/de.json'; - $l = new L10N($this->getFactory(), 'test', 'de', 'de_AT', [$transFile]); - - $this->assertEquals($expected, $l->t($string, ['1', '2'])); - } - - public static function localizationData(): array { - return [ - // timestamp as string - ["February 13, 2009, 11:31:30\xE2\x80\xAFPM UTC", 'en', 'en_US', 'datetime', '1234567890'], - ['13. Februar 2009, 23:31:30 UTC', 'de', 'de_DE', 'datetime', '1234567890'], - ['February 13, 2009', 'en', 'en_US', 'date', '1234567890'], - ['13. Februar 2009', 'de', 'de_DE', 'date', '1234567890'], - ["11:31:30\xE2\x80\xAFPM UTC", 'en', 'en_US', 'time', '1234567890'], - ['23:31:30 UTC', 'de', 'de_DE', 'time', '1234567890'], - - // timestamp as int - ["February 13, 2009, 11:31:30\xE2\x80\xAFPM UTC", 'en', 'en_US', 'datetime', 1234567890], - ['13. Februar 2009, 23:31:30 UTC', 'de', 'de_DE', 'datetime', 1234567890], - ['February 13, 2009', 'en', 'en_US', 'date', 1234567890], - ['13. Februar 2009', 'de', 'de_DE', 'date', 1234567890], - ["11:31:30\xE2\x80\xAFPM UTC", 'en', 'en_US', 'time', 1234567890], - ['23:31:30 UTC', 'de', 'de_DE', 'time', 1234567890], - - // DateTime object - ["February 13, 2009, 11:31:30\xE2\x80\xAFPM GMT+0", 'en', 'en_US', 'datetime', new DateTime('@1234567890')], - ['13. Februar 2009, 23:31:30 GMT+0', 'de', 'de_DE', 'datetime', new DateTime('@1234567890')], - ['February 13, 2009', 'en', 'en_US', 'date', new DateTime('@1234567890')], - ['13. Februar 2009', 'de', 'de_DE', 'date', new DateTime('@1234567890')], - ["11:31:30\xE2\x80\xAFPM GMT+0", 'en', 'en_US', 'time', new DateTime('@1234567890')], - ['23:31:30 GMT+0', 'de', 'de_DE', 'time', new DateTime('@1234567890')], - - // DateTimeImmutable object - ["February 13, 2009, 11:31:30\xE2\x80\xAFPM GMT+0", 'en', 'en_US', 'datetime', new DateTimeImmutable('@1234567890')], - ['13. Februar 2009, 23:31:30 GMT+0', 'de', 'de_DE', 'datetime', new DateTimeImmutable('@1234567890')], - ['February 13, 2009', 'en', 'en_US', 'date', new DateTimeImmutable('@1234567890')], - ['13. Februar 2009', 'de', 'de_DE', 'date', new DateTimeImmutable('@1234567890')], - ["11:31:30\xE2\x80\xAFPM GMT+0", 'en', 'en_US', 'time', new DateTimeImmutable('@1234567890')], - ['23:31:30 GMT+0', 'de', 'de_DE', 'time', new DateTimeImmutable('@1234567890')], - - // en_GB - ['13 February 2009, 23:31:30 GMT+0', 'en_GB', 'en_GB', 'datetime', new DateTime('@1234567890')], - ['13 February 2009', 'en_GB', 'en_GB', 'date', new DateTime('@1234567890')], - ['23:31:30 GMT+0', 'en_GB', 'en_GB', 'time', new DateTime('@1234567890')], - ['13 February 2009, 23:31:30 GMT+0', 'en-GB', 'en_GB', 'datetime', new DateTime('@1234567890')], - ['13 February 2009', 'en-GB', 'en_GB', 'date', new DateTime('@1234567890')], - ['23:31:30 GMT+0', 'en-GB', 'en_GB', 'time', new DateTime('@1234567890')], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localizationData')] - public function testNumericStringLocalization($expectedDate, $lang, $locale, $type, $value): void { - $l = new L10N($this->getFactory(), 'test', $lang, $locale, []); - $this->assertSame($expectedDate, $l->l($type, $value)); - } - - public static function firstDayData(): array { - return [ - [1, 'de', 'de_DE'], - [0, 'en', 'en_US'], - ]; - } - - /** - * @param $expected - * @param $lang - * @param $locale - */ - #[\PHPUnit\Framework\Attributes\DataProvider('firstDayData')] - public function testFirstWeekDay($expected, $lang, $locale): void { - $l = new L10N($this->getFactory(), 'test', $lang, $locale, []); - $this->assertSame($expected, $l->l('firstday', 'firstday')); - } - - public static function jsDateData(): array { - return [ - ['dd.MM.yy', 'de', 'de_DE'], - ['M/d/yy', 'en', 'en_US'], - ]; - } - - /** - * @param $expected - * @param $lang - * @param $locale - */ - #[\PHPUnit\Framework\Attributes\DataProvider('jsDateData')] - public function testJSDate($expected, $lang, $locale): void { - $l = new L10N($this->getFactory(), 'test', $lang, $locale, []); - $this->assertSame($expected, $l->l('jsdate', 'jsdate')); - } - - public function testFactoryGetLanguageCode(): void { - $l = $this->getFactory()->get('lib', 'de'); - $this->assertEquals('de', $l->getLanguageCode()); - } - - public function testServiceGetLanguageCode(): void { - $l = Util::getL10N('lib', 'de'); - $this->assertEquals('de', $l->getLanguageCode()); - } - - public function testWeekdayName(): void { - $l = Util::getL10N('lib', 'de'); - $this->assertEquals('Mo.', $l->l('weekdayName', new \DateTime('2017-11-6'), ['width' => 'abbreviated'])); - } - - /** - * @param $locale - * @param $language - */ - #[\PHPUnit\Framework\Attributes\DataProvider('findLanguageFromLocaleData')] - public function testFindLanguageFromLocale($locale, $language): void { - $this->assertEquals( - $language, - Server::get(IFactory::class)->findLanguageFromLocale('lib', $locale) - ); - } - - public static function findLanguageFromLocaleData(): array { - return [ - 'en_US' => ['en_US', 'en'], - 'en_UK' => ['en_UK', 'en'], - 'de_DE' => ['de_DE', 'de_DE'], - 'de_AT' => ['de_AT', 'de'], - 'es_EC' => ['es_EC', 'es_EC'], - 'fi_FI' => ['fi_FI', 'fi'], - 'zh_CN' => ['zh_CN', 'zh_CN'], - ]; - } -} --- a/tests/lib/L10N/LanguageIteratorTest.php +++ /dev/null @@ -1,84 +0,0 @@ -user = $this->createMock(IUser::class); - $this->config = $this->createMock(IConfig::class); - - $this->iterator = new LanguageIterator($this->user, $this->config); - } - - public static function languageSettingsProvider(): array { - return [ - // all language settings set - [ 'de_DE', 'es_CU', 'zh_TW', ['de_DE', 'de', 'es_CU', 'es', 'zh_TW', 'zh', 'en']], - [ 'de', 'es', 'zh', ['de', 'es', 'zh', 'en']], - [ 'en', 'en', 'en', ['en', 'en', 'en', 'en']], - // one possible setting is missing each - [ false, 'es_CU', 'zh_TW', ['es_CU', 'es', 'zh_TW', 'zh', 'en']], - [ false, 'es', 'zh_TW', ['es', 'zh_TW', 'zh', 'en']], - [ false, 'es_CU', 'zh', ['es_CU', 'es', 'zh', 'en']], - [ 'de_DE', null, 'zh_TW', ['de_DE', 'de', 'zh_TW', 'zh', 'en']], - [ 'de_DE', null, 'zh', ['de_DE', 'de', 'zh', 'en']], - [ 'de', null, 'zh_TW', ['de', 'zh_TW', 'zh', 'en']], - [ 'de_DE', 'es_CU', 'en', ['de_DE', 'de', 'es_CU', 'es', 'en', 'en']], - [ 'de', 'es_CU', 'en', ['de', 'es_CU', 'es', 'en', 'en']], - [ 'de_DE', 'es', 'en', ['de_DE', 'de', 'es', 'en', 'en']], - // two possible settings are missing each - [ false, null, 'zh_TW', ['zh_TW', 'zh', 'en']], - [ false, null, 'zh', ['zh', 'en']], - [ false, 'es_CU', 'en', ['es_CU', 'es', 'en', 'en']], - [ false, 'es', 'en', ['es', 'en', 'en']], - [ 'de_DE', null, 'en', ['de_DE', 'de', 'en', 'en']], - [ 'de', null, 'en', ['de', 'en', 'en']], - // nothing is set - [ false, null, 'en', ['en', 'en']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('languageSettingsProvider')] - public function testIterator($forcedLang, $userLang, $sysLang, $expectedValues): void { - $this->config->expects($this->any()) - ->method('getSystemValue') - ->willReturnMap([ - ['force_language', false, $forcedLang], - ]); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->willReturnMap([ - ['default_language', 'en', $sysLang], - ]); - $this->config->expects($this->any()) - ->method('getUserValue') - ->willReturn($userLang); - - foreach ($expectedValues as $expected) { - $this->assertTrue($this->iterator->valid()); - $this->assertSame($expected, $this->iterator->current()); - $this->iterator->next(); - } - $this->assertFalse($this->iterator->valid()); - } -} --- a/tests/lib/LargeFileHelperGetFileSizeTest.php +++ /dev/null @@ -1,84 +0,0 @@ -helper = new LargeFileHelper(); - } - - public static function dataFileNameProvider(): array { - $path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR; - - return [ - [ $path . 'lorem.txt', 446 ], - [ $path . 'strängé filename (duplicate #2).txt', 446 ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataFileNameProvider')] - public function XtestGetFileSizeViaCurl($filename, $fileSize) { - if (!extension_loaded('curl')) { - $this->markTestSkipped( - 'The PHP curl extension is required for this test.' - ); - } - if (Server::get(IniGetWrapper::class)->getString('open_basedir') !== '') { - $this->markTestSkipped( - 'The PHP curl extension does not work with the file:// protocol when open_basedir is enabled.' - ); - } - $this->assertSame( - $fileSize, - $this->helper->getFileSizeViaCurl($filename) - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataFileNameProvider')] - public function testGetFileSizeViaExec($filename, $fileSize): void { - if (escapeshellarg('strängé') !== '\'strängé\'') { - $this->markTestSkipped('Your escapeshell args removes accents'); - } - if (!Util::isFunctionEnabled('exec')) { - $this->markTestSkipped( - 'The exec() function needs to be enabled for this test.' - ); - } - $this->assertSame( - $fileSize, - $this->helper->getFileSizeViaExec($filename) - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataFileNameProvider')] - public function testGetFileSizeNative($filename, $fileSize): void { - $this->assertSame( - $fileSize, - $this->helper->getFileSizeNative($filename) - ); - } -} --- a/tests/lib/LargeFileHelperTest.php +++ /dev/null @@ -1,48 +0,0 @@ -helper = new LargeFileHelper; - } - - public function testFormatUnsignedIntegerFloat(): void { - $this->assertSame( - '9007199254740992', - $this->helper->formatUnsignedInteger((float)9007199254740992) - ); - } - - public function testFormatUnsignedIntegerInt(): void { - $this->assertSame( - PHP_INT_SIZE === 4 ? '4294967295' : '18446744073709551615', - $this->helper->formatUnsignedInteger(-1) - ); - } - - public function testFormatUnsignedIntegerString(): void { - $this->assertSame( - '9007199254740993', - $this->helper->formatUnsignedInteger('9007199254740993') - ); - } - - public function testFormatUnsignedIntegerStringException(): void { - $this->expectException(\UnexpectedValueException::class); - - $this->helper->formatUnsignedInteger('900ABCD254740993'); - } -} --- a/tests/lib/LegacyHookTest.php +++ /dev/null @@ -1,52 +0,0 @@ -assertTrue(\OC_Hook::emit('LegacyHookTest', 'error')); - - $this->assertCount(1, \OC_Hook::$thrownExceptions); - $this->assertInstanceOf(\TypeError::class, \OC_Hook::$thrownExceptions[0]); - } - - public function testEmitRethrowsHintException(): void { - \OC_Hook::connect('LegacyHookTest', 'hint', self::class, 'throwHintException'); - - $this->expectException(HintException::class); - \OC_Hook::emit('LegacyHookTest', 'hint'); - } -} --- a/tests/lib/Lock/DBLockingProviderTest.php +++ /dev/null @@ -1,127 +0,0 @@ -currentTime = time(); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturnCallback(function () { - return $this->currentTime; - }); - parent::setUp(); - } - - /** - * @return ILockingProvider - */ - #[\Override] - protected function getInstance() { - $this->connection = Server::get(IDBConnection::class); - return new DBLockingProvider($this->connection, $this->timeFactory, 3600); - } - - #[\Override] - protected function tearDown(): void { - $qb = $this->connection->getQueryBuilder(); - $qb->delete('file_locks')->executeStatement(); - parent::tearDown(); - } - - public function testCleanEmptyLocks(): void { - $this->currentTime = 100; - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE); - - $this->currentTime = 200; - $this->instance->acquireLock('bar', ILockingProvider::LOCK_EXCLUSIVE); - $this->instance->changeLock('asd', ILockingProvider::LOCK_SHARED); - - $this->currentTime = 150 + 3600; - - $this->assertEquals(3, $this->getLockEntryCount()); - - $this->instance->cleanExpiredLocks(); - - $this->assertEquals(2, $this->getLockEntryCount()); - } - - private function getLockEntryCount(): int { - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select($qb->func()->count('*')) - ->from('file_locks') - ->executeQuery(); - return (int)$result->fetchOne(); - } - - protected function getLockValue($key) { - $query = $this->connection->getQueryBuilder(); - $query->select('lock') - ->from('file_locks') - ->where($query->expr()->eq('key', $query->createNamedParameter($key))); - - $result = $query->executeQuery(); - $rows = $result->fetchOne(); - $result->closeCursor(); - - return $rows; - } - - public function testDoubleShared(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(1, $this->getLockValue('foo')); - - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(1, $this->getLockValue('foo')); - - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(1, $this->getLockValue('foo')); - - $this->instance->releaseAll(); - - $this->assertEquals(0, $this->getLockValue('foo')); - } -} --- a/tests/lib/Lock/LockingProvider.php +++ /dev/null @@ -1,243 +0,0 @@ -instance = $this->getInstance(); - } - - public function testExclusiveLock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - } - - public function testSharedLock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - } - - public function testDoubleSharedLock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - } - - public function testReleaseSharedLock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - } - - public function testDoubleExclusiveLock(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testReleaseExclusiveLock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testExclusiveLockAfterShared(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testExclusiveLockAfterSharedReleased(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public function testReleaseAll(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE); - $this->instance->acquireLock('fizz#A=23', ILockingProvider::LOCK_EXCLUSIVE); - - $this->instance->releaseAll(); - - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertFalse($this->instance->isLocked('fizz#A=23', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public function testReleaseAllAfterChange(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE); - - $this->instance->changeLock('bar', ILockingProvider::LOCK_EXCLUSIVE); - - $this->instance->releaseAll(); - - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public function testReleaseAllAfterUnlock(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE); - - $this->instance->releaseLock('bar', ILockingProvider::LOCK_SHARED); - - $this->instance->releaseAll(); - - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public function testReleaseAfterReleaseAll(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - - $this->instance->releaseAll(); - - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - } - - public function testSharedLockAfterExclusive(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - } - - public function testLockedExceptionHasPathForShared(): void { - try { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - - $this->fail('Expected locked exception'); - } catch (LockedException $e) { - $this->assertEquals('foo', $e->getPath()); - } - } - - public function testLockedExceptionHasPathForExclusive(): void { - try { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - - $this->fail('Expected locked exception'); - } catch (LockedException $e) { - $this->assertEquals('foo', $e->getPath()); - } - } - - public function testChangeLockToExclusive(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - } - - public function testChangeLockToShared(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); - $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); - } - - public function testChangeLockToExclusiveDoubleShared(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testChangeLockToExclusiveNoShared(): void { - $this->expectException(LockedException::class); - - $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testChangeLockToExclusiveFromExclusive(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } - - public function testChangeLockToSharedNoExclusive(): void { - $this->expectException(LockedException::class); - - $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); - } - - public function testChangeLockToSharedFromShared(): void { - $this->expectException(LockedException::class); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); - } - - public function testReleaseNonExistingShared(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - // releasing a lock once to many should not result in a locked state - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); - $this->instance->releaseLock('foo', ILockingProvider::LOCK_EXCLUSIVE); - } -} --- a/tests/lib/Lock/MemcacheLockingProviderTest.php +++ /dev/null @@ -1,39 +0,0 @@ -memcache = new ArrayCache(); - $timeProvider = Server::get(ITimeFactory::class); - return new MemcacheLockingProvider($this->memcache, $timeProvider); - } - - #[\Override] - protected function tearDown(): void { - $this->memcache->clear(); - parent::tearDown(); - } -} --- a/tests/lib/Lock/NonCachingDBLockingProviderTest.php +++ /dev/null @@ -1,44 +0,0 @@ -connection = Server::get(IDBConnection::class); - return new DBLockingProvider($this->connection, $this->timeFactory, 3600, false); - } - - #[\Override] - public function testDoubleShared(): void { - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(2, $this->getLockValue('foo')); - - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(1, $this->getLockValue('foo')); - - $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); - - $this->assertEquals(0, $this->getLockValue('foo')); - } -} --- a/tests/lib/Lockdown/Filesystem/NoFSTest.php +++ /dev/null @@ -1,50 +0,0 @@ -setScope([ - IToken::SCOPE_FILESYSTEM => true - ]); - Server::get(ILockdownManager::class)->setToken($token); - parent::tearDown(); - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $token = new PublicKeyToken(); - $token->setScope([ - IToken::SCOPE_FILESYSTEM => false - ]); - - Server::get(ILockdownManager::class)->setToken($token); - $this->createUser('foo', 'var'); - } - - public function testSetupFS(): void { - \OC_Util::tearDownFS(); - \OC_Util::setupFS('foo'); - - $this->assertInstanceOf(NullStorage::class, Filesystem::getStorage('/foo/files')); - } -} --- a/tests/lib/Lockdown/Filesystem/NullCacheTest.php +++ /dev/null @@ -1,137 +0,0 @@ -cache = new NullCache(); - } - - public function testGetNumericStorageId(): void { - $this->assertSame(-1, $this->cache->getNumericStorageId()); - } - - public function testGetEmpty(): void { - $this->assertFalse($this->cache->get('foo')); - } - - public function testGet(): void { - $data = $this->cache->get(''); - - $this->assertEquals(-1, $data['fileid']); - $this->assertEquals(-1, $data['parent']); - $this->assertEquals('', $data['name']); - $this->assertEquals('', $data['path']); - $this->assertEquals('0', $data['size']); - $this->assertEquals('', $data['etag']); - $this->assertEquals(FileInfo::MIMETYPE_FOLDER, $data['mimetype']); - $this->assertEquals('httpd', $data['mimepart']); - $this->assertEquals(Constants::PERMISSION_READ, $data['permissions']); - } - - public function testGetFolderContents(): void { - $this->assertSame([], $this->cache->getFolderContents('foo')); - } - - public function testGetFolderContentsById(): void { - $this->assertSame([], $this->cache->getFolderContentsById(42)); - } - - public function testPut(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->put('foo', ['size' => 100]); - } - - public function testInsert(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->insert('foo', ['size' => 100]); - } - - public function testUpdate(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->update('foo', ['size' => 100]); - } - - public function testGetId(): void { - $this->assertSame(-1, $this->cache->getId('foo')); - } - - public function testGetParentId(): void { - $this->assertSame(-1, $this->cache->getParentId('foo')); - } - - public function testInCache(): void { - $this->assertTrue($this->cache->inCache('')); - $this->assertFalse($this->cache->inCache('foo')); - } - - public function testRemove(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->remove('foo'); - } - - public function testMove(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->move('foo', 'bar'); - } - - public function testMoveFromCache(): void { - $sourceCache = $this->createMock(ICache::class); - - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->cache->moveFromCache($sourceCache, 'foo', 'bar'); - } - - public function testGetStatus(): void { - $this->assertSame(ICache::COMPLETE, $this->cache->getStatus('foo')); - } - - public function testSearch(): void { - $this->assertSame([], $this->cache->search('foo')); - } - - public function testSearchByMime(): void { - $this->assertSame([], $this->cache->searchByMime('foo')); - } - - public function testGetIncomplete(): void { - $this->assertSame([], $this->cache->getIncomplete()); - } - - public function testGetPathById(): void { - $this->assertSame('', $this->cache->getPathById(42)); - } - - public function testNormalize(): void { - $this->assertSame('foo/ bar /', $this->cache->normalize('foo/ bar /')); - } -} --- a/tests/lib/Lockdown/Filesystem/NullStorageTest.php +++ /dev/null @@ -1,230 +0,0 @@ -storage = new NullStorage([]); - } - - public function testGetId(): void { - $this->assertSame('null', $this->storage->getId()); - } - - public function testMkdir(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->mkdir('foo'); - } - - public function testRmdir(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->rmdir('foo'); - } - - public function testOpendir(): void { - $this->assertInstanceOf(IteratorDirectory::class, $this->storage->opendir('foo')); - } - - public function testIs_dir(): void { - $this->assertTrue($this->storage->is_dir('')); - $this->assertFalse($this->storage->is_dir('foo')); - } - - public function testIs_file(): void { - $this->assertFalse($this->storage->is_file('foo')); - } - - public function testStat(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->stat('foo'); - } - - public function testFiletype(): void { - $this->assertSame('dir', $this->storage->filetype('')); - $this->assertFalse($this->storage->filetype('foo')); - } - - public function testFilesize(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->filesize('foo'); - } - - public function testIsCreatable(): void { - $this->assertFalse($this->storage->isCreatable('foo')); - } - - public function testIsReadable(): void { - $this->assertTrue($this->storage->isReadable('')); - $this->assertFalse($this->storage->isReadable('foo')); - } - - public function testIsUpdatable(): void { - $this->assertFalse($this->storage->isUpdatable('foo')); - } - - public function testIsDeletable(): void { - $this->assertFalse($this->storage->isDeletable('foo')); - } - - public function testIsSharable(): void { - $this->assertFalse($this->storage->isSharable('foo')); - } - - public function testGetPermissions(): void { - $this->assertEquals(0, $this->storage->getPermissions('foo')); - } - - public function testFile_exists(): void { - $this->assertTrue($this->storage->file_exists('')); - $this->assertFalse($this->storage->file_exists('foo')); - } - - public function testFilemtime(): void { - $this->assertFalse($this->storage->filemtime('foo')); - } - - public function testFile_get_contents(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->file_get_contents('foo'); - } - - public function testFile_put_contents(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->file_put_contents('foo', 'bar'); - } - - public function testUnlink(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->unlink('foo'); - } - - public function testRename(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->rename('foo', 'bar'); - } - - public function testCopy(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->copy('foo', 'bar'); - } - - public function testFopen(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->fopen('foo', 'R'); - } - - public function testGetMimeType(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->getMimeType('foo'); - } - - public function testHash(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->hash('md5', 'foo', true); - } - - public function testFree_space(): void { - $this->assertSame(FileInfo::SPACE_UNKNOWN, $this->storage->free_space('foo')); - } - - public function testTouch(): void { - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->touch('foo'); - } - - public function testGetLocalFile(): void { - $this->assertFalse($this->storage->getLocalFile('foo')); - } - - public function testHasUpdated(): void { - $this->assertFalse($this->storage->hasUpdated('foo', 42)); - } - - public function testGetETag(): void { - $this->assertSame('', $this->storage->getETag('foo')); - } - - public function testIsLocal(): void { - $this->assertFalse($this->storage->isLocal()); - } - - public function testGetDirectDownload(): void { - $this->assertFalse($this->storage->getDirectDownload('foo')); - } - - public function testCopyFromStorage(): void { - $sourceStorage = $this->createMock(IStorage::class); - - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->copyFromStorage($sourceStorage, 'foo', 'bar'); - } - - public function testMoveFromStorage(): void { - $sourceStorage = $this->createMock(IStorage::class); - - $this->expectException(ForbiddenException::class); - $this->expectExceptionMessage('This request is not allowed to access the filesystem'); - - $this->storage->moveFromStorage($sourceStorage, 'foo', 'bar'); - } - - public function testTest() { - $this->assertTrue($this->storage->test()); - return true; - } - - public function testGetOwner(): void { - $this->assertFalse($this->storage->getOwner('foo')); - } - - public function testGetCache(): void { - $this->assertInstanceOf(NullCache::class, $this->storage->getCache('foo')); - } -} --- a/tests/lib/Lockdown/LockdownManagerTest.php +++ /dev/null @@ -1,48 +0,0 @@ -sessionCallback = function () { - return $this->createMock(ISession::class); - }; - } - - public function testCanAccessFilesystemDisabled(): void { - $manager = new LockdownManager($this->sessionCallback); - $this->assertTrue($manager->canAccessFilesystem()); - } - - public function testCanAccessFilesystemAllowed(): void { - $token = new PublicKeyToken(); - $token->setScope([IToken::SCOPE_FILESYSTEM => true]); - $manager = new LockdownManager($this->sessionCallback); - $manager->setToken($token); - $this->assertTrue($manager->canAccessFilesystem()); - } - - public function testCanAccessFilesystemNotAllowed(): void { - $token = new PublicKeyToken(); - $token->setScope([IToken::SCOPE_FILESYSTEM => false]); - $manager = new LockdownManager($this->sessionCallback); - $manager->setToken($token); - $this->assertFalse($manager->canAccessFilesystem()); - } -} --- a/tests/lib/Log/ExceptionSerializerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -createMock(SystemConfig::class); - $this->serializer = new ExceptionSerializer($config); - } - - private function emit($arguments) { - \call_user_func_array([$this, 'bind'], $arguments); - } - - private function bind(array &$myValues): void { - throw new \Exception('my exception'); - } - - private function customMagicAuthThing(string $login, string $parole): void { - throw new \Exception('expected custom auth exception'); - } - - /** - * this test ensures that the serializer does not overwrite referenced - * variables. It is crafted after a scenario we experienced: the DAV server - * emitting the "validateTokens" event, of which later on a handled - * exception was passed to the logger. The token was replaced, the original - * variable overwritten. - */ - public function testSerializer(): void { - try { - $secret = ['Secret']; - $this->emit([&$secret]); - } catch (\Exception $e) { - $serializedData = $this->serializer->serializeException($e); - $this->assertSame(['Secret'], $secret); - $this->assertSame(ExceptionSerializer::SENSITIVE_VALUE_PLACEHOLDER, $serializedData['Trace'][0]['args'][0]); - } - } - - public function testSerializerWithRegisteredMethods(): void { - $this->serializer->enlistSensitiveMethods(self::class, ['customMagicAuthThing']); - try { - $this->customMagicAuthThing('u57474', 'Secret'); - } catch (\Exception $e) { - $serializedData = $this->serializer->serializeException($e); - $this->assertSame('customMagicAuthThing', $serializedData['Trace'][0]['function']); - $this->assertSame(ExceptionSerializer::SENSITIVE_VALUE_PLACEHOLDER, $serializedData['Trace'][0]['args'][0]); - $this->assertFalse(isset($serializedData['Trace'][0]['args'][1])); - } - } -} --- a/tests/lib/Log/FileTest.php +++ /dev/null @@ -1,95 +0,0 @@ -restore_logfile = $config->getValue('logfile'); - $this->restore_logdateformat = $config->getValue('logdateformat'); - - $config->setValue('logfile', $config->getValue('datadirectory') . '/logtest.log'); - $this->logFile = new File($config->getValue('datadirectory') . '/logtest.log', '', $config); - } - #[\Override] - protected function tearDown(): void { - $config = Server::get(SystemConfig::class); - if (isset($this->restore_logfile)) { - $config->getValue('logfile', $this->restore_logfile); - } else { - $config->deleteValue('logfile'); - } - if (isset($this->restore_logdateformat)) { - $config->getValue('logdateformat', $this->restore_logdateformat); - } else { - $config->deleteValue('logdateformat'); - } - $this->logFile = new File($this->restore_logfile, '', $config); - parent::tearDown(); - } - - public function testLogging(): void { - $config = Server::get(IConfig::class); - # delete old logfile - unlink($config->getSystemValue('logfile')); - - # set format & write log line - $config->setSystemValue('logdateformat', 'u'); - $this->logFile->write('code', ['something' => 'extra', 'message' => 'Testing logging'], ILogger::ERROR); - - # read log line - $handle = @fopen($config->getSystemValue('logfile'), 'r'); - $line = fread($handle, 1000); - fclose($handle); - - # check log has data content - $values = (array)json_decode($line, true); - $this->assertArrayNotHasKey('message', $values['data']); - $this->assertEquals('extra', $values['data']['something']); - $this->assertEquals('Testing logging', $values['message']); - } - - public function testMicrosecondsLogTimestamp(): void { - $config = Server::get(IConfig::class); - # delete old logfile - unlink($config->getSystemValue('logfile')); - - # set format & write log line - $config->setSystemValue('logdateformat', 'u'); - $this->logFile->write('test', 'message', ILogger::ERROR); - - # read log line - $handle = @fopen($config->getSystemValue('logfile'), 'r'); - $line = fread($handle, 1000); - fclose($handle); - - # check timestamp has microseconds part - $values = (array)json_decode($line); - $microseconds = $values['time']; - $this->assertNotEquals(0, $microseconds); - } -} --- a/tests/lib/Log/LogFactoryTest.php +++ /dev/null @@ -1,132 +0,0 @@ -c = $this->createMock(ContainerInterface::class); - $this->systemConfig = $this->createMock(SystemConfig::class); - - $this->factory = new LogFactory($this->c, $this->systemConfig, \OC::$SERVERROOT); - } - - public static function fileTypeProvider(): array { - return [ - [ - 'file' - ], - [ - 'nextcloud' - ], - [ - 'owncloud' - ], - [ - 'krzxkyr_default' - ] - ]; - } - - /** - * @param string $type - */ - #[\PHPUnit\Framework\Attributes\DataProvider('fileTypeProvider')] - public function testFile(string $type): void { - $datadir = \OC::$SERVERROOT . '/data'; - $defaultLog = $datadir . '/nextcloud.log'; - - $this->systemConfig->expects($this->exactly(3)) - ->method('getValue') - ->willReturnMap([ - ['datadirectory', $datadir, $datadir], - ['logfile', $defaultLog, $defaultLog], - ['logfilemode', 0640, 0640], - ]); - - $log = $this->factory->get($type); - $this->assertInstanceOf(File::class, $log); - } - - public static function logFilePathProvider():array { - return [ - [ - '/dev/null', - '/dev/null' - ], - [ - '/xdev/youshallfallback', - \OC::$SERVERROOT . '/data/nextcloud.log' - ] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('logFilePathProvider')] - public function testFileCustomPath($path, $expected): void { - $datadir = \OC::$SERVERROOT . '/data'; - $defaultLog = $datadir . '/nextcloud.log'; - - $this->systemConfig->expects($this->exactly(3)) - ->method('getValue') - ->willReturnMap([ - ['datadirectory', $datadir, $datadir], - ['logfile', $defaultLog, $path], - ['logfilemode', 0640, 0640], - ]); - - $log = $this->factory->get('file'); - $this->assertInstanceOf(File::class, $log); - $this->assertSame($expected, $log->getLogFilePath()); - } - - public function testErrorLog(): void { - $log = $this->factory->get('errorlog'); - $this->assertInstanceOf(Errorlog::class, $log); - } - - public function testSystemLog(): void { - $this->c->expects($this->once()) - ->method('get') - ->with(Syslog::class) - ->willReturn($this->createMock(Syslog::class)); - - $log = $this->factory->get('syslog'); - $this->assertInstanceOf(Syslog::class, $log); - } - - public function testSystemdLog(): void { - $this->c->expects($this->once()) - ->method('get') - ->with(Systemdlog::class) - ->willReturn($this->createMock(Systemdlog::class)); - - $log = $this->factory->get('systemd'); - $this->assertInstanceOf(Systemdlog::class, $log); - } -} --- a/tests/lib/Log/PsrLoggerAdapterTest.php +++ /dev/null @@ -1,83 +0,0 @@ -logger = $this->createMock(Log::class); - $this->loggerAdapter = new PsrLoggerAdapter($this->logger); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPsrLoggingLevels')] - public function testLoggingWithPsrLogLevels(string $level, int $expectedLevel): void { - $this->logger->expects(self::once()) - ->method('log') - ->with($expectedLevel, 'test message', ['app' => 'test']); - $this->loggerAdapter->log($level, 'test message', ['app' => 'test']); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataPsrLoggingLevels')] - public function testLogLevelToInt(string $level, int $expectedLevel): void { - $this->assertEquals($expectedLevel, PsrLoggerAdapter::logLevelToInt($level)); - } - - public static function dataPsrLoggingLevels(): array { - return [ - [LogLevel::ALERT, ILogger::ERROR], - [LogLevel::CRITICAL, ILogger::ERROR], - [LogLevel::DEBUG, ILogger::DEBUG], - [LogLevel::EMERGENCY, ILogger::FATAL], - [LogLevel::ERROR, ILogger::ERROR], - [LogLevel::INFO, ILogger::INFO], - [LogLevel::NOTICE, ILogger::INFO], - [LogLevel::WARNING, ILogger::WARN], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataInvalidLoggingLevel')] - public function testInvalidLoggingLevel($level): void { - $this->logger->expects(self::never()) - ->method('log'); - $this->expectException(InvalidArgumentException::class); - - $this->loggerAdapter->log($level, 'valid message'); - } - - public static function dataInvalidLoggingLevel(): array { - return [ - // invalid string - ['this is not a level'], - // int out of range - [ILogger::DEBUG - 1], - [ILogger::FATAL + 1], - // float is not allowed - [1.2345], - // boolean is not a level - [true], - [false], - // - [null], - ]; - } -} --- a/tests/lib/LoggerTest.php +++ /dev/null @@ -1,339 +0,0 @@ -logs = []; - $this->config = $this->createMock(SystemConfig::class); - $this->registry = $this->createMock(IRegistry::class); - $this->logger = new Log($this, $this->config, crashReporters: $this->registry); - } - - private function mockDefaultLogLevel(): void { - $this->config->expects($this->any()) - ->method('getValue') - ->willReturnMap([ - ['loglevel', ILogger::WARN, ILogger::WARN], - ]); - } - - public function testInterpolation(): void { - $this->mockDefaultLogLevel(); - $logger = $this->logger; - $logger->warning('{Message {nothing} {user} {foo.bar} a}', ['user' => 'Bob', 'foo.bar' => 'Bar']); - - $expected = ['2 {Message {nothing} Bob Bar a}']; - $this->assertEquals($expected, $this->getLogs()); - } - - public function testAppCondition(): void { - $this->config->expects($this->any()) - ->method('getValue') - ->willReturnMap([ - ['loglevel', ILogger::WARN, ILogger::WARN], - ['log.condition', [], ['apps' => ['files']]] - ]); - $logger = $this->logger; - - $logger->info('Don\'t display info messages'); - $logger->info('Show info messages of files app', ['app' => 'files']); - $logger->warning('Show warning messages of other apps'); - - $expected = [ - '1 Show info messages of files app', - '2 Show warning messages of other apps', - ]; - $this->assertEquals($expected, $this->getLogs()); - } - - public static function dataMatchesCondition(): array { - return [ - [ - 'user0', - [ - 'apps' => ['app2'], - ], - [ - '1 Info of app2', - ], - ], - [ - 'user2', - [ - 'users' => ['user1', 'user2'], - 'apps' => ['app1'], - ], - [ - '1 Info of app1', - ], - ], - [ - 'user3', - [ - 'users' => ['user3'], - ], - [ - '1 Info without app', - '1 Info of app1', - '1 Info of app2', - '0 Debug of app3', - ], - ], - [ - 'user4', - [ - 'users' => ['user4'], - 'apps' => ['app3'], - 'loglevel' => 0, - ], - [ - '0 Debug of app3', - ], - ], - [ - 'user4', - [ - 'message' => ' of ', - ], - [ - '1 Info of app1', - '1 Info of app2', - '0 Debug of app3', - ], - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataMatchesCondition')] - public function testMatchesCondition(string $userId, array $conditions, array $expectedLogs): void { - $this->config->expects($this->any()) - ->method('getValue') - ->willReturnMap([ - ['loglevel', ILogger::WARN, ILogger::WARN], - ['log.condition', [], ['matches' => [ - $conditions, - ]]], - ]); - $logger = $this->logger; - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn($userId); - $userSession = $this->createMock(IUserSession::class); - $userSession->method('getUser') - ->willReturn($user); - $this->overwriteService(IUserSession::class, $userSession); - - $logger->info('Info without app'); - $logger->info('Info of app1', ['app' => 'app1']); - $logger->info('Info of app2', ['app' => 'app2']); - $logger->debug('Debug of app3', ['app' => 'app3']); - - $this->assertEquals($expectedLogs, $this->getLogs()); - } - - public function testMatchesConditionIncreaseLoglevel(): void { - $this->config->expects($this->any()) - ->method('getValue') - ->willReturnMap([ - ['loglevel', ILogger::WARN, ILogger::INFO], - ['log.condition', [], ['matches' => [ - [ - 'message' => 'catched', - 'loglevel' => 3, - ] - ]]], - ]); - $logger = $this->logger; - - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('test-userid'); - $userSession = $this->createMock(IUserSession::class); - $userSession->method('getUser') - ->willReturn($user); - $this->overwriteService(IUserSession::class, $userSession); - - $logger->info('catched message'); - $logger->info('info level message'); - - $this->assertEquals(['1 info level message'], $this->getLogs()); - } - - public function testLoggingWithDataArray(): void { - $this->mockDefaultLogLevel(); - /** @var IWriter&MockObject */ - $writerMock = $this->createMock(IWriter::class); - $logFile = new Log($writerMock, $this->config); - $writerMock->expects($this->once())->method('write')->with('no app in context', ['something' => 'extra', 'message' => 'Testing logging with john']); - $logFile->error('Testing logging with {user}', ['something' => 'extra', 'user' => 'john']); - } - - private function getLogs(): array { - return $this->logs; - } - - #[\Override] - public function write(string $app, $message, int $level) { - $textMessage = $message; - if (is_array($message)) { - $textMessage = $message['message']; - } - $this->logs[] = $level . ' ' . $textMessage; - } - - public static function userAndPasswordData(): array { - return [ - ['mySpecialUsername', 'MySuperSecretPassword'], - ['my-user', '324324()#ä234'], - ['my-user', ')qwer'], - ['my-user', 'qwer)asdf'], - ['my-user', 'qwer)'], - ['my-user', '(qwer'], - ['my-user', 'qwer(asdf'], - ['my-user', 'qwer('], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('userAndPasswordData')] - public function testDetectlogin(string $user, string $password): void { - $this->mockDefaultLogLevel(); - $e = new \Exception('test'); - $this->registry->expects($this->once()) - ->method('delegateReport') - ->with($e, ['level' => 3]); - - $this->logger->logException($e); - - $logLines = $this->getLogs(); - foreach ($logLines as $logLine) { - if (is_array($logLine)) { - $logLine = json_encode($logLine); - } - $this->assertStringNotContainsString($user, $logLine); - $this->assertStringNotContainsString($password, $logLine); - $this->assertStringContainsString('*** sensitive parameters replaced ***', $logLine); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('userAndPasswordData')] - public function testDetectcheckPassword(string $user, string $password): void { - $this->mockDefaultLogLevel(); - $e = new \Exception('test'); - $this->registry->expects($this->once()) - ->method('delegateReport') - ->with($e, ['level' => 3]); - - $this->logger->logException($e); - - $logLines = $this->getLogs(); - foreach ($logLines as $logLine) { - if (is_array($logLine)) { - $logLine = json_encode($logLine); - } - $this->assertStringNotContainsString($user, $logLine); - $this->assertStringNotContainsString($password, $logLine); - $this->assertStringContainsString('*** sensitive parameters replaced ***', $logLine); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('userAndPasswordData')] - public function testDetectvalidateUserPass(string $user, string $password): void { - $this->mockDefaultLogLevel(); - $e = new \Exception('test'); - $this->registry->expects($this->once()) - ->method('delegateReport') - ->with($e, ['level' => 3]); - - $this->logger->logException($e); - - $logLines = $this->getLogs(); - foreach ($logLines as $logLine) { - if (is_array($logLine)) { - $logLine = json_encode($logLine); - } - $this->assertStringNotContainsString($user, $logLine); - $this->assertStringNotContainsString($password, $logLine); - $this->assertStringContainsString('*** sensitive parameters replaced ***', $logLine); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('userAndPasswordData')] - public function testDetecttryLogin(string $user, string $password): void { - $this->mockDefaultLogLevel(); - $e = new \Exception('test'); - $this->registry->expects($this->once()) - ->method('delegateReport') - ->with($e, ['level' => 3]); - - $this->logger->logException($e); - - $logLines = $this->getLogs(); - foreach ($logLines as $logLine) { - if (is_array($logLine)) { - $logLine = json_encode($logLine); - } - $this->assertStringNotContainsString($user, $logLine); - $this->assertStringNotContainsString($password, $logLine); - $this->assertStringContainsString('*** sensitive parameters replaced ***', $logLine); - } - } - - #[\PHPUnit\Framework\Attributes\DataProvider('userAndPasswordData')] - public function testDetectclosure(string $user, string $password): void { - $this->mockDefaultLogLevel(); - $a = function ($user, $password): void { - throw new \Exception('test'); - }; - $this->registry->expects($this->once()) - ->method('delegateReport'); - - try { - $a($user, $password); - } catch (\Exception $e) { - $this->logger->logException($e); - } - - $logLines = $this->getLogs(); - foreach ($logLines as $logLine) { - if (is_array($logLine)) { - $logLine = json_encode($logLine); - } - $log = explode('\n', $logLine); - unset($log[1]); // Remove `testDetectclosure(` because we are not testing this here, but the closure on stack trace 0 - $logLine = implode('\n', $log); - - $this->assertStringNotContainsString($user, $logLine); - $this->assertStringNotContainsString($password, $logLine); - $this->assertStringContainsString('*** sensitive parameters replaced ***', $logLine); - } - } -} --- a/tests/lib/Mail/EMailTemplateTest.php +++ /dev/null @@ -1,236 +0,0 @@ -defaults = $this->createMock(Defaults::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->l10n = $this->createMock(IFactory::class); - - $this->l10n->method('get') - ->with('lib', '') - ->willReturn($this->createMock(IL10N::class)); - - $this->emailTemplate = new EMailTemplate( - $this->defaults, - $this->urlGenerator, - $this->l10n, - 252, - 120, - 'test.TestTemplate', - [] - ); - } - - public function testEMailTemplateCustomFooter(): void { - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultColorPrimary') - ->willReturn('#0082c9'); - $this->defaults - ->expects($this->any()) - ->method('getLogo') - ->willReturn('/img/logo-mail-header.png'); - $this->defaults - ->expects($this->any()) - ->method('getName') - ->willReturn('TestCloud'); - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultTextColorPrimary') - ->willReturn('#ffffff'); - $this->urlGenerator - ->expects($this->once()) - ->method('getAbsoluteURL') - ->with('/img/logo-mail-header.png') - ->willReturn('https://example.org/img/logo-mail-header.png'); - - $this->emailTemplate->addHeader(); - $this->emailTemplate->addHeading('Welcome aboard'); - $this->emailTemplate->addBodyText('Welcome to your Nextcloud account, you can add, protect, and share your data.'); - $this->emailTemplate->addBodyText('Your username is: abc'); - $this->emailTemplate->addBodyButtonGroup( - 'Set your password', 'https://example.org/resetPassword/123', - 'Install Client', 'https://nextcloud.com/install/#install-clients' - ); - $this->emailTemplate->addFooter( - 'TestCloud - A safe home for your data
This is an automatically sent email, please do not reply.' - ); - - $expectedHTML = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email.html'); - $this->assertSame($expectedHTML, $this->emailTemplate->renderHtml()); - $expectedTXT = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email.txt'); - $this->assertSame($expectedTXT, $this->emailTemplate->renderText()); - } - - public function testEMailTemplateDefaultFooter(): void { - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultColorPrimary') - ->willReturn('#0082c9'); - $this->defaults - ->expects($this->any()) - ->method('getName') - ->willReturn('TestCloud'); - $this->defaults - ->expects($this->any()) - ->method('getSlogan') - ->willReturn('A safe home for your data'); - $this->defaults - ->expects($this->any()) - ->method('getLogo') - ->willReturn('/img/logo-mail-header.png'); - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultTextColorPrimary') - ->willReturn('#ffffff'); - $this->urlGenerator - ->expects($this->once()) - ->method('getAbsoluteURL') - ->with('/img/logo-mail-header.png') - ->willReturn('https://example.org/img/logo-mail-header.png'); - - $this->emailTemplate->addHeader(); - $this->emailTemplate->addHeading('Welcome aboard'); - $this->emailTemplate->addBodyText('Welcome to your Nextcloud account, you can add, protect, and share your data.'); - $this->emailTemplate->addBodyText('Your username is: abc'); - $this->emailTemplate->addBodyButtonGroup( - 'Set your password', 'https://example.org/resetPassword/123', - 'Install Client', 'https://nextcloud.com/install/#install-clients' - ); - $this->emailTemplate->addFooter(); - - $expectedHTML = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-custom.html'); - $this->assertSame($expectedHTML, $this->emailTemplate->renderHtml()); - $expectedTXT = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-custom.txt'); - $this->assertSame($expectedTXT, $this->emailTemplate->renderText()); - } - - public function testEMailTemplateSingleButton(): void { - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultColorPrimary') - ->willReturn('#0082c9'); - $this->defaults - ->expects($this->any()) - ->method('getName') - ->willReturn('TestCloud'); - $this->defaults - ->expects($this->any()) - ->method('getSlogan') - ->willReturn('A safe home for your data'); - $this->defaults - ->expects($this->any()) - ->method('getLogo') - ->willReturn('/img/logo-mail-header.png'); - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultTextColorPrimary') - ->willReturn('#ffffff'); - $this->urlGenerator - ->expects($this->once()) - ->method('getAbsoluteURL') - ->with('/img/logo-mail-header.png') - ->willReturn('https://example.org/img/logo-mail-header.png'); - - $this->emailTemplate->addHeader(); - $this->emailTemplate->addHeading('Welcome aboard'); - $this->emailTemplate->addBodyText('Welcome to your Nextcloud account, you can add, protect, and share your data.'); - $this->emailTemplate->addBodyText('Your username is: abc'); - $this->emailTemplate->addBodyButton( - 'Set your password', 'https://example.org/resetPassword/123', - false - ); - $this->emailTemplate->addFooter(); - - $expectedHTML = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-single-button.html'); - $this->assertSame($expectedHTML, $this->emailTemplate->renderHtml()); - $expectedTXT = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-single-button.txt'); - $this->assertSame($expectedTXT, $this->emailTemplate->renderText()); - } - - public function testEMailTemplateAlternativePlainTexts(): void { - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultColorPrimary') - ->willReturn('#0082c9'); - $this->defaults - ->expects($this->any()) - ->method('getName') - ->willReturn('TestCloud'); - $this->defaults - ->expects($this->any()) - ->method('getSlogan') - ->willReturn('A safe home for your data'); - $this->defaults - ->expects($this->any()) - ->method('getLogo') - ->willReturn('/img/logo-mail-header.png'); - $this->defaults - ->expects($this->atLeastOnce()) - ->method('getDefaultTextColorPrimary') - ->willReturn('#ffffff'); - $this->urlGenerator - ->expects($this->once()) - ->method('getAbsoluteURL') - ->with('/img/logo-mail-header.png') - ->willReturn('https://example.org/img/logo-mail-header.png'); - - $this->emailTemplate->addHeader(); - $this->emailTemplate->addHeading('Welcome aboard', 'Welcome aboard - text'); - $this->emailTemplate->addBodyText('Welcome to your Nextcloud account, you can add, protect, and share your data.', 'Welcome to your Nextcloud account, you can add, protect, and share your data. - text'); - $this->emailTemplate->addBodyText('Your username is: abc'); - $this->emailTemplate->addBodyButtonGroup( - 'Set your password', 'https://example.org/resetPassword/123', - 'Install Client', 'https://nextcloud.com/install/#install-clients', - 'Set your password - text', 'Install Client - text' - ); - $this->emailTemplate->addFooter(); - - $expectedHTML = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-custom.html'); - $this->assertSame($expectedHTML, $this->emailTemplate->renderHtml()); - $expectedTXT = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/new-account-email-custom-text-alternative.txt'); - $this->assertSame($expectedTXT, $this->emailTemplate->renderText()); - } - - public function testEMailTemplateEmbedsLogo(): void { - $this->defaults->method('getDefaultColorPrimary')->willReturn('#0082c9'); - $this->defaults->method('getName')->willReturn('TestCloud'); - $this->defaults->method('getLogoImage') - ->willReturn(['content' => 'PNGDATA', 'mimeType' => 'image/png']); - $this->urlGenerator->expects($this->never())->method('getAbsoluteURL'); - - $this->emailTemplate->addHeader(); - - $this->assertStringContainsString('src="cid:logo"', $this->emailTemplate->renderHtml()); - $this->assertSame( - [['name' => 'logo', 'content' => 'PNGDATA', 'mimeType' => 'image/png']], - $this->emailTemplate->getInlineImages() - ); - } -} --- a/tests/lib/Mail/EmailValidatorTest.php +++ /dev/null @@ -1,53 +0,0 @@ -appConfig = $this->createMock(IAppConfig::class); - $this->emailValidator = new EmailValidator($this->appConfig); - } - - public static function mailAddressProvider(): array { - return [ - ['lukas@nextcloud.com', true, false], - ['lukas@localhost', true, false], - ['lukas@192.168.1.1', true, false], - ['lukas@éxämplè.com', true, false], - ['asdf', false, false], - ['', false, false], - ['lukas@nextcloud.org@nextcloud.com', false, false], - ['test@localhost', true, false], - ['test@localhost', false, true], - ]; - } - - #[DataProvider('mailAddressProvider')] - public function testIsValid($email, $expected, $strict): void { - $this->appConfig - ->expects($this->atMost(1)) - ->method('getValueString') - ->with('core', 'enforce_strict_email_check', 'yes') - ->willReturn($strict ? 'yes' : 'no'); - $this->assertSame($expected, $this->emailValidator->isValid($email)); - } -} --- a/tests/lib/Mail/MailerTest.php +++ /dev/null @@ -1,341 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->defaults = $this->createMock(Defaults::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->l10n = $this->createMock(IL10N::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->mailer = new Mailer( - $this->config, - $this->logger, - $this->defaults, - $this->urlGenerator, - $this->l10n, - $this->dispatcher, - $this->createMock(IFactory::class), - $this->createMock(IEmailValidator::class), - ); - } - - /** - * @return array - */ - public static function sendmailModeProvider(): array { - return [ - 'smtp' => ['smtp', ' -bs'], - 'pipe' => ['pipe', ' -t -i'], - ]; - } - - /** - * @param $sendmailMode - * @param $binaryParam - */ - #[\PHPUnit\Framework\Attributes\DataProvider('sendmailModeProvider')] - public function testGetSendmailInstanceSendMail($sendmailMode, $binaryParam): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'sendmail'], - ['mail_sendmailmode', 'smtp', $sendmailMode], - ]); - - $path = Server::get(IBinaryFinder::class)->findBinaryPath('sendmail'); - if ($path === false) { - $path = '/usr/sbin/sendmail'; - } - - $expected = new SendmailTransport($path . $binaryParam, null, $this->logger); - $this->assertEquals($expected, self::invokePrivate($this->mailer, 'getSendMailInstance')); - } - - /** - * @param $sendmailMode - * @param $binaryParam - */ - #[\PHPUnit\Framework\Attributes\DataProvider('sendmailModeProvider')] - public function testGetSendmailInstanceSendMailQmail($sendmailMode, $binaryParam): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'qmail'], - ['mail_sendmailmode', 'smtp', $sendmailMode], - ]); - - $sendmail = new SendmailTransport('/var/qmail/bin/sendmail' . $binaryParam, null, $this->logger); - $this->assertEquals($sendmail, self::invokePrivate($this->mailer, 'getSendMailInstance')); - } - - public function testEventForNullTransport(): void { - $this->config - ->expects($this->exactly(1)) - ->method('getSystemValueString') - ->with('mail_smtpmode', 'smtp') - ->willReturn('null'); - - $message = $this->createMock(Message::class); - $message->expects($this->once()) - ->method('getSymfonyEmail') - ->willReturn((new Email())->to('foo@bar.com')->from('bar@foo.com')->text('')); - - $event = new BeforeMessageSent($message); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with($this->equalTo($event)); - - $this->mailer->send($message); - } - - public function testGetInstanceDefault(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - $this->assertInstanceOf(SymfonyMailer::class, $mailer); - $transport = self::invokePrivate($mailer, 'transport'); - $this->assertInstanceOf(EsmtpTransport::class, $transport); - } - - public function testGetInstanceSendmail(): void { - $this->config - ->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'sendmail'], - ['mail_sendmailmode', 'smtp', 'smtp'], - ]); - - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - $this->assertInstanceOf(SymfonyMailer::class, $mailer); - $transport = self::invokePrivate($mailer, 'transport'); - $this->assertInstanceOf(SendmailTransport::class, $transport); - } - - public function testEvents(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ['mail_smtpport', 25, 25], - ]); - $this->mailer = $this->getMockBuilder(Mailer::class) - ->onlyMethods(['getInstance']) - ->setConstructorArgs( - [ - $this->config, - $this->logger, - $this->defaults, - $this->urlGenerator, - $this->l10n, - $this->dispatcher, - $this->createMock(IFactory::class), - $this->createMock(IEmailValidator::class), - ] - ) - ->getMock(); - - $message = $this->createMock(Message::class); - - $event = new BeforeMessageSent($message); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with($this->equalTo($event)); - - $this->mailer->send($message); - } - - public function testCreateMessage(): void { - $this->config - ->expects($this->any()) - ->method('getSystemValueBool') - ->with('mail_send_plaintext_only', false) - ->willReturn(false); - $this->assertInstanceOf('\OC\Mail\Message', $this->mailer->createMessage()); - } - - public function testSendInvalidMailException(): void { - $this->config - ->method('getSystemValue') - ->willReturnMap([ - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - $this->expectException(\Exception::class); - - /** @var Message&MockObject */ - $message = $this->getMockBuilder('\OC\Mail\Message') - ->disableOriginalConstructor()->getMock(); - $message->expects($this->once()) - ->method('getSymfonyEmail') - ->willReturn(new Email()); - - $this->mailer->send($message); - } - - public function testCreateEMailTemplate(): void { - $this->config->method('getSystemValueString') - ->with('mail_template_class', '') - ->willReturnArgument(1); - $this->config->method('getAppValue') - ->with('theming', 'logoDimensions', Mailer::DEFAULT_DIMENSIONS) - ->willReturn(Mailer::DEFAULT_DIMENSIONS); - - $this->assertSame(EMailTemplate::class, get_class($this->mailer->createEMailTemplate('tests.MailerTest'))); - } - - public function testStreamingOptions(): void { - $this->config->method('getSystemValue') - ->willReturnMap([ - ['mail_smtpstreamoptions', [], ['foo' => 1]], - ]); - $this->config->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'smtp'], - ['overwrite.cli.url', '', ''], - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ]); - $this->config->method('getSystemValueInt') - ->willReturnMap([ - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - /** @var EsmtpTransport $transport */ - $transport = self::invokePrivate($mailer, 'transport'); - $this->assertInstanceOf(EsmtpTransport::class, $transport); - $this->assertEquals(1, count($transport->getStream()->getStreamOptions())); - $this->assertTrue(isset($transport->getStream()->getStreamOptions()['foo'])); - } - - public function testStreamingOptionsWrongType(): void { - $this->config->method('getSystemValue') - ->willReturnMap([ - ['mail_smtpstreamoptions', [], 'bar'], - ]); - $this->config->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'smtp'], - ['overwrite.cli.url', '', ''], - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ]); - $this->config->method('getSystemValueInt') - ->willReturnMap([ - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - /** @var EsmtpTransport $transport */ - $transport = self::invokePrivate($mailer, 'transport'); - $this->assertInstanceOf(EsmtpTransport::class, $transport); - $this->assertEquals(0, count($transport->getStream()->getStreamOptions())); - } - - public function testLocalDomain(): void { - $this->config->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'smtp'], - ['overwrite.cli.url', '', 'https://some.valid.url.com:8080'], - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ]); - $this->config->method('getSystemValueInt') - ->willReturnMap([ - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - - /** @var SymfonyMailer $mailer */ - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - self::assertInstanceOf(SymfonyMailer::class, $mailer); - - /** @var EsmtpTransport $transport */ - $transport = self::invokePrivate($mailer, 'transport'); - self::assertInstanceOf(EsmtpTransport::class, $transport); - self::assertEquals('some.valid.url.com', $transport->getLocalDomain()); - } - - public function testLocalDomainInvalidUrl(): void { - $this->config->method('getSystemValueString') - ->willReturnMap([ - ['mail_smtpmode', 'smtp', 'smtp'], - ['overwrite.cli.url', '', 'https:only.slash.does.not.work:8080'], - ['mail_smtphost', '127.0.0.1', '127.0.0.1'], - ]); - $this->config->method('getSystemValueInt') - ->willReturnMap([ - ['mail_smtpport', 25, 25], - ['mail_smtptimeout', 10, 10], - ]); - - /** @var SymfonyMailer $mailer */ - $mailer = self::invokePrivate($this->mailer, 'getInstance'); - self::assertInstanceOf(SymfonyMailer::class, $mailer); - - /** @var EsmtpTransport $transport */ - $transport = self::invokePrivate($mailer, 'transport'); - self::assertInstanceOf(EsmtpTransport::class, $transport); - self::assertEquals('[127.0.0.1]', $transport->getLocalDomain()); - } - - public function testCaching(): void { - $symfonyMailer1 = self::invokePrivate($this->mailer, 'getInstance'); - $symfonyMailer2 = self::invokePrivate($this->mailer, 'getInstance'); - self::assertSame($symfonyMailer1, $symfonyMailer2); - } -} --- a/tests/lib/Mail/MessageTest.php +++ /dev/null @@ -1,295 +0,0 @@ - 'Lukas Reschke'], - [new Address('lukas@owncloud.com', 'Lukas Reschke')] - ], - [ - [ - 'lukas@owncloud.com' => 'Lukas Reschke', - 'lukas@öwnclöüd.com', - 'lukäs@owncloud.örg' => 'Lükäs Réschke' - ], - [ - new Address('lukas@owncloud.com', 'Lukas Reschke'), - new Address('lukas@öwnclöüd.com'), - new Address('lukäs@owncloud.örg', 'Lükäs Réschke') - ] - ], - [ - ['lukas@öwnclöüd.com'], - [new Address('lukas@öwnclöüd.com')] - ], - ]; - } - - /** - * @return array - */ - public function getMailAddressProvider() { - return [ - [[], []], - [['lukas@owncloud.com' => 'Lukas Reschke'], ['lukas@owncloud.com' => 'Lukas Reschke']], - ]; - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->symfonyEmail = $this->createMock(Email::class); - - $this->message = new Message($this->symfonyEmail, false); - } - - /** - * - * @param string $unconverted - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('mailAddressProvider')] - public function testConvertAddresses($unconverted, $expected): void { - $this->assertEquals($expected, self::invokePrivate($this->message, 'convertAddresses', [$unconverted])); - } - - public function testSetRecipients(): void { - $this->message = $this->message->setFrom(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']); - $this->message = $this->message->setTo(['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"]); - $this->message = $this->message->setReplyTo(['penny@stardewvalley-library.co.edu' => 'Penny']); - $this->message = $this->message->setCc(['gunther@stardewvalley-library.co.edu' => 'Gunther']); - $this->message = $this->message->setBcc(['pam@stardewvalley-bus.com' => 'Pam']); - - $this->symfonyEmail - ->expects($this->once()) - ->method('from') - ->with(new Address('pierres-general-store@stardewvalley.com', 'Pierres General Store')); - $this->symfonyEmail - ->expects($this->once()) - ->method('to') - ->with(new Address('lewis-tent@stardewvalley.com', "Lewis' Tent Life")); - $this->symfonyEmail - ->expects($this->once()) - ->method('replyTo') - ->with(new Address('penny@stardewvalley-library.co.edu', 'Penny')); - $this->symfonyEmail - ->expects($this->once()) - ->method('cc') - ->with(new Address('gunther@stardewvalley-library.co.edu', 'Gunther')); - $this->symfonyEmail - ->expects($this->once()) - ->method('bcc') - ->with(new Address('pam@stardewvalley-bus.com', 'Pam')); - - $this->message->setRecipients(); - } - - public function testSetTo(): void { - $expected = ['pierres-general-store@stardewvalley.com' => 'Pierres General Store']; - - $message = $this->message->setTo(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']); - - $this->assertEquals($expected, $message->getTo()); - } - public function testSetRecipientsException(): void { - $message = $this->message->setTo(['lewis-tent@~~~~.com' => "Lewis' Tent Life"]); - - $this->symfonyEmail - ->expects($this->once()) - ->method('to') - ->willThrowException(new RfcComplianceException()); - - $this->expectException(RfcComplianceException::class); - $message->setRecipients(); - } - - public function testSetRecipientsEmptyValues(): void { - $message = $this->message->setTo([]); - - $this->symfonyEmail - ->expects($this->once()) - ->method('to'); - - $message->setRecipients(); - } - - public function testSetGetFrom(): void { - $expected = ['pierres-general-store@stardewvalley.com' => 'Pierres General Store']; - - $message = $this->message->setFrom(['pierres-general-store@stardewvalley.com' => 'Pierres General Store']); - - $this->assertEquals($expected, $message->getFrom()); - } - - public function testSetGetTo(): void { - $expected = ['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"]; - - $message = $this->message->setTo(['lewis-tent@stardewvalley.com' => "Lewis' Tent Life"]); - - $this->assertEquals($expected, $message->getTo()); - } - - public function testSetGetReplyTo(): void { - $expected = ['penny@stardewvalley-library.co.edu' => 'Penny']; - - $message = $this->message->setReplyTo(['penny@stardewvalley-library.co.edu' => 'Penny']); - - $this->assertEquals($expected, $message->getReplyTo()); - } - - public function testSetGetCC(): void { - $expected = ['gunther@stardewvalley-library.co.edu' => 'Gunther']; - - $message = $this->message->setCc(['gunther@stardewvalley-library.co.edu' => 'Gunther']); - - $this->assertEquals($expected, $message->getCc()); - } - - public function testSetGetBCC(): void { - $expected = ['pam@stardewvalley-bus.com' => 'Pam']; - - $message = $this->message->setBcc(['pam@stardewvalley-bus.com' => 'Pam']); - - $this->assertEquals($expected, $message->getBcc()); - } - - public function testSetPlainBody(): void { - $this->symfonyEmail - ->expects($this->once()) - ->method('text') - ->with('Fancy Body'); - - $this->message->setPlainBody('Fancy Body'); - } - - public function testGetPlainBody(): void { - $this->symfonyEmail - ->expects($this->once()) - ->method('getTextBody') - ->willReturn('Fancy Body'); - - $this->assertSame('Fancy Body', $this->message->getPlainBody()); - } - - public function testSetHtmlBody(): void { - $this->symfonyEmail - ->expects($this->once()) - ->method('html') - ->with('Fancy Body', 'utf-8'); - - $this->message->setHtmlBody('Fancy Body'); - } - - public function testPlainTextRenderOption(): void { - /** @var MockObject|Email $symfonyEmail */ - $symfonyEmail = $this->getMockBuilder(Email::class) - ->disableOriginalConstructor()->getMock(); - /** @var MockObject|IEMailTemplate $template */ - $template = $this->getMockBuilder(IEMailTemplate::class) - ->disableOriginalConstructor()->getMock(); - - $message = new Message($symfonyEmail, true); - - $template - ->expects($this->never()) - ->method('renderHTML'); - $template - ->expects($this->once()) - ->method('renderText'); - $template - ->expects($this->once()) - ->method('renderSubject'); - - $message->useTemplate($template); - } - - public function testBothRenderingOptions(): void { - /** @var MockObject|Email $symfonyEmail */ - $symfonyEmail = $this->getMockBuilder(Email::class) - ->disableOriginalConstructor()->getMock(); - /** @var MockObject|IEMailTemplate $template */ - $template = $this->getMockBuilder(IEMailTemplate::class) - ->disableOriginalConstructor()->getMock(); - - $message = new Message($symfonyEmail, false); - - $template - ->expects($this->once()) - ->method('renderHTML'); - $template - ->expects($this->once()) - ->method('renderText'); - $template - ->expects($this->once()) - ->method('renderSubject'); - - $message->useTemplate($template); - } - - public function testSetAutoSubmitted1(): void { - $headers = new Headers($this->createMock(HeaderInterface::class)); - $headers->addTextHeader(AutoSubmitted::HEADER, 'yes'); - $symfonyEmail = $this->createMock(Email::class); - - $symfonyEmail->method('getHeaders') - ->willReturn($headers); - - $message = new Message($symfonyEmail, false); - $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED); - $this->assertNotSame('no', $message->getAutoSubmitted()); - } - - public function testSetAutoSubmitted2(): void { - $headers = new Headers($this->createMock(HeaderInterface::class)); - $headers->addTextHeader(AutoSubmitted::HEADER, 'no'); - $symfonyEmail = $this->createMock(Email::class); - - $symfonyEmail->method('getHeaders') - ->willReturn($headers); - - $message = new Message($symfonyEmail, false); - $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED); - $this->assertSame('auto-generated', $message->getAutoSubmitted()); - } - - public function testGetAutoSubmitted(): void { - $headers = new Headers($this->createMock(HeaderInterface::class)); - $headers->addTextHeader(AutoSubmitted::HEADER, 'no'); - $symfonyEmail = $this->createMock(Email::class); - - $symfonyEmail->method('getHeaders') - ->willReturn($headers); - - $message = new Message($symfonyEmail, false); - $this->assertSame('no', $message->getAutoSubmitted()); - } -} --- a/tests/lib/Mail/Provider/AddressTest.php +++ /dev/null @@ -1,48 +0,0 @@ -address = new Address('user1@testing.com', 'User One'); - - } - - public function testAddress(): void { - - // test set by constructor - $this->assertEquals('user1@testing.com', $this->address->getAddress()); - // test set by setter - $this->address->setAddress('user2@testing.com'); - $this->assertEquals('user2@testing.com', $this->address->getAddress()); - - } - - public function testLabel(): void { - - // test set by constructor - $this->assertEquals('User One', $this->address->getLabel()); - // test set by setter - $this->address->setLabel('User Two'); - $this->assertEquals('User Two', $this->address->getLabel()); - - } - -} --- a/tests/lib/Mail/Provider/AttachmentTest.php +++ /dev/null @@ -1,73 +0,0 @@ -attachment = new Attachment( - 'This is the contents of a file', - 'example1.txt', - 'text/plain', - false - ); - - } - - public function testName(): void { - - // test set by constructor - $this->assertEquals('example1.txt', $this->attachment->getName()); - // test set by setter - $this->attachment->setName('example2.txt'); - $this->assertEquals('example2.txt', $this->attachment->getName()); - - } - - public function testType(): void { - - // test set by constructor - $this->assertEquals('text/plain', $this->attachment->getType()); - // test set by setter - $this->attachment->setType('text/html'); - $this->assertEquals('text/html', $this->attachment->getType()); - - } - - public function testContents(): void { - - // test set by constructor - $this->assertEquals('This is the contents of a file', $this->attachment->getContents()); - // test set by setter - $this->attachment->setContents('This is the modified contents of a file'); - $this->assertEquals('This is the modified contents of a file', $this->attachment->getContents()); - - } - - public function testEmbedded(): void { - - // test set by constructor - $this->assertEquals(false, $this->attachment->getEmbedded()); - // test set by setter - $this->attachment->setEmbedded(true); - $this->assertEquals(true, $this->attachment->getEmbedded()); - - } - -} --- a/tests/lib/Mail/Provider/ManagerTest.php +++ /dev/null @@ -1,191 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - - // construct service registration - $registration = $this->createMock(ServiceRegistration::class); - $registration - ->method('getService') - ->willReturn('Mock\Provider\MailProvider'); - // construct registration context - $context = $this->createMock(RegistrationContext::class); - $context - ->method('getMailProviders') - ->willReturn([$registration]); - // construct coordinator - $this->coordinator = $this->createMock(Coordinator::class); - $this->coordinator - ->method('getRegistrationContext') - ->willReturn($context); - - // construct mail service - $this->service = $this->createMock(IService::class); - $this->service - ->method('id') - ->willReturn('100'); - $this->service - ->method('getLabel') - ->willReturn('Mock Mail Service'); - $this->service - ->method('getPrimaryAddress') - ->willReturn((new Address('user1@testing.com', 'User One'))); - // construct mail provider - $this->provider = $this->createMock(IProvider::class); - $this->provider - ->method('id') - ->willReturn('mock-provider'); - $this->provider - ->method('label') - ->willReturn('Mock Provider'); - $this->provider - ->method('listServices') - ->willReturnMap([ - ['user0', []], - ['user1', [$this->service->id() => $this->service]] - ]); - $this->provider - ->method('findServiceById') - ->willReturnMap([ - ['user0', '100', null], - ['user1', '100', $this->service] - ]); - $this->provider - ->method('findServiceByAddress') - ->willReturnMap([ - ['user0', 'user0@testing.com', null], - ['user1', 'user1@testing.com', $this->service] - ]); - // construct container interface - $this->container = $this->createMock(ContainerInterface::class); - $this->container - ->method('get') - ->willReturnMap([ - ['Mock\Provider\MailProvider', $this->provider] - ]); - - } - - public function testHas(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with providers found - $this->assertTrue($manager->has()); - - } - - public function testCount(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with providers found - $this->assertGreaterThan(0, $manager->count()); - - } - - public function testTypes(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with providers found - $this->assertEquals(['mock-provider' => 'Mock Provider'], $manager->types()); - - } - - public function testProviders(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with providers found - $this->assertEquals([$this->provider->id() => $this->provider], $manager->providers()); - - } - - public function testFindProviderById(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with providers found - $this->assertEquals($this->provider, $manager->findProviderById($this->provider->id())); - - } - - public function testServices(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with no services found - $this->assertEquals([], $manager->services('user0')); - // test result with services found - $this->assertEquals([$this->provider->id() => [$this->service->id() => $this->service]], $manager->services('user1')); - - } - - public function testFindServiceById(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with no services found and not provider specified - $this->assertEquals(null, $manager->findServiceById('user0', '100')); - // test result with no services found and provider specified - $this->assertEquals(null, $manager->findServiceById('user0', '100', $this->provider->id())); - // test result with services found and not provider specified - $this->assertEquals($this->service, $manager->findServiceById('user1', '100')); - // test result with services found and provider specified - $this->assertEquals($this->service, $manager->findServiceById('user1', '100', $this->provider->id())); - - } - - public function testFindServiceByAddress(): void { - - // construct mail manager - $manager = new Manager($this->coordinator, $this->container, $this->logger); - // test result with no services found and not provider specified - $this->assertEquals(null, $manager->findServiceByAddress('user0', 'user0@testing.com')); - // test result with no services found and provider specified - $this->assertEquals(null, $manager->findServiceByAddress('user0', 'user0@testing.com', $this->provider->id())); - // test result with services found and not provider specified - $this->assertEquals($this->service, $manager->findServiceByAddress('user1', 'user1@testing.com')); - // test result with services found and provider specified - $this->assertEquals($this->service, $manager->findServiceByAddress('user1', 'user1@testing.com', $this->provider->id())); - - } - -} --- a/tests/lib/Mail/Provider/MessageTest.php +++ /dev/null @@ -1,165 +0,0 @@ -message = new Message( - ['id' => 'cd02ea42-feac-4863-b9d8-484d16a587ea'] - ); - $this->address1 = new Address( - 'user1@testing.com', - 'User One' - ); - $this->address2 = new Address( - 'user2@testing.com', - 'User Two' - ); - $this->attachment1 = new Attachment( - 'This is the contents of the first attachment', - 'example1.txt', - 'text/plain', - false - ); - $this->attachment2 = new Attachment( - 'This is the contents of the second attachment', - 'example1.txt', - 'text/plain', - false - ); - - } - - public function testId(): void { - - // test set by constructor - $this->assertEquals('cd02ea42-feac-4863-b9d8-484d16a587ea', $this->message->id()); - - } - - public function testFrom(): void { - - // test not set - $this->assertNull($this->message->getFrom()); - // test set by setter - $this->message->setFrom($this->address1); - $this->assertEquals($this->address1, $this->message->getFrom()); - - } - - public function testReplyTo(): void { - - // test not set - $this->assertNull($this->message->getReplyTo()); - // test set by setter - $this->message->setReplyTo($this->address1); - $this->assertEquals($this->address1, $this->message->getReplyTo()); - - } - - public function testTo(): void { - - // test not set - $this->assertEquals([], $this->message->getTo()); - // test set by setter single - $this->message->setTo($this->address1); - $this->assertEquals([$this->address1], $this->message->getTo()); - // test set by setter multiple - $this->message->setTo($this->address1, $this->address2); - $this->assertEquals([$this->address1, $this->address2], $this->message->getTo()); - - } - - public function testCc(): void { - - // test not set - $this->assertEquals([], $this->message->getCc()); - // test set by setter single - $this->message->setCc($this->address1); - $this->assertEquals([$this->address1], $this->message->getCc()); - // test set by setter multiple - $this->message->setCc($this->address1, $this->address2); - $this->assertEquals([$this->address1, $this->address2], $this->message->getCc()); - - } - - public function testBcc(): void { - - // test not set - $this->assertEquals([], $this->message->getBcc()); - // test set by setter single - $this->message->setBcc($this->address1); - $this->assertEquals([$this->address1], $this->message->getBcc()); - // test set by setter multiple - $this->message->setBcc($this->address1, $this->address2); - $this->assertEquals([$this->address1, $this->address2], $this->message->getBcc()); - - } - - public function testSubject(): void { - - // test not set - $this->assertNull($this->message->getSubject()); - // test set by setter - $this->message->setSubject('Testing Mail Subject'); - $this->assertEquals('Testing Mail Subject', $this->message->getSubject()); - - } - - public function testBody(): void { - - // test not set - $this->assertNull($this->message->getBody()); - // test set by setter - text body - $this->message->setBody('Testing Text Body', false); - $this->assertEquals('Testing Text Body', $this->message->getBody()); - $this->message->setBodyPlain('Testing Text Body Again', false); - $this->assertEquals('Testing Text Body Again', $this->message->getBodyPlain()); - // test set by setter - html body - $this->message->setBody('Testing HTML Body', true); - $this->assertEquals('Testing HTML Body', $this->message->getBody()); - $this->message->setBodyHtml('Testing HTML Body Again', false); - $this->assertEquals('Testing HTML Body Again', $this->message->getBodyHtml()); - - } - - public function testAttachments(): void { - - // test not set - $this->assertEquals([], $this->message->getAttachments()); - // test set by setter single - $this->message->setAttachments($this->attachment1); - $this->assertEquals([$this->attachment1], $this->message->getAttachments()); - // test set by setter multiple - $this->message->setAttachments($this->attachment1, $this->attachment2); - $this->assertEquals([$this->attachment1, $this->attachment2], $this->message->getAttachments()); - - } -} --- a/tests/lib/Memcache/APCuTest.php +++ /dev/null @@ -1,38 +0,0 @@ -markTestSkipped('The APCu extension is not available.'); - return; - } - $this->instance = new APCu($this->getUniqueID()); - } - - public function testCasIntChanged(): void { - $this->instance->set('foo', 1); - $this->assertTrue($this->instance->cas('foo', 1, 2)); - $this->assertEquals(2, $this->instance->get('foo')); - } - - public function testCasIntNotChanged(): void { - $this->instance->set('foo', 1); - $this->assertFalse($this->instance->cas('foo', 2, 3)); - $this->assertEquals(1, $this->instance->get('foo')); - } -} --- a/tests/lib/Memcache/ArrayCacheTest.php +++ /dev/null @@ -1,22 +0,0 @@ -instance = new ArrayCache(''); - } -} --- a/tests/lib/Memcache/Cache.php +++ /dev/null @@ -1,158 +0,0 @@ -assertFalse($this->instance->hasKey('foo')); - $this->instance->set('foo', 'bar'); - $this->assertTrue($this->instance->hasKey('foo')); - } - - public function testGetAfterSet(): void { - $this->assertNull($this->instance->get('foo')); - $this->instance->set('foo', 'bar'); - $this->assertEquals('bar', $this->instance->get('foo')); - } - - public function testGetArrayAfterSet(): void { - $this->assertNull($this->instance->get('foo')); - $this->instance->set('foo', ['bar']); - $this->assertEquals(['bar'], $this->instance->get('foo')); - } - - public function testDoesNotExistAfterRemove(): void { - $this->instance->set('foo', 'bar'); - $this->instance->remove('foo'); - $this->assertFalse($this->instance->hasKey('foo')); - } - - public function testRemoveNonExisting(): void { - $this->instance->remove('foo'); - $this->assertFalse($this->instance->hasKey('foo')); - } - - public function testArrayAccessSet(): void { - $this->instance['foo'] = 'bar'; - $this->assertEquals('bar', $this->instance->get('foo')); - } - - public function testArrayAccessGet(): void { - $this->instance->set('foo', 'bar'); - $this->assertEquals('bar', $this->instance['foo']); - } - - public function testArrayAccessExists(): void { - $this->assertFalse(isset($this->instance['foo'])); - $this->instance->set('foo', 'bar'); - $this->assertTrue(isset($this->instance['foo'])); - } - - public function testArrayAccessUnset(): void { - $this->instance->set('foo', 'bar'); - unset($this->instance['foo']); - $this->assertFalse($this->instance->hasKey('foo')); - } - - public function testAdd(): void { - $this->assertTrue($this->instance->add('foo', 'bar')); - $this->assertEquals('bar', $this->instance->get('foo')); - $this->assertFalse($this->instance->add('foo', 'asd')); - $this->assertEquals('bar', $this->instance->get('foo')); - } - - public function testInc(): void { - $this->assertEquals(1, $this->instance->inc('foo')); - $this->assertEquals(1, $this->instance->get('foo')); - $this->assertEquals(2, $this->instance->inc('foo')); - $this->assertEquals(2, $this->instance->get('foo')); - $this->assertEquals(12, $this->instance->inc('foo', 10)); - $this->assertEquals(12, $this->instance->get('foo')); - - $this->instance->set('foo', 'bar'); - $this->assertFalse($this->instance->inc('foo')); - $this->assertEquals('bar', $this->instance->get('foo')); - } - - public function testDec(): void { - $this->assertFalse($this->instance->dec('foo')); - $this->instance->set('foo', 20); - $this->assertEquals(19, $this->instance->dec('foo')); - $this->assertEquals(19, $this->instance->get('foo')); - $this->assertEquals(9, $this->instance->dec('foo', 10)); - - $this->instance->set('foo', 'bar'); - $this->assertFalse($this->instance->dec('foo')); - $this->assertEquals('bar', $this->instance->get('foo')); - } - - public function testCasNotChanged(): void { - $this->instance->set('foo', 'bar'); - $this->assertTrue($this->instance->cas('foo', 'bar', 'asd')); - $this->assertEquals('asd', $this->instance->get('foo')); - } - - public function testCasChanged(): void { - $this->instance->set('foo', 'bar1'); - $this->assertFalse($this->instance->cas('foo', 'bar', 'asd')); - $this->assertEquals('bar1', $this->instance->get('foo')); - } - - public function testCasNotSet(): void { - $this->assertFalse($this->instance->cas('foo', 'bar', 'asd')); - } - - public function testCadNotChanged(): void { - $this->instance->set('foo', 'bar'); - $this->assertTrue($this->instance->cad('foo', 'bar')); - $this->assertFalse($this->instance->hasKey('foo')); - } - - public function testCadChanged(): void { - $this->instance->set('foo', 'bar1'); - $this->assertFalse($this->instance->cad('foo', 'bar')); - $this->assertTrue($this->instance->hasKey('foo')); - } - - public function testCadNotSet(): void { - $this->assertFalse($this->instance->cad('foo', 'bar')); - } - - public function testNcadNotChanged(): void { - $this->instance->set('foo', 'bar'); - $this->assertFalse($this->instance->ncad('foo', 'bar')); - $this->assertTrue($this->instance->hasKey('foo')); - } - - public function testNcadChanged(): void { - $this->instance->set('foo', 'bar1'); - $this->assertTrue($this->instance->ncad('foo', 'bar')); - $this->assertFalse($this->instance->hasKey('foo')); - } - - public function testNcadNotSet(): void { - $this->assertFalse($this->instance->ncad('foo', 'bar')); - } - - #[\Override] - protected function tearDown(): void { - if ($this->instance) { - $this->instance->clear(); - } - - parent::tearDown(); - } -} --- a/tests/lib/Memcache/CasTraitTest.php +++ /dev/null @@ -1,68 +0,0 @@ -getMockBuilder(CasTraitTestClass::class)->onlyMethods([ - 'set', - 'get', - 'add', - 'remove', - ])->getMock(); - - $mock->expects($this->any()) - ->method('set') - ->willReturnCallback(function ($key, $value, $ttl) use ($sourceCache) { - return $sourceCache->set($key, $value, $ttl); - }); - - $mock->expects($this->any()) - ->method('get') - ->willReturnCallback(function ($key) use ($sourceCache) { - return $sourceCache->get($key); - }); - - $mock->expects($this->any()) - ->method('add') - ->willReturnCallback(function ($key, $value, $ttl) use ($sourceCache) { - return $sourceCache->add($key, $value, $ttl); - }); - - $mock->expects($this->any()) - ->method('remove') - ->willReturnCallback(function ($key) use ($sourceCache) { - return $sourceCache->remove($key); - }); - return $mock; - } - - public function testCasNotChanged(): void { - $cache = $this->getCache(); - $cache->set('foo', 'bar'); - $this->assertTrue($cache->cas('foo', 'bar', 'asd')); - $this->assertEquals('asd', $cache->get('foo')); - } - - public function testCasChanged(): void { - $cache = $this->getCache(); - $cache->set('foo', 'bar1'); - $this->assertFalse($cache->cas('foo', 'bar', 'asd')); - $this->assertEquals('bar1', $cache->get('foo')); - } -} --- a/tests/lib/Memcache/CasTraitTestClass.php +++ /dev/null @@ -1,30 +0,0 @@ -getMockBuilder(LoggerInterface::class)->getMock(); - $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - $serverVersion = $this->createMock(ServerVersion::class); - $factory = new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache, $lockingCache); - $this->assertTrue(is_a($factory->createLocal(), $expectedLocalCache)); - $this->assertTrue(is_a($factory->createDistributed(), $expectedDistributedCache)); - $this->assertTrue(is_a($factory->createLocking(), $expectedLockingCache)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('cacheUnavailableProvider')] - public function testCacheNotAvailableException($localCache, $distributedCache): void { - $this->expectException(HintException::class); - - $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - $serverVersion = $this->createMock(ServerVersion::class); - new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache); - } - - public function testCreateInMemory(): void { - $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); - $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - $serverVersion = $this->createMock(ServerVersion::class); - $factory = new Factory($logger, $profiler, $serverVersion, null, null, null); - - $cache = $factory->createInMemory(); - $cache->set('test', 48); - - self::assertSame(48, $cache->get('test')); - } -} --- a/tests/lib/Memcache/KeyValueCacheFactoryTest.php +++ /dev/null @@ -1,275 +0,0 @@ -config = $this->createMock(SystemConfig::class); - $this->eventLogger = $this->createMock(IEventLogger::class); - $this->factory = new KeyValueCacheFactory($this->config, $this->eventLogger); - } - - public function testSingleServerTcp(): void { - [$parameters, $options] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example', 'port' => 6380], - ]); - - $this->assertSame([ - 'scheme' => 'tcp', - 'host' => 'cache.example', - 'port' => 6380, - ], $parameters); - $this->assertSame([], $options); - } - - public function testSingleServerDefaultPort(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - ]); - - $this->assertSame(6379, $parameters['port']); - } - - public function testSingleServerUnixSocketFromProtocol(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => '/tmp/cache.sock', 'protocol' => 'unix'], - ]); - - $this->assertSame('unix', $parameters['scheme']); - $this->assertSame('/tmp/cache.sock', $parameters['path']); - $this->assertArrayNotHasKey('port', $parameters); - $this->assertArrayNotHasKey('host', $parameters); - } - - public function testSingleServerUnixSocketInferredFromHost(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => '/tmp/cache.sock'], - ]); - - $this->assertSame('unix', $parameters['scheme']); - $this->assertSame('/tmp/cache.sock', $parameters['path']); - } - - public function testSingleServerTls(): void { - $sslContext = ['cafile' => '/certs/ca.crt']; - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example', 'protocol' => 'tls'], - 'ssl_context' => $sslContext, - ]); - - $this->assertSame('tls', $parameters['scheme']); - $this->assertSame($sslContext, $parameters['ssl']); - } - - public function testSslContextIgnoredWithoutTls(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'ssl_context' => ['cafile' => '/certs/ca.crt'], - ]); - - $this->assertArrayNotHasKey('ssl', $parameters); - } - - public function testSingleServerWithDbIndex(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'dbindex' => 5, - ]); - - $this->assertSame(5, $parameters['database']); - } - - public function testAuthWithUserAndPassword(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'user' => 'nextcloud', - 'password' => 's3cret', - ]); - - $this->assertSame('nextcloud', $parameters['username']); - $this->assertSame('s3cret', $parameters['password']); - } - - public function testAuthWithPasswordOnly(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'password' => 's3cret', - ]); - - $this->assertSame('s3cret', $parameters['password']); - $this->assertArrayNotHasKey('username', $parameters); - } - - public function testEmptyPasswordIsIgnored(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'user' => 'nextcloud', - 'password' => '', - ]); - - $this->assertArrayNotHasKey('password', $parameters); - $this->assertArrayNotHasKey('username', $parameters); - } - - public function testTimeoutAndPersistence(): void { - [$parameters] = $this->factory->buildConnectionConfig([ - 'server' => ['host' => 'cache.example'], - 'timeout' => 1.5, - 'read_timeout' => 2.5, - 'persistent' => true, - ]); - - $this->assertSame(1.5, $parameters['timeout']); - $this->assertSame(2.5, $parameters['read_write_timeout']); - $this->assertTrue($parameters['persistent']); - } - - public function testMissingServerThrows(): void { - $this->expectException(\RuntimeException::class); - $this->factory->buildConnectionConfig([]); - } - - public function testCluster(): void { - [$parameters, $options] = $this->factory->buildConnectionConfig([ - 'seeds' => [ - ['host' => 'node1', 'port' => 7000], - ['host' => 'node2', 'port' => 7001], - ], - 'password' => 's3cret', - ]); - - $this->assertCount(2, $parameters); - $this->assertSame('node1', $parameters[0]['host']); - $this->assertSame(7000, $parameters[0]['port']); - $this->assertSame('node2', $parameters[1]['host']); - $this->assertSame('redis', $options['cluster']); - // Authentication is propagated to the nodes discovered in the cluster - $this->assertSame('s3cret', $options['parameters']['password']); - } - - public function testClusterEmptySeedsThrows(): void { - $this->expectException(\RuntimeException::class); - $this->factory->buildConnectionConfig(['seeds' => []]); - } - - public function testSentinel(): void { - [$parameters, $options] = $this->factory->buildConnectionConfig([ - 'sentinel' => [ - 'service' => 'mymaster', - 'seeds' => [ - ['host' => 'sentinel1', 'port' => 26379], - ['host' => 'sentinel2', 'port' => 26380], - ], - ], - 'password' => 's3cret', - 'dbindex' => 3, - ]); - - $this->assertCount(2, $parameters); - $this->assertSame('sentinel1', $parameters[0]['host']); - $this->assertSame(26379, $parameters[0]['port']); - $this->assertSame('sentinel', $options['replication']); - $this->assertSame('mymaster', $options['service']); - // Auth and database are applied to the resolved primary / replica - $this->assertSame('s3cret', $options['parameters']['password']); - $this->assertSame(3, $options['parameters']['database']); - } - - public function testSentinelMissingServiceThrows(): void { - $this->expectException(\RuntimeException::class); - $this->factory->buildConnectionConfig([ - 'sentinel' => ['seeds' => [['host' => 'sentinel1', 'port' => 26379]]], - ]); - } - - public function testSentinelMissingSeedsThrows(): void { - $this->expectException(\RuntimeException::class); - $this->factory->buildConnectionConfig([ - 'sentinel' => ['service' => 'mymaster'], - ]); - } - - public function testIsAvailableWithoutConfig(): void { - $this->config->method('getValue')->with('memcache.kvstore', [])->willReturn([]); - $this->assertFalse($this->factory->isAvailable()); - } - - public function testIsAvailableWithConfig(): void { - $this->config->method('getValue')->with('memcache.kvstore', []) - ->willReturn(['server' => ['host' => 'localhost']]); - $this->assertTrue($this->factory->isAvailable()); - } - - public function testGetInstanceThrowsWhenUnavailable(): void { - $this->config->method('getValue')->with('memcache.kvstore', [])->willReturn([]); - $this->expectException(\RuntimeException::class); - $this->factory->getInstance(); - } - - public function testGetInstanceSingleServer(): void { - $this->config->method('getValue')->with('memcache.kvstore', []) - ->willReturn(['server' => ['host' => 'localhost', 'port' => 6379]]); - - $client = $this->factory->getInstance(); - $this->assertInstanceOf(Client::class, $client); - $this->assertInstanceOf(NodeConnectionInterface::class, $client->getConnection()); - } - - public function testGetInstanceCluster(): void { - $this->config->method('getValue')->with('memcache.kvstore', []) - ->willReturn(['seeds' => [['host' => 'localhost', 'port' => 7000]]]); - - $client = $this->factory->getInstance(); - $this->assertInstanceOf(ClusterInterface::class, $client->getConnection()); - } - - public function testGetInstanceSentinel(): void { - $this->config->method('getValue')->with('memcache.kvstore', []) - ->willReturn([ - 'sentinel' => [ - 'service' => 'mymaster', - 'seeds' => [['host' => 'localhost', 'port' => 26379]], - ], - ]); - - $client = $this->factory->getInstance(); - $this->assertInstanceOf(SentinelReplication::class, $client->getConnection()); - } - - public function testGetInstanceIsMemoized(): void { - $this->config->method('getValue')->with('memcache.kvstore', []) - ->willReturn(['server' => ['host' => 'localhost']]); - - $this->assertSame($this->factory->getInstance(), $this->factory->getInstance()); - } -} --- a/tests/lib/Memcache/KeyValueCacheTest.php +++ /dev/null @@ -1,108 +0,0 @@ -getSystemValue('memcache.kvstore', []) === []) { - self::markTestSkipped('Key-value store not configured in config.php'); - } - - if (!KeyValueCache::isAvailable()) { - self::markTestSkipped('The predis library is not available.'); - } - - try { - $instance = new KeyValueCache(self::getUniqueID()); - if ($instance->set(self::getUniqueID(), self::getUniqueID()) === false) { - self::markTestSkipped('Key-value store server seems to be down.'); - } - } catch (CommunicationException $e) { - self::markTestSkipped('Key-value store server is not reachable: ' . $e->getMessage()); - } - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->instance = new KeyValueCache($this->getUniqueID()); - } - - /** - * @return array - */ - public static function roundtripValuesProvider(): array { - return [ - 'string' => ['some string value'], - 'empty string' => [''], - 'numeric string' => ['0123'], - 'integer' => [1234], - 'zero' => [0], - 'negative integer' => [-42], - 'boolean true' => [true], - 'boolean false' => [false], - 'null' => [null], - 'list' => [['a', 'b', 'c']], - 'associative array' => [['foo' => 'bar', 'baz' => 42]], - 'nested array' => [['a' => ['b' => ['c' => 'd']]]], - ]; - } - - /** - * A value that is written to the cache must be readable again unchanged. - * - * Runs against every configured topology (single server, Sentinel and - * cluster) through the CI matrix, see .github/workflows/phpunit-kvstore.yml. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('roundtripValuesProvider')] - public function testStoreAndReadBack(mixed $value): void { - $this->assertNull($this->instance->get('roundtrip'), 'key should be empty before storing'); - - $this->assertNotFalse($this->instance->set('roundtrip', $value), 'value should be stored'); - $this->assertTrue($this->instance->hasKey('roundtrip'), 'stored key should exist'); - $this->assertEquals($value, $this->instance->get('roundtrip'), 'stored value should be read back unchanged'); - } - - public function testScriptHashes(): void { - $scripts = self::invokePrivate($this->instance, 'LUA_SCRIPTS'); - foreach ($scripts as $script) { - $this->assertEquals(sha1($script[0]), $script[1]); - } - } - - public function testCasTtlNotChanged(): void { - $this->instance->set('foo', 'bar', 50); - $this->assertTrue($this->instance->compareSetTTL('foo', 'bar', 100)); - // allow for 1s of inaccuracy due to time moving forward - $this->assertLessThan(1, 100 - $this->instance->getTTL('foo')); - } - - public function testCasTtlChanged(): void { - $this->instance->set('foo', 'bar1', 50); - $this->assertFalse($this->instance->compareSetTTL('foo', 'bar', 100)); - // allow for 1s of inaccuracy due to time moving forward - $this->assertLessThan(1, 50 - $this->instance->getTTL('foo')); - } -} --- a/tests/lib/Memcache/MemcachedTest.php +++ /dev/null @@ -1,58 +0,0 @@ -set(self::getUniqueID(), self::getUniqueID()) === false) { - self::markTestSkipped('memcached server seems to be down.'); - } - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->instance = new Memcached($this->getUniqueID()); - } - - #[\Override] - public function testClear(): void { - // Memcached is sometimes broken with clear(), so we don't test it thoroughly - $value = 'ipsum lorum'; - $this->instance->set('1_value1', $value); - $this->instance->set('1_value2', $value); - $this->instance->set('2_value1', $value); - $this->instance->set('3_value1', $value); - - $this->assertTrue($this->instance->clear('1_')); - - $this->assertFalse($this->instance->hasKey('1_value1')); - $this->assertFalse($this->instance->hasKey('1_value2')); - //$this->assertTrue($this->instance->hasKey('2_value1')); - //$this->assertTrue($this->instance->hasKey('3_value1')); - - $this->assertTrue($this->instance->clear()); - - $this->assertFalse($this->instance->hasKey('1_value1')); - $this->assertFalse($this->instance->hasKey('1_value2')); - $this->assertFalse($this->instance->hasKey('2_value1')); - $this->assertFalse($this->instance->hasKey('3_value1')); - } -} --- a/tests/lib/Memcache/RedisTest.php +++ /dev/null @@ -1,120 +0,0 @@ -getSystemValue('redis', []) === []) { - self::markTestSkipped('Redis not configured in config.php'); - } - - $errorOccurred = false; - set_error_handler( - function ($errno, $errstr): void { - throw new \RuntimeException($errstr, 123456789); - }, - E_WARNING - ); - $instance = null; - try { - $instance = new Redis(self::getUniqueID()); - } catch (\RuntimeException $e) { - $errorOccurred = $e->getCode() === 123456789 ? $e->getMessage() : false; - } - restore_error_handler(); - if ($errorOccurred !== false) { - self::markTestSkipped($errorOccurred); - } - - if ($instance === null) { - throw new \Exception('redis server is not reachable'); - } - - if ($instance->set(self::getUniqueID(), self::getUniqueID()) === false) { - self::markTestSkipped('redis server seems to be down.'); - } - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $this->instance = new Redis($this->getUniqueID()); - } - - public function testScriptHashes(): void { - foreach (Redis::LUA_SCRIPTS as $script) { - $this->assertEquals(sha1($script[0]), $script[1]); - } - } - - public function testCasTtlNotChanged(): void { - $this->instance->set('foo', 'bar', 50); - $this->assertTrue($this->instance->compareSetTTL('foo', 'bar', 100)); - // allow for 1s of inaccuracy due to time moving forward - $this->assertLessThan(1, 100 - $this->instance->getTTL('foo')); - } - - public function testCasTtlChanged(): void { - $this->instance->set('foo', 'bar1', 50); - $this->assertFalse($this->instance->compareSetTTL('foo', 'bar', 100)); - // allow for 1s of inaccuracy due to time moving forward - $this->assertLessThan(1, 50 - $this->instance->getTTL('foo')); - } - - public function testClearWithPrefixOnlyRemovesMatchingKeys(): void { - $this->instance->set('foo1', 'a'); - $this->instance->set('foo2', 'b'); - $this->instance->set('bar1', 'c'); - - $this->assertTrue($this->instance->clear('foo')); - - $this->assertFalse($this->instance->hasKey('foo1')); - $this->assertFalse($this->instance->hasKey('foo2')); - $this->assertTrue($this->instance->hasKey('bar1')); - } - - public function testClearWithoutMatchesReturnsTrue(): void { - // Nothing is stored under this prefix; clearing must not error out - // (regression guard for calling UNLINK/DEL with an empty key list). - $this->assertTrue($this->instance->clear('no-such-prefix')); - } - - public function testClearRemovesEntriesAcrossMultipleScanBatches(): void { - // More keys than a single SCAN batch (self::SCAN_COUNT) to exercise the - // cursor loop and make sure nothing is left behind. - $count = 1500; - for ($i = 0; $i < $count; $i++) { - $this->instance->set('bulk-' . $i, $i); - } - - $this->assertTrue($this->instance->clear('bulk-')); - - $this->assertFalse($this->instance->hasKey('bulk-0')); - $this->assertFalse($this->instance->hasKey('bulk-' . ($count - 1))); - $this->assertFalse($this->instance->hasKey('bulk-' . intdiv($count, 2))); - } -} --- a/tests/lib/MemoryInfoTest.php +++ /dev/null @@ -1,85 +0,0 @@ -iniSettingBeforeTest = ini_get('memory_limit'); - } - - #[\PHPUnit\Framework\Attributes\AfterClass()] - public function restoreMemoryInfoIniSetting() { - ini_set('memory_limit', $this->iniSettingBeforeTest); - } - - public static function getMemoryLimitTestData(): array { - return [ - 'unlimited' => ['-1', -1,], - '524288000 bytes' => ['524288000', 524288000,], - '500M' => ['500M', 524288000,], - '512000K' => ['512000K', 524288000,], - '2G' => ['2G', 2147483648,], - ]; - } - - /** - * Tests that getMemoryLimit works as expected. - * - * @param string $iniValue The "memory_limit" ini data. - * @param int|float $expected The expected detected memory limit. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getMemoryLimitTestData')] - public function testMemoryLimit(string $iniValue, int|float $expected): void { - ini_set('memory_limit', $iniValue); - $memoryInfo = new MemoryInfo(); - self::assertEquals($expected, $memoryInfo->getMemoryLimit()); - } - - public static function getSufficientMemoryTestData(): array { - return [ - 'unlimited' => [-1, true,], - '512M' => [512 * 1024 * 1024, true,], - '1G' => [1024 * 1024 * 1024, true,], - '256M' => [256 * 1024 * 1024, false,], - ]; - } - - /** - * Tests that isMemoryLimitSufficient returns the correct values. - * - * @param int $memoryLimit The memory limit - * @param bool $expected If the memory limit is sufficient. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getSufficientMemoryTestData')] - public function testIsMemoryLimitSufficient(int $memoryLimit, bool $expected): void { - /* @var MemoryInfo|MockObject $memoryInfo */ - $memoryInfo = $this->getMockBuilder(MemoryInfo::class) - ->onlyMethods(['getMemoryLimit',]) - ->getMock(); - - $memoryInfo - ->method('getMemoryLimit') - ->willReturn($memoryLimit); - - $isMemoryLimitSufficient = $memoryInfo->isMemoryLimitSufficient(); - self::assertEquals($expected, $isMemoryLimitSufficient); - } -} --- a/tests/lib/Migration/BackgroundRepairTest.php +++ /dev/null @@ -1,105 +0,0 @@ -jobList = $this->createMock(JobList::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->time = $this->createMock(ITimeFactory::class); - $this->time->method('getTime') - ->willReturn(999999); - $this->appManager = $this->createMock(IAppManager::class); - $this->repair = new Repair($this->dispatcher, $this->logger); - $this->job = new BackgroundRepair($this->repair, $this->time, $this->logger, $this->jobList, $this->appManager); - } - - public function testNoArguments(): void { - $this->jobList->expects($this->once())->method('remove'); - $this->job->start($this->jobList); - } - - public function testUnknownStep(): void { - $this->dispatcher->expects($this->never())->method('dispatchTyped'); - - $this->jobList->expects($this->once())->method('remove'); - $this->logger->expects($this->once())->method('error'); - - $this->job->setArgument([ - 'app' => 'test', - 'step' => 'j' - ]); - $this->job->start($this->jobList); - } - - public function testWorkingStep(): void { - $this->dispatcher->expects($this->once())->method('dispatchTyped') - ->with($this->equalTo(new RepairStepEvent('A test repair step'))); - - $this->jobList->expects($this->once())->method('remove'); - $this->appManager->expects(self::once()) - ->method('loadApp') - ->with('test'); - - $this->job->setArgument([ - 'app' => 'test', - 'step' => '\Test\Migration\TestRepairStep' - ]); - $this->job->start($this->jobList); - } -} --- a/tests/lib/Migration/MetadataManagerTest.php +++ /dev/null @@ -1,197 +0,0 @@ -appManager = Server::get(IAppManager::class); - } - - public function testExtractMigrationAttributes(): void { - $metadataManager = Server::get(MetadataManager::class); - $this->appManager->loadApp('testing'); - - $this->assertEquals( - self::getMigrationMetadata(), - json_decode(json_encode($metadataManager->extractMigrationAttributes('testing')), true), - ); - - $this->appManager->disableApp('testing'); - } - - public function testDeserializeMigrationMetadata(): void { - $metadataManager = Server::get(MetadataManager::class); - $this->assertEquals( - [ - 'core' => [], - 'apps' => [ - 'testing' => [ - '30000Date20240102030405' => [ - new DropTable('old_table'), - new CreateTable('new_table', - description: 'Table is used to store things, but also to get more things', - notes: ['this is a notice', 'and another one, if really needed'] - ), - new AddColumn('my_table'), - new AddColumn('my_table', 'another_field'), - new AddColumn('other_table', 'last_one', ColumnType::DATE), - new AddIndex('my_table'), - new AddIndex('my_table', IndexType::PRIMARY), - new DropColumn('other_table'), - new DropColumn('other_table', 'old_column', - description: 'field is not used anymore and replaced by \'last_one\'' - ), - new DropIndex('other_table'), - new ModifyColumn('other_table'), - new ModifyColumn('other_table', 'this_field'), - new ModifyColumn('other_table', 'this_field', ColumnType::BIGINT) - ] - ] - ] - ], - $metadataManager->getMigrationsAttributesFromReleaseMetadata( - [ - 'core' => [], - 'apps' => ['testing' => self::getMigrationMetadata()] - ] - ) - ); - } - - private static function getMigrationMetadata(): array { - return [ - '30000Date20240102030405' => [ - [ - 'class' => 'OCP\\Migration\\Attributes\\DropTable', - 'table' => 'old_table', - 'description' => '', - 'notes' => [], - 'columns' => [] - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\CreateTable', - 'table' => 'new_table', - 'description' => 'Table is used to store things, but also to get more things', - 'notes' - => [ - 'this is a notice', - 'and another one, if really needed' - ], - 'columns' => [] - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\AddColumn', - 'table' => 'my_table', - 'description' => '', - 'notes' => [], - 'name' => '', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\AddColumn', - 'table' => 'my_table', - 'description' => '', - 'notes' => [], - 'name' => 'another_field', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\AddColumn', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'name' => 'last_one', - 'type' => 'date' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\AddIndex', - 'table' => 'my_table', - 'description' => '', - 'notes' => [], - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\AddIndex', - 'table' => 'my_table', - 'description' => '', - 'notes' => [], - 'type' => 'primary' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\DropColumn', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'name' => '', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\DropColumn', - 'table' => 'other_table', - 'description' => 'field is not used anymore and replaced by \'last_one\'', - 'notes' => [], - 'name' => 'old_column', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\DropIndex', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'name' => '', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'name' => 'this_field', - 'type' => '' - ], - [ - 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', - 'table' => 'other_table', - 'description' => '', - 'notes' => [], - 'name' => 'this_field', - 'type' => 'bigint' - ], - ] - ]; - } -} --- a/tests/lib/Mock/Config/MockAppConfig.php +++ /dev/null @@ -1,200 +0,0 @@ -config[$app][$key]); - } - - #[\Override] - public function getValues($app, $key): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getFilteredValues($app): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getApps(): array { - return array_keys($this->config); - } - - #[\Override] - public function getKeys(string $app): array { - return array_keys($this->config[$app] ?? []); - } - - #[\Override] - public function isSensitive(string $app, string $key, ?bool $lazy = false): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function isLazy(string $app, string $key): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValueString(string $app, string $key, string $default = '', bool $lazy = false): string { - return (string)(($this->config[$app] ?? [])[$key] ?? $default); - } - - #[\Override] - public function getValueInt(string $app, string $key, int $default = 0, bool $lazy = false): int { - return (int)(($this->config[$app] ?? [])[$key] ?? $default); - } - - #[\Override] - public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float { - return (float)(($this->config[$app] ?? [])[$key] ?? $default); - } - - #[\Override] - public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool { - return (bool)(($this->config[$app] ?? [])[$key] ?? $default); - } - - #[\Override] - public function getValueArray(string $app, string $key, array $default = [], bool $lazy = false): array { - return ($this->config[$app] ?? [])[$key] ?? $default; - } - - #[\Override] - public function getValueType(string $app, string $key, ?bool $lazy = null): int { - throw new \Exception('not implemented'); - } - - #[\Override] - public function setValueString(string $app, string $key, string $value, bool $lazy = false, bool $sensitive = false): bool { - $this->config[$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueInt(string $app, string $key, int $value, bool $lazy = false, bool $sensitive = false): bool { - $this->config[$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueFloat(string $app, string $key, float $value, bool $lazy = false, bool $sensitive = false): bool { - $this->config[$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueBool(string $app, string $key, bool $value, bool $lazy = false): bool { - $this->config[$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueArray(string $app, string $key, array $value, bool $lazy = false, bool $sensitive = false): bool { - $this->config[$app][$key] = $value; - return true; - } - - #[\Override] - public function updateSensitive(string $app, string $key, bool $sensitive): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateLazy(string $app, string $key, bool $lazy): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getDetails(string $app, string $key): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function convertTypeToInt(string $type): int { - return match (strtolower($type)) { - 'mixed' => IAppConfig::VALUE_MIXED, - 'string' => IAppConfig::VALUE_STRING, - 'integer' => IAppConfig::VALUE_INT, - 'float' => IAppConfig::VALUE_FLOAT, - 'boolean' => IAppConfig::VALUE_BOOL, - 'array' => IAppConfig::VALUE_ARRAY, - default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type) - }; - } - - #[\Override] - public function convertTypeToString(int $type): string { - $type &= ~self::VALUE_SENSITIVE; - - return match ($type) { - IAppConfig::VALUE_MIXED => 'mixed', - IAppConfig::VALUE_STRING => 'string', - IAppConfig::VALUE_INT => 'integer', - IAppConfig::VALUE_FLOAT => 'float', - IAppConfig::VALUE_BOOL => 'boolean', - IAppConfig::VALUE_ARRAY => 'array', - default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type) - }; - } - - #[\Override] - public function deleteKey(string $app, string $key): void { - if ($this->hasKey($app, $key)) { - unset($this->config[$app][$key]); - } - } - - #[\Override] - public function deleteApp(string $app): void { - if (isset($this->config[$app])) { - unset($this->config[$app]); - } - } - - #[\Override] - public function clearCache(bool $reload = false): void { - } - - #[\Override] - public function searchKeys(string $app, string $prefix = '', bool $lazy = false): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getKeyDetails(string $app, string $key): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getAppInstalledVersions(bool $onlyEnabled = false): array { - throw new \Exception('not implemented'); - } -} --- a/tests/lib/Mock/Config/MockUserConfig.php +++ /dev/null @@ -1,249 +0,0 @@ -config); - } - - #[\Override] - public function getApps(string $userId): array { - return array_keys($this->config[$userId] ?? []); - } - - #[\Override] - public function getKeys(string $userId, string $app): array { - if (isset($this->config[$userId][$app])) { - return array_keys($this->config[$userId][$app]); - } else { - return []; - } - } - - #[\Override] - public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool { - return isset($this->config[$userId][$app][$key]); - } - - #[\Override] - public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function isLazy(string $userId, string $app, string $key): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValues(string $userId, string $app, string $prefix = '', bool $filtered = false): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getAllValues(string $userId, bool $filtered = false): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValuesByUsers(string $app, string $key, ?ValueType $typedAs = null, ?array $userIds = null): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator { - throw new \Exception('not implemented'); - } - - #[\Override] - public function searchUsersByValueInt(string $app, string $key, int $value): Generator { - throw new \Exception('not implemented'); - } - - #[\Override] - public function searchUsersByValues(string $app, string $key, array $values): Generator { - throw new \Exception('not implemented'); - } - - #[\Override] - public function searchUsersByValueBool(string $app, string $key, bool $value): Generator { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValueString(string $userId, string $app, string $key, string $default = '', bool $lazy = false): string { - if (isset($this->config[$userId][$app])) { - return (string)$this->config[$userId][$app][$key]; - } else { - return $default; - } - } - - #[\Override] - public function getValueInt(string $userId, string $app, string $key, int $default = 0, bool $lazy = false): int { - if (isset($this->config[$userId][$app])) { - return (int)$this->config[$userId][$app][$key]; - } else { - return $default; - } - } - - #[\Override] - public function getValueFloat(string $userId, string $app, string $key, float $default = 0, bool $lazy = false): float { - if (isset($this->config[$userId][$app])) { - return (float)$this->config[$userId][$app][$key]; - } else { - return $default; - } - } - - #[\Override] - public function getValueBool(string $userId, string $app, string $key, bool $default = false, bool $lazy = false): bool { - if (isset($this->config[$userId][$app])) { - return (bool)$this->config[$userId][$app][$key]; - } else { - return $default; - } - } - - #[\Override] - public function getValueArray(string $userId, string $app, string $key, array $default = [], bool $lazy = false): array { - if (isset($this->config[$userId][$app])) { - return $this->config[$userId][$app][$key]; - } else { - return $default; - } - } - - #[\Override] - public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int { - throw new \Exception('not implemented'); - } - - #[\Override] - public function setValueString(string $userId, string $app, string $key, string $value, bool $lazy = false, int $flags = 0): bool { - $this->config[$userId][$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueInt(string $userId, string $app, string $key, int $value, bool $lazy = false, int $flags = 0): bool { - $this->config[$userId][$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueFloat(string $userId, string $app, string $key, float $value, bool $lazy = false, int $flags = 0): bool { - $this->config[$userId][$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueBool(string $userId, string $app, string $key, bool $value, bool $lazy = false): bool { - $this->config[$userId][$app][$key] = $value; - return true; - } - - #[\Override] - public function setValueArray(string $userId, string $app, string $key, array $value, bool $lazy = false, int $flags = 0): bool { - $this->config[$userId][$app][$key] = $value; - return true; - } - - #[\Override] - public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateGlobalIndexed(string $app, string $key, bool $indexed): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool { - throw new \Exception('not implemented'); - } - - #[\Override] - public function updateGlobalLazy(string $app, string $key, bool $lazy): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function getDetails(string $userId, string $app, string $key): array { - throw new \Exception('not implemented'); - } - - #[\Override] - public function deleteUserConfig(string $userId, string $app, string $key): void { - unset($this->config[$userId][$app][$key]); - } - - #[\Override] - public function deleteKey(string $app, string $key): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function deleteApp(string $app): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function deleteAllUserConfig(string $userId): void { - unset($this->config[$userId]); - } - - #[\Override] - public function clearCache(string $userId, bool $reload = false): void { - throw new \Exception('not implemented'); - } - - #[\Override] - public function clearCacheAll(): void { - throw new \Exception('not implemented'); - } -} --- a/tests/lib/NaturalSortTest.php +++ /dev/null @@ -1,266 +0,0 @@ -markTestSkipped('The intl module is not available, natural sorting might not work as expected.'); - return; - } - $comparator = NaturalSort::getInstance(); - usort($array, [$comparator, 'compare']); - $this->assertEquals($sorted, $array); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('defaultCollatorDataProvider')] - public function testDefaultCollatorCompare($array, $sorted): void { - $comparator = new NaturalSort(new NaturalSort_DefaultCollator()); - usort($array, [$comparator, 'compare']); - $this->assertEquals($sorted, $array); - } - - /** - * Data provider for natural sorting with php5-intl's Collator. - * Must provide the same result as in core/js/tests/specs/coreSpec.js - * @return array test cases - */ - public static function naturalSortDataProvider(): array { - return [ - // different casing - [ - // unsorted - [ - 'aaa', - 'bbb', - 'BBB', - 'AAA' - ], - // sorted - [ - 'aaa', - 'AAA', - 'bbb', - 'BBB' - ] - ], - // numbers - [ - // unsorted - [ - '124.txt', - 'abc1', - '123.txt', - 'abc', - 'abc2', - 'def (2).txt', - 'ghi 10.txt', - 'abc12', - 'def.txt', - 'def (1).txt', - 'ghi 2.txt', - 'def (10).txt', - 'abc10', - 'def (12).txt', - 'z', - 'ghi.txt', - 'za', - 'ghi 1.txt', - 'ghi 12.txt', - 'zz', - '15.txt', - '15b.txt', - ], - // sorted - [ - '15.txt', - '15b.txt', - '123.txt', - '124.txt', - 'abc', - 'abc1', - 'abc2', - 'abc10', - 'abc12', - 'def.txt', - 'def (1).txt', - 'def (2).txt', - 'def (10).txt', - 'def (12).txt', - 'ghi.txt', - 'ghi 1.txt', - 'ghi 2.txt', - 'ghi 10.txt', - 'ghi 12.txt', - 'z', - 'za', - 'zz', - ] - ], - // chinese characters - [ - // unsorted - [ - '十.txt', - '一.txt', - '二.txt', - '十 2.txt', - '三.txt', - '四.txt', - 'abc.txt', - '五.txt', - '七.txt', - '八.txt', - '九.txt', - '六.txt', - '十一.txt', - '波.txt', - '破.txt', - '莫.txt', - '啊.txt', - '123.txt', - ], - // sorted - [ - '123.txt', - 'abc.txt', - '一.txt', - '七.txt', - '三.txt', - '九.txt', - '二.txt', - '五.txt', - '八.txt', - '六.txt', - '十.txt', - '十 2.txt', - '十一.txt', - '啊.txt', - '四.txt', - '波.txt', - '破.txt', - '莫.txt', - ] - ], - // with umlauts - [ - // unsorted - [ - 'öh.txt', - 'Äh.txt', - 'oh.txt', - 'Üh 2.txt', - 'Üh.txt', - 'ah.txt', - 'Öh.txt', - 'uh.txt', - 'üh.txt', - 'äh.txt', - ], - // sorted - [ - 'ah.txt', - 'äh.txt', - 'Äh.txt', - 'oh.txt', - 'öh.txt', - 'Öh.txt', - 'uh.txt', - 'üh.txt', - 'Üh.txt', - 'Üh 2.txt', - ] - ], - ]; - } - - /** - * Data provider for natural sorting with \OC\NaturalSort_DefaultCollator. - * Must provide the same result as in core/js/tests/specs/coreSpec.js - * @return array test cases - */ - public static function defaultCollatorDataProvider(): array { - return [ - // different casing - [ - // unsorted - [ - 'aaa', - 'bbb', - 'BBB', - 'AAA' - ], - // sorted - [ - 'aaa', - 'AAA', - 'bbb', - 'BBB' - ] - ], - // numbers - [ - // unsorted - [ - '124.txt', - 'abc1', - '123.txt', - 'abc', - 'abc2', - 'def (2).txt', - 'ghi 10.txt', - 'abc12', - 'def.txt', - 'def (1).txt', - 'ghi 2.txt', - 'def (10).txt', - 'abc10', - 'def (12).txt', - 'z', - 'ghi.txt', - 'za', - 'ghi 1.txt', - 'ghi 12.txt', - 'zz', - '15.txt', - '15b.txt', - ], - // sorted - [ - '15.txt', - '15b.txt', - '123.txt', - '124.txt', - 'abc', - 'abc1', - 'abc2', - 'abc10', - 'abc12', - 'def.txt', - 'def (1).txt', - 'def (2).txt', - 'def (10).txt', - 'def (12).txt', - 'ghi.txt', - 'ghi 1.txt', - 'ghi 2.txt', - 'ghi 10.txt', - 'ghi 12.txt', - 'z', - 'za', - 'zz', - ] - ], - ]; - } -} --- a/tests/lib/NavigationManagerTest.php +++ /dev/null @@ -1,963 +0,0 @@ -appManager = $this->createMock(AppManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->l10nFac = $this->createMock(IFactory::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->groupManager = $this->createMock(Manager::class); - $this->config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->navigationManager = new NavigationManager( - $this->appManager, - $this->urlGenerator, - $this->l10nFac, - $this->userSession, - $this->groupManager, - $this->config, - $this->logger, - $this->dispatcher, - ); - - $this->navigationManager->clear(false); - } - - public static function addArrayData(): array { - return [ - [ - 'entry' => [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'icon' => 'optional', - 'href' => 'url', - 'type' => 'settings', - 'classes' => '', - 'unread' => 0 - ], - 'expectedEntry' => [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'icon' => 'optional', - 'href' => 'url', - 'active' => false, - 'type' => 'settings', - 'classes' => '', - 'unread' => 0, - ] - ], - [ - 'entry' => [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - //'icon' => 'optional', - 'href' => 'url', - 'active' => true, - 'unread' => 0, - ], - 'expectedEntry' => [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'icon' => '', - 'href' => 'url', - 'active' => false, - 'type' => 'link', - 'classes' => '', - 'unread' => 0, - 'default' => true, - ] - ] - ]; - } - - /** - * - * @param array $entry - * @param array $expectedEntry - */ - #[\PHPUnit\Framework\Attributes\DataProvider('addArrayData')] - public function testAddArray(array $entry, array $expectedEntry): void { - $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists'); - $this->navigationManager->add($entry); - - $navigationEntries = $this->navigationManager->getAll('all'); - $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists'); - $this->assertEquals($expectedEntry, $navigationEntries['entry id']); - - $this->navigationManager->clear(false); - $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()'); - } - - /** - * - * @param array $entry - * @param array $expectedEntry - */ - #[\PHPUnit\Framework\Attributes\DataProvider('addArrayData')] - public function testAddClosure(array $entry, array $expectedEntry): void { - global $testAddClosureNumberOfCalls; - $testAddClosureNumberOfCalls = 0; - - $this->navigationManager->add(function () use ($entry) { - global $testAddClosureNumberOfCalls; - $testAddClosureNumberOfCalls++; - - return $entry; - }); - - $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()'); - - $this->navigationManager->setup(); - $navigationEntries = $this->navigationManager->getAll('all'); - $this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is called by getAll()'); - $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists'); - $this->assertEquals($expectedEntry, $navigationEntries['entry id']); - - $navigationEntries = $this->navigationManager->getAll('all'); - $this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is only called once for getAll()'); - $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists'); - $this->assertEquals($expectedEntry, $navigationEntries['entry id']); - - $this->navigationManager->clear(false); - $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()'); - } - - /** - * Entry points that never call setup() (e.g. the OCS dispatch in ocs/v1.php) - * must not silently lose closure-registered entries such as an app's nav - * link: getAll() should only resolve what it can, not resolve nothing. - */ - public function testGetAllDoesNotResolveClosureBeforeSetup(): void { - $numberOfCalls = 0; - $this->navigationManager->add(function () use (&$numberOfCalls) { - $numberOfCalls++; - - return [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'href' => 'url', - ]; - }); - - $navigationEntries = $this->navigationManager->getAll('all'); - - $this->assertEquals(0, $numberOfCalls, 'Expected that the closure is not called by getAll() before setup()'); - $this->assertEmpty($navigationEntries, 'Expected no navigation entry exists before setup()'); - - $this->navigationManager->setup(); - $navigationEntries = $this->navigationManager->getAll('all'); - - $this->assertEquals(1, $numberOfCalls, 'Expected that the closure is called by getAll() once setup() has run'); - $this->assertArrayHasKey('entry id', $navigationEntries); - } - - public function testAddClosureAfterSetup(): void { - $this->navigationManager->setup(); - $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists'); - - $numberOfCalls = 0; - $this->navigationManager->add(function () use (&$numberOfCalls) { - $numberOfCalls++; - - return [ - 'id' => 'late entry', - 'name' => 'link text', - 'order' => 1, - 'href' => 'url', - ]; - }); - - $this->assertEquals(0, $numberOfCalls, 'Expected that the closure is not called by add()'); - - $navigationEntries = $this->navigationManager->getAll('all'); - $this->assertEquals(1, $numberOfCalls, 'Expected that the closure added after setup() is called by getAll()'); - $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists'); - $this->assertArrayHasKey('late entry', $navigationEntries); - - $navigationEntries = $this->navigationManager->getAll('all'); - $this->assertEquals(1, $numberOfCalls, 'Expected that the closure is only called once'); - $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists'); - $this->assertArrayHasKey('late entry', $navigationEntries); - } - - public function testGetAllFiltersActions(): void { - $this->navigationManager->add([ - 'id' => 'files', - 'name' => 'Files', - 'order' => 1, - 'href' => 'url', - ]); - $this->navigationManager->add([ - 'id' => 'logout', - 'name' => 'Log out', - 'order' => 2, - 'href' => 'url', - 'type' => INavigationManager::TYPE_ACTION, - ]); - - $this->assertEquals(['logout'], array_keys($this->navigationManager->getAll(INavigationManager::TYPE_ACTION))); - $this->assertEquals(['files'], array_keys($this->navigationManager->getAll(INavigationManager::TYPE_APPS))); - $this->assertEquals(['files', 'logout'], array_keys($this->navigationManager->getAll(INavigationManager::TYPE_ALL))); - } - - public function testAddArrayClearGetAll(): void { - $entry = [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'icon' => 'optional', - 'href' => 'url' - ]; - - $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists'); - $this->navigationManager->add($entry); - $this->navigationManager->clear(false); - $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()'); - } - - public function testAddClosureClearGetAll(): void { - $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists'); - - $entry = [ - 'id' => 'entry id', - 'name' => 'link text', - 'order' => 1, - 'icon' => 'optional', - 'href' => 'url' - ]; - - global $testAddClosureNumberOfCalls; - $testAddClosureNumberOfCalls = 0; - - $this->navigationManager->add(function () use ($entry) { - global $testAddClosureNumberOfCalls; - $testAddClosureNumberOfCalls++; - - return $entry; - }); - - $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()'); - $this->navigationManager->clear(false); - $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by clear()'); - $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()'); - $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by getAll()'); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesNavigationConfig')] - public function testWithAppManager($expected, $navigation, $isAdmin = false): void { - $l = $this->createMock(IL10N::class); - $l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - - /* Return default value */ - $this->config->method('getUserValue') - ->willReturnArgument(3); - - $this->appManager->expects($this->any()) - ->method('isEnabledForUser') - ->with('theming') - ->willReturn(true); - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('test') - ->willReturn($navigation); - $this->appManager->expects($this->any()) - ->method('isAppLoaded') - ->willReturnMap([ - ['test', true], - ['files', true], - ]); - $this->urlGenerator->expects($this->any()) - ->method('imagePath') - ->willReturnCallback(function ($appName, $file) { - return "/apps/$appName/img/$file"; - }); - $this->appManager->expects($this->any()) - ->method('getAppIcon') - ->willReturnCallback(fn (string $appName) => "/apps/$appName/img/app.svg"); - $this->l10nFac->expects($this->any())->method('get')->willReturn($l); - $this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) { - if ($route === 'core.login.logout') { - return 'https://example.com/logout'; - } - return '/apps/test/'; - }); - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('user001'); - $this->userSession->expects($this->any())->method('getUser')->willReturn($user); - $this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true); - $this->appManager->expects($this->any()) - ->method('getEnabledAppsForUser') - ->with($user) - ->willReturn(['test']); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn($isAdmin); - - $this->navigationManager->clear(); - $this->dispatcher->expects($this->atLeastOnce()) - ->method('dispatchTyped') - ->willReturnCallback(function ($event): void { - $this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event); - }); - $this->navigationManager->setup(); - $entries = $this->navigationManager->getAll('all'); - $this->assertEquals($expected, $entries); - } - - public static function providesNavigationConfig(): array { - return [ - 'minimalistic' => [ - ['test' => [ - 'id' => 'test', - 'order' => 100, - 'href' => '/apps/test/', - 'icon' => '/apps/test/img/app.svg', - 'name' => 'Test', - 'active' => false, - 'type' => 'link', - 'classes' => '', - 'unread' => 0, - 'default' => true, - 'app' => 'test', - ]], - ['navigations' => [ - 'navigation' => [ - ['route' => 'test.page.index', 'name' => 'Test'] - ] - ]] - ], - 'minimalistic-settings' => [ - ['test' => [ - 'id' => 'test', - 'order' => 100, - 'href' => '/apps/test/', - 'icon' => '/apps/test/img/app.svg', - 'name' => 'Test', - 'active' => false, - 'type' => 'settings', - 'classes' => '', - 'unread' => 0, - ]], - ['navigations' => [ - 'navigation' => [ - ['route' => 'test.page.index', 'name' => 'Test', 'type' => 'settings'] - ], - ]] - ], - 'with-multiple' => [ - ['test' => [ - 'id' => 'test', - 'order' => 100, - 'href' => '/apps/test/', - 'icon' => '/apps/test/img/app.svg', - 'name' => 'Test', - 'active' => false, - 'type' => 'link', - 'classes' => '', - 'unread' => 0, - 'default' => false, - 'app' => 'test', - ], - 'test1' => [ - 'id' => 'test1', - 'order' => 50, - 'href' => '/apps/test/', - 'icon' => '/apps/test/img/app.svg', - 'name' => 'Other test', - 'active' => false, - 'type' => 'link', - 'classes' => '', - 'unread' => 0, - 'default' => true, // because of order - 'app' => 'test', - ]], - ['navigations' => [ - 'navigation' => [ - ['route' => 'test.page.index', 'name' => 'Test'], - ['route' => 'test.page.index', 'name' => 'Other test', 'order' => 50], - ] - ]] - ], - 'admin' => [ - ['test' => [ - 'id' => 'test', - 'order' => 100, - 'href' => '/apps/test/', - 'icon' => '/apps/test/img/app.svg', - 'name' => 'Test', - 'active' => false, - 'type' => 'link', - 'classes' => '', - 'unread' => 0, - 'default' => true, - 'app' => 'test', - ]], - ['navigations' => [ - 'navigation' => [ - ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test'] - ], - ]], - true - ], - 'no name' => [ - [], // nothing because the entry is not added because it has no name - ['navigations' => [ - 'navigation' => [ - ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index'] - ], - ]], - true - ], - 'no admin' => [ - [], // nothing because user is not an admin - ['navigations' => [ - 'navigation' => [ - ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test'] - ], - ]], - false, - ] - ]; - } - - public function testWithAppManagerAndApporder(): void { - $l = $this->createMock(IL10N::class); - $l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - - $testOrder = 12; - $expected = [ - 'test' => [ - 'type' => 'link', - 'id' => 'test', - 'order' => $testOrder, - 'href' => '/apps/test/', - 'name' => 'Test', - 'icon' => '/apps/test/img/app.svg', - 'active' => false, - 'classes' => '', - 'unread' => 0, - 'default' => true, - 'app' => 'test', - ], - ]; - $navigation = ['navigations' => [ - 'navigation' => [ - ['route' => 'test.page.index', 'name' => 'Test'] - ], - ]]; - - $this->config->method('getUserValue') - ->willReturnCallback( - function (string $userId, string $appName, string $key, mixed $default = '') use ($testOrder) { - $this->assertEquals('user001', $userId); - if ($key === 'apporder') { - return json_encode(['test' => ['app' => 'test', 'order' => $testOrder]]); - } - return $default; - } - ); - - $this->appManager->expects($this->any()) - ->method('isEnabledForUser') - ->with('theming') - ->willReturn(true); - $this->appManager->expects($this->once()) - ->method('getAppIcon') - ->with('test') - ->willReturn('/apps/test/img/app.svg'); - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('test') - ->willReturn($navigation); - $this->appManager->expects($this->atLeastOnce()) - ->method('isAppLoaded') - ->willReturnMap([ - ['test', true], - ['files', true], - ]); - $this->l10nFac->expects($this->any())->method('get')->willReturn($l); - $this->urlGenerator->expects($this->any())->method('imagePath')->willReturnCallback(function ($appName, $file) { - return "/apps/$appName/img/$file"; - }); - $this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) { - if ($route === 'core.login.logout') { - return 'https://example.com/logout'; - } - return '/apps/test/'; - }); - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('user001'); - $this->userSession->expects($this->any())->method('getUser')->willReturn($user); - $this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true); - $this->appManager->expects($this->any()) - ->method('getEnabledAppsForUser') - ->with($user) - ->willReturn(['test']); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - $this->navigationManager->clear(); - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->willReturnCallback(function ($event): void { - $this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event); - }); - $this->navigationManager->setup(); - $entries = $this->navigationManager->getAll(); - $this->assertEquals($expected, $entries); - } - - /** - * Known apps get a default order, all other apps keep the order from their info.xml. - */ - public function testDefaultAppOrder(): void { - $this->userSession->method('isLoggedIn')->willReturn(false); - $this->appManager->method('getEnabledApps')->willReturn([]); - $this->appManager->method('isEnabledForUser')->willReturn(true); - - // order as shipped by the apps themselves - $apps = ['circles' => 80, 'activity' => 1, 'other' => 2, 'spreed' => -5, 'files' => 0, 'dashboard' => -10]; - foreach ($apps as $id => $order) { - $this->navigationManager->add(['id' => $id, 'name' => $id, 'href' => '/', 'order' => $order]); - } - - $this->assertSame( - ['dashboard', 'files', 'spreed', 'circles', 'activity', 'other'], - array_keys($this->navigationManager->getAll()), - ); - } - - /** - * Users that sorted the apps themselves keep their order, also for apps they never sorted. - */ - public function testDefaultAppOrderIsSkippedForCustomOrder(): void { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user001'); - $this->userSession->method('getUser')->willReturn($user); - $this->userSession->method('isLoggedIn')->willReturn(true); - $this->appManager->method('getEnabledAppsForUser')->willReturn([]); - $this->appManager->method('isEnabledForUser')->willReturn(true); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - $this->config->method('getUserValue') - ->willReturnCallback(static function (string $userId, string $appName, string $key, mixed $default = '') { - return $key === 'apporder' ? json_encode(['other' => ['app' => 'other', 'order' => 0]]) : $default; - }); - - // `circles` is not part of the user order, so it keeps the order from its info.xml - // instead of moving to the front of the user order - foreach (['other' => 2, 'circles' => 80] as $id => $order) { - $this->navigationManager->add(['id' => $id, 'name' => $id, 'href' => '/', 'order' => $order]); - } - - $this->assertSame( - ['other', 'circles'], - array_keys($this->navigationManager->getAll()), - ); - } - - /** - * Navigation entries of enabled apps that are not booted yet must not be resolved. - */ - public function testResolveOnlyLoadedApps(): void { - /* Return default value */ - $this->config->method('getUserValue')->willReturnArgument(3); - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user001'); - $this->userSession->method('getUser')->willReturn($user); - $this->userSession->method('isLoggedIn')->willReturn(true); - $this->appManager->method('getEnabledAppsForUser')->with($user)->willReturn(['test']); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - // The app is enabled but not booted yet ... - $this->appManager->expects($this->atLeastOnce()) - ->method('isAppLoaded') - ->with('test') - ->willReturn(false); - // ... so its info.xml navigation entries must never be read - $this->appManager->expects($this->never())->method('getAppInfo'); - - $this->navigationManager->clear(); - $this->assertEquals([], $this->navigationManager->getAll('all')); - } - - /** - * The LoadAdditionalEntriesEvent is only dispatched by setup(), not by getAll(). - */ - public function testGetAllDoesNotDispatchAdditionalEntries(): void { - $this->userSession->method('isLoggedIn')->willReturn(false); - $this->appManager->method('getEnabledApps')->willReturn([]); - - $this->dispatcher->expects($this->never())->method('dispatchTyped'); - - $this->navigationManager->clear(); - $this->assertEquals([], $this->navigationManager->getAll('all')); - } - - /** - * An app's info.xml must only be resolved once, even across multiple getAll() calls - * and even when the app does not provide any navigation entries. - */ - public function testAppInfoResolvedOnlyOnce(): void { - $this->config->method('getUserValue')->willReturnArgument(3); - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user001'); - $this->userSession->method('getUser')->willReturn($user); - $this->userSession->method('isLoggedIn')->willReturn(true); - $this->appManager->method('getEnabledAppsForUser')->with($user)->willReturn(['test']); - $this->appManager->method('isAppLoaded')->with('test')->willReturn(true); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - // App has no navigation entries; info.xml must only be read once - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('test') - ->willReturn(['navigations' => []]); - - $this->navigationManager->clear(); - $this->assertEquals([], $this->navigationManager->getAll('all')); - $this->assertEquals([], $this->navigationManager->getAll('all')); - } - - /** - * clear(false) keeps the resolved state, so already loaded apps are not resolved again; - * clear(true) resets it, forcing a fresh resolve. - */ - public function testClearResetsResolvedStateOnlyWhenRequested(): void { - $this->config->method('getUserValue')->willReturnArgument(3); - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user001'); - $this->userSession->method('getUser')->willReturn($user); - $this->userSession->method('isLoggedIn')->willReturn(true); - $this->appManager->method('getEnabledAppsForUser')->with($user)->willReturn(['test']); - $this->appManager->method('isAppLoaded')->with('test')->willReturn(true); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - // Resolved once for the initial getAll(), then again after clear(true) resets the state - $this->appManager->expects($this->exactly(2)) - ->method('getAppInfo') - ->with('test') - ->willReturn(['navigations' => []]); - - $this->navigationManager->clear(); - $this->assertEquals([], $this->navigationManager->getAll('all')); - - // Soft clear keeps the resolved state, so getAppInfo is not called again - $this->navigationManager->clear(false); - $this->assertEquals([], $this->navigationManager->getAll('all')); - - // Full clear resets the resolved state, so the app is resolved again - $this->navigationManager->clear(true); - $this->assertEquals([], $this->navigationManager->getAll('all')); - } - - public static function provideDefaultEntries(): array { - return [ - // none specified, default to files - [ - '', - '', - '{}', - true, - 'files', - ], - // none specified, without fallback - [ - '', - '', - '{}', - false, - '', - ], - // unexisting or inaccessible app specified, default to files - [ - 'unexist', - '', - '{}', - true, - 'files', - ], - // unexisting or inaccessible app specified, without fallbacks - [ - 'unexist', - '', - '{}', - false, - '', - ], - // non-standard app - [ - 'settings', - '', - '{}', - true, - 'settings', - ], - // non-standard app, without fallback - [ - 'settings', - '', - '{}', - false, - 'settings', - ], - // non-standard app with fallback - [ - 'unexist,settings', - '', - '{}', - true, - 'settings', - ], - // system default app and user apporder - [ - // system default is settings - 'unexist,settings', - '', - // apporder says default app is files (order is lower) - '{"files_id":{"app":"files","order":1},"settings_id":{"app":"settings","order":2}}', - true, - // system default should override apporder - 'settings' - ], - // user-customized defaultapp - [ - '', - 'files', - '', - true, - 'files', - ], - // user-customized defaultapp with systemwide - [ - 'unexist,settings', - 'files', - '', - true, - 'files', - ], - // user-customized defaultapp with system wide and apporder - [ - 'unexist,settings', - 'files', - '{"settings_id":{"app":"settings","order":1},"files_id":{"app":"files","order":2}}', - true, - 'files', - ], - // user-customized apporder fallback - [ - '', - '', - '{"settings_id":{"app":"settings","order":1},"files":{"app":"files","order":2}}', - true, - 'settings', - ], - // user-customized apporder fallback with missing app key (entries added by closures does not always have an app key set (Nextcloud 27 spreed app for example)) - [ - '', - '', - '{"spreed":{"order":1},"files":{"app":"files","order":2}}', - true, - 'files', - ], - // user-customized apporder, but called without fallback - [ - '', - '', - '{"settings":{"app":"settings","order":1},"files":{"app":"files","order":2}}', - false, - '', - ], - // user-customized apporder with an app that has multiple routes - [ - '', - '', - '{"settings_id":{"app":"settings","order":1},"settings_id_2":{"app":"settings","order":3},"id_files":{"app":"files","order":2}}', - true, - 'settings', - ], - // closure navigation entries are also resolved - [ - 'closure2', - '', - '', - true, - 'closure2', - ], - [ - '', - 'closure2', - '', - true, - 'closure2', - ], - [ - '', - '', - '{"closure2":{"order":1,"app":"closure2","href":"/closure2"}}', - true, - 'closure2', - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultEntries')] - public function testGetDefaultEntryIdForUser(string $defaultApps, string $userDefaultApps, string $userApporder, bool $withFallbacks, string $expectedApp): void { - $this->navigationManager->add([ - 'id' => 'files', - ]); - $this->navigationManager->add([ - 'id' => 'settings', - ]); - $this->navigationManager->add(static function (): array { - return [ - 'id' => 'closure1', - 'href' => '/closure1', - ]; - }); - $this->navigationManager->add(static function (): array { - return [ - 'id' => 'closure2', - 'href' => '/closure2', - ]; - }); - - $this->appManager->method('getEnabledApps')->willReturn(['files']); - $this->appManager->method('getEnabledAppsForUser')->willReturn(['files']); - $this->appManager->expects($this->atLeastOnce()) - ->method('isAppLoaded') - ->willReturnMap([ - ['test', true], - ['files', true], - ]); - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user1'); - - $this->userSession->expects($this->atLeastOnce()) - ->method('getUser') - ->willReturn($user); - - $this->config->expects($this->atLeastOnce()) - ->method('getSystemValueString') - ->with('defaultapp', $this->anything()) - ->willReturn($defaultApps); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->willReturnMap([ - ['user1', 'core', 'defaultapp', '', $userDefaultApps], - ['user1', 'core', 'apporder', '[]', $userApporder], - ]); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - $this->navigationManager->setup(); - $this->assertEquals($expectedApp, $this->navigationManager->getDefaultEntryIdForUser(null, $withFallbacks)); - } - - public function testDefaultEntryUpdated(): void { - $this->appManager->method('getEnabledApps')->willReturn([]); - $this->appManager->method('getEnabledAppsForUser')->willReturn([]); - $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false); - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user1'); - - $this->userSession - ->method('getUser') - ->willReturn($user); - - $this->config - ->method('getSystemValueString') - ->with('defaultapp', $this->anything()) - ->willReturn('app4,app3,app2,app1'); - - $this->config - ->method('getUserValue') - ->willReturnMap([ - ['user1', 'core', 'defaultapp', '', ''], - ['user1', 'core', 'apporder', '[]', ''], - ]); - - $this->navigationManager->add([ - 'id' => 'app1', - ]); - - $this->assertEquals('app1', $this->navigationManager->getDefaultEntryIdForUser(null, false)); - $this->assertEquals(true, $this->navigationManager->get('app1')['default']); - - $this->navigationManager->add([ - 'id' => 'app3', - ]); - - $this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false)); - $this->assertEquals(false, $this->navigationManager->get('app1')['default']); - $this->assertEquals(true, $this->navigationManager->get('app3')['default']); - - $this->navigationManager->add([ - 'id' => 'app2', - ]); - - $this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false)); - $this->assertEquals(false, $this->navigationManager->get('app1')['default']); - $this->assertEquals(false, $this->navigationManager->get('app2')['default']); - $this->assertEquals(true, $this->navigationManager->get('app3')['default']); - - $this->navigationManager->add([ - 'id' => 'app4', - ]); - - $this->assertEquals('app4', $this->navigationManager->getDefaultEntryIdForUser(null, false)); - $this->assertEquals(false, $this->navigationManager->get('app1')['default']); - $this->assertEquals(false, $this->navigationManager->get('app2')['default']); - $this->assertEquals(false, $this->navigationManager->get('app3')['default']); - $this->assertEquals(true, $this->navigationManager->get('app4')['default']); - } -} --- a/tests/lib/Net/HostnameClassifierTest.php +++ /dev/null @@ -1,61 +0,0 @@ -classifier = new HostnameClassifier(); - } - - public static function localHostnamesData(): array { - return [ - ['localhost'], - ['localHost'], - ['random-host'], - ['another-host.local'], - ['service.localhost'], - ['randomdomain.internal'], - ['another-host.local.'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localHostnamesData')] - public function testLocalHostname(string $host): void { - $isLocal = $this->classifier->isLocalHostname($host); - - self::assertTrue($isLocal); - } - - public static function publicHostnamesData(): array { - return [ - ['example.com'], - ['example.net'], - ['example.org'], - ['host.domain'], - ['cloud.domain.tld'], - ['cloud.domain.tld.'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('publicHostnamesData')] - public function testPublicHostname(string $host): void { - $isLocal = $this->classifier->isLocalHostname($host); - - self::assertFalse($isLocal); - } -} --- a/tests/lib/Net/IpAddressClassifierTest.php +++ /dev/null @@ -1,94 +0,0 @@ -classifier = new IpAddressClassifier(); - } - - public static function publicIpAddressData(): array { - return [ - ['8.8.8.8'], - ['8.8.4.4'], - ['2001:4860:4860::8888'], - ['2001:4860:4860::8844'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('publicIpAddressData')] - public function testPublicAddress(string $ip): void { - $isLocal = $this->classifier->isLocalAddress($ip); - - self::assertFalse($isLocal); - } - - public static function localIpAddressData(): array { - return [ - ['127.0.0.1'], - ['127.0.0.13'], // all 127.0.0.0/8 network is loopback address - ['192.168.0.1'], - ['fe80::200:5aee:feaa:20a2'], - ['fe80::1fc4:15d8:78db:2319%enp4s0'], // v6 zone ID - ['0:0:0:0:0:ffff:10.0.0.1'], - ['0:0:0:0:0:ffff:127.0.0.0'], - ['10.0.0.1'], - ['::'], - ['::1'], - ['100.100.100.200'], - ['192.0.0.1'], - ['64:ff9b::a9fe:a9fe'], // NAT64 of 169.254.169.254 - ['64:ff9b:1::a9fe:a9fe'], // rfc8215 - ['::ffff:127.0.0.1'], - ['2130706433'], - ['0177.0.0.1'], - ['169.254.169.254'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localIpAddressData')] - public function testLocalAddress(string $ip): void { - $isLocal = $this->classifier->isLocalAddress($ip); - - self::assertTrue($isLocal); - } - - public static function mappedAddresses(): array { - return [ - ['64:ff9b::a9fe:a9fe', '169.254.169.254'], - ['::ffff:7f00:1', '127.0.0.1'], - ['::127.0.0.1', '127.0.0.1'], - ['::7f00:1', '127.0.0.1'], - ['2001:0000:4136:e378:8000:63bf:3fff:fdd2', '192.0.2.45'], - ['2001:4860:4860::8888', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('mappedAddresses')] - public function testMappedAddresses(string $ipv6, ?string $ipv4): void { - $mapped = $this->classifier->getMappedIpv4(IPv6::parseString($ipv6)); - - if ($ipv4 === null) { - self::assertEquals(null, $mapped); - } else { - self::assertEquals(IPv4::parseString($ipv4), $mapped); - } - } -} --- a/tests/lib/Notification/ActionTest.php +++ /dev/null @@ -1,196 +0,0 @@ -action = new Action(); - } - - public static function dataSetLabel(): array { - return [ - ['test1'], - [str_repeat('a', 1)], - [str_repeat('a', 32)], - ]; - } - - /** - * @param string $label - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLabel')] - public function testSetLabel($label): void { - $this->assertSame('', $this->action->getLabel()); - $this->assertSame($this->action, $this->action->setLabel($label)); - $this->assertSame($label, $this->action->getLabel()); - } - - public static function dataSetLabelInvalid(): array { - return [ - [''], - [str_repeat('a', 33)], - ]; - } - - /** - * @param mixed $label - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLabelInvalid')] - public function testSetLabelInvalid($label): void { - $this->expectException(\InvalidArgumentException::class); - - $this->action->setLabel($label); - } - - public static function dataSetParsedLabel(): array { - return [ - ['test1'], - [str_repeat('a', 1)], - [str_repeat('a', 32)], - ]; - } - - /** - * @param string $label - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedLabel')] - public function testSetParsedLabel($label): void { - $this->assertSame('', $this->action->getParsedLabel()); - $this->assertSame($this->action, $this->action->setParsedLabel($label)); - $this->assertSame($label, $this->action->getParsedLabel()); - } - - public static function dataSetParsedLabelInvalid(): array { - return [ - [''], - ]; - } - - /** - * @param mixed $label - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedLabelInvalid')] - public function testSetParsedLabelInvalid($label): void { - $this->expectException(\InvalidArgumentException::class); - - $this->action->setParsedLabel($label); - } - - public static function dataSetLink(): array { - return [ - ['http://example.tld/', 'GET'], - ['https://example.tld/api/v1/resource', 'POST'], - ['https://example.tld/?q=1&r=2', 'PUT'], - ['https://example.tld/path#frag', 'DELETE'], - ['https://example.tld/web', 'WEB'], - // Maximum length (256 chars total, including the scheme) - ['https://' . str_repeat('a', 256 - 8), 'GET'], - ]; - } - - /** - * @param string $link - * @param string $type - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLink')] - public function testSetLink($link, $type): void { - $this->assertSame('', $this->action->getLink()); - $this->assertSame($this->action, $this->action->setLink($link, $type)); - $this->assertSame($link, $this->action->getLink()); - $this->assertSame($type, $this->action->getRequestType()); - } - - public static function dataSetLinkInvalid(): array { - return [ - // Invalid link — empty / too long - ['', 'GET'], - ['https://' . str_repeat('a', 257 - 8), 'GET'], - - // Disallowed schemes - ['javascript:alert(1)', 'WEB'], - ['JavaScript:alert(1)', 'WEB'], - ['javascript:alert(1)', 'GET'], - ['data:text/html,', 'WEB'], - ['vbscript:msgbox("xss")', 'WEB'], - ['file:///etc/passwd', 'GET'], - ['mailto:test@example.tld', 'WEB'], - ['ftp://example.tld/', 'GET'], - - // Relative urls - ['/relative/path', 'WEB'], - ['//protocol-relative.tld/', 'GET'], - ['url', 'GET'], - - // Invalid type (with a valid http(s) link, so the type check is what trips) - ['https://example.tld/', 'notGET'], - ]; - } - - /** - * @param mixed $link - * @param mixed $type - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLinkInvalid')] - public function testSetLinkInvalid($link, $type): void { - $this->expectException(\InvalidArgumentException::class); - - $this->action->setLink($link, $type); - } - - public static function dataSetPrimary(): array { - return [ - [true], - [false], - ]; - } - - /** - * @param bool $primary - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetPrimary')] - public function testSetPrimary($primary): void { - $this->assertSame(false, $this->action->isPrimary()); - $this->assertSame($this->action, $this->action->setPrimary($primary)); - $this->assertSame($primary, $this->action->isPrimary()); - } - - public function testIsValid(): void { - $this->assertFalse($this->action->isValid()); - $this->assertFalse($this->action->isValidParsed()); - $this->action->setLabel('label'); - $this->assertFalse($this->action->isValid()); - $this->assertFalse($this->action->isValidParsed()); - $this->action->setLink('https://example.tld/', 'GET'); - $this->assertTrue($this->action->isValid()); - $this->assertFalse($this->action->isValidParsed()); - } - - public function testIsValidParsed(): void { - $this->assertFalse($this->action->isValid()); - $this->assertFalse($this->action->isValidParsed()); - $this->action->setParsedLabel('label'); - $this->assertFalse($this->action->isValid()); - $this->assertFalse($this->action->isValidParsed()); - $this->action->setLink('https://example.tld/', 'GET'); - $this->assertFalse($this->action->isValid()); - $this->assertTrue($this->action->isValidParsed()); - } -} --- a/tests/lib/Notification/DummyApp.php +++ /dev/null @@ -1,43 +0,0 @@ -validator = $this->createMock(IValidator::class); - $this->richTextFormatter = $this->createMock(IRichTextFormatter::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->cache = $this->createMock(ICache::class); - $this->subscriptionRegistry = $this->createMock(IRegistry::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cacheFactory->method('createDistributed') - ->with('notifications') - ->willReturn($this->cache); - - $this->registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator = $this->createMock(Coordinator::class); - $this->coordinator->method('getRegistrationContext') - ->willReturn($this->registrationContext); - - $this->manager = new Manager( - $this->validator, - $this->userManager, - $this->cacheFactory, - $this->subscriptionRegistry, - $this->logger, - $this->coordinator, - $this->richTextFormatter, - ); - } - - public function testRegisterApp(): void { - $this->assertEquals([], self::invokePrivate($this->manager, 'getApps')); - - $this->manager->registerApp(DummyApp::class); - - $this->assertCount(1, self::invokePrivate($this->manager, 'getApps')); - $this->assertCount(1, self::invokePrivate($this->manager, 'getApps')); - - $this->manager->registerApp(DummyApp::class); - - $this->assertCount(2, self::invokePrivate($this->manager, 'getApps')); - } - - public function testRegisterAppInvalid(): void { - $this->manager->registerApp(DummyNotifier::class); - - $this->logger->expects($this->once()) - ->method('error'); - self::invokePrivate($this->manager, 'getApps'); - } - - public function testRegisterNotifier(): void { - $this->assertEquals([], self::invokePrivate($this->manager, 'getNotifiers')); - - $this->manager->registerNotifierService(DummyNotifier::class); - - $this->assertCount(1, self::invokePrivate($this->manager, 'getNotifiers')); - $this->assertCount(1, self::invokePrivate($this->manager, 'getNotifiers')); - - $this->manager->registerNotifierService(DummyNotifier::class); - - $this->assertCount(2, self::invokePrivate($this->manager, 'getNotifiers')); - } - - public function testRegisterNotifierBootstrap(): void { - $this->registrationContext->method('getNotifierServices') - ->willReturn([ - new ServiceRegistration('app', DummyNotifier::class), - ]); - - $this->assertCount(1, self::invokePrivate($this->manager, 'getNotifiers')); - $this->assertCount(1, self::invokePrivate($this->manager, 'getNotifiers')); - } - - public function testRegisterNotifierInvalid(): void { - $this->manager->registerNotifierService(DummyApp::class); - - $this->logger->expects($this->once()) - ->method('error'); - self::invokePrivate($this->manager, 'getNotifiers'); - } - - public function testCreateNotification(): void { - $action = $this->manager->createNotification(); - $this->assertInstanceOf(INotification::class, $action); - } - - public function testNotify(): void { - /** @var INotification|MockObject $notification */ - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - $notification->expects($this->once()) - ->method('isValid') - ->willReturn(true); - - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->validator, - $this->userManager, - $this->cacheFactory, - $this->subscriptionRegistry, - $this->logger, - $this->coordinator, - $this->richTextFormatter, - ]) - ->onlyMethods(['getApps']) - ->getMock(); - - $manager->expects($this->once()) - ->method('getApps') - ->willReturn([]); - - $manager->notify($notification); - } - - public function testNotifyInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - - /** @var INotification|MockObject $notification */ - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - $notification->expects($this->once()) - ->method('isValid') - ->willReturn(false); - - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->validator, - $this->userManager, - $this->cacheFactory, - $this->subscriptionRegistry, - $this->logger, - $this->coordinator, - $this->richTextFormatter, - ]) - ->onlyMethods(['getApps']) - ->getMock(); - - $manager->expects($this->never()) - ->method('getApps'); - - $manager->notify($notification); - } - - public function testMarkProcessed(): void { - /** @var INotification|MockObject $notification */ - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->validator, - $this->userManager, - $this->cacheFactory, - $this->subscriptionRegistry, - $this->logger, - $this->coordinator, - $this->richTextFormatter, - ]) - ->onlyMethods(['getApps']) - ->getMock(); - - $manager->expects($this->once()) - ->method('getApps') - ->willReturn([]); - - $manager->markProcessed($notification); - } - - public function testGetCount(): void { - /** @var INotification|MockObject $notification */ - $notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); - - $manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->validator, - $this->userManager, - $this->cacheFactory, - $this->subscriptionRegistry, - $this->logger, - $this->coordinator, - $this->richTextFormatter, - ]) - ->onlyMethods(['getApps']) - ->getMock(); - - $manager->expects($this->once()) - ->method('getApps') - ->willReturn([]); - - $manager->getCount($notification); - } - - public static function dataIsFairUseOfFreePushService(): array { - return [ - [true, 999, true], - [true, 1000, true], - [false, 999, true], - [false, 1000, false], - ]; - } - - /** - * @param bool $hasValidSubscription - * @param int $userCount - * @param bool $isFair - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsFairUseOfFreePushService')] - public function testIsFairUseOfFreePushService(bool $hasValidSubscription, int $userCount, bool $isFair): void { - $this->subscriptionRegistry->method('delegateHasValidSubscription') - ->willReturn($hasValidSubscription); - - $this->userManager->method('countSeenUsers') - ->willReturn($userCount); - - $this->assertSame($isFair, $this->manager->isFairUseOfFreePushService()); - } -} --- a/tests/lib/Notification/NotificationTest.php +++ /dev/null @@ -1,652 +0,0 @@ -validator = $this->createMock(IValidator::class); - $this->richTextFormatter = $this->createMock(IRichTextFormatter::class); - $this->notification = new Notification($this->validator, $this->richTextFormatter); - } - - protected static function dataValidString($maxLength): array { - $dataSets = [ - ['test1'], - ['1564'], - [str_repeat('a', 1)], - ]; - if ($maxLength !== false) { - $dataSets[] = [str_repeat('a', $maxLength)]; - } - return $dataSets; - } - - protected static function dataInvalidString($maxLength): array { - $dataSets = [ - [''] - ]; - if ($maxLength !== false) { - $dataSets[] = [str_repeat('a', $maxLength + 1)]; - } - return $dataSets; - } - - public static function dataSetApp(): array { - return self::dataValidString(32); - } - - /** - * @param string $app - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetApp')] - public function testSetApp($app): void { - $this->assertSame('', $this->notification->getApp()); - $this->assertSame($this->notification, $this->notification->setApp($app)); - $this->assertSame($app, $this->notification->getApp()); - } - - public static function dataSetAppInvalid(): array { - return self::dataInvalidString(32); - } - - /** - * @param mixed $app - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetAppInvalid')] - public function testSetAppInvalid($app): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setApp($app); - } - - public static function dataSetUser(): array { - return self::dataValidString(64); - } - - /** - * @param string $user - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetUser')] - public function testSetUser($user): void { - $this->assertSame('', $this->notification->getUser()); - $this->assertSame($this->notification, $this->notification->setUser($user)); - $this->assertSame($user, $this->notification->getUser()); - } - - public static function dataSetUserInvalid(): array { - return self::dataInvalidString(64); - } - - /** - * @param mixed $user - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetUserInvalid')] - public function testSetUserInvalid($user): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setUser($user); - } - - public static function dataSetDateTime(): array { - $past = new \DateTime(); - $past->sub(new \DateInterval('P1Y')); - $current = new \DateTime(); - $future = new \DateTime(); - $future->add(new \DateInterval('P1Y')); - - return [ - [$past], - [$current], - [$future], - ]; - } - - /** - * @param \DateTime $dateTime - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDateTime')] - public function testSetDateTime(\DateTime $dateTime): void { - $this->assertSame(0, $this->notification->getDateTime()->getTimestamp()); - $this->assertSame($this->notification, $this->notification->setDateTime($dateTime)); - $this->assertSame($dateTime, $this->notification->getDateTime()); - } - - public static function dataSetDateTimeZero(): array { - $nineTeenSeventy = new \DateTime(); - $nineTeenSeventy->setTimestamp(0); - return [ - [$nineTeenSeventy], - ]; - } - - /** - * @param \DateTime $dateTime - * @expectedMessage 'The given date time is invalid' - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDateTimeZero')] - public function testSetDateTimeZero($dateTime): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setDateTime($dateTime); - } - - public static function dataSetObject(): array { - return [ - ['a', '21'], - [str_repeat('a', 64), '42'], - ]; - } - - /** - * @param string $type - * @param string $id - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetObject')] - public function testSetObject($type, $id): void { - $this->assertSame('', $this->notification->getObjectType()); - $this->assertSame('', $this->notification->getObjectId()); - $this->assertSame($this->notification, $this->notification->setObject($type, $id)); - $this->assertSame($type, $this->notification->getObjectType()); - $this->assertSame($id, $this->notification->getObjectId()); - } - - public static function dataSetObjectTypeInvalid(): array { - return self::dataInvalidString(64); - } - - public static function dataSetObjectIdInvalid(): array { - return [ - [''], - [str_repeat('a', 64 + 1)], - ]; - } - - /** - * @param mixed $id - * @expectedMessage 'The given object id is invalid' - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetObjectIdInvalid')] - public function testSetObjectIdInvalid($id): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setObject('object', $id); - } - - public static function dataSetSubject(): array { - return [ - ['a', []], - [str_repeat('a', 64), [str_repeat('a', 160)]], - [str_repeat('a', 64), array_fill(0, 160, 'a')], - ]; - } - - /** - * @param string $subject - * @param array $parameters - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetSubject')] - public function testSetSubject($subject, $parameters): void { - $this->assertSame('', $this->notification->getSubject()); - $this->assertSame([], $this->notification->getSubjectParameters()); - $this->assertSame($this->notification, $this->notification->setSubject($subject, $parameters)); - $this->assertSame($subject, $this->notification->getSubject()); - $this->assertSame($parameters, $this->notification->getSubjectParameters()); - } - - public static function dataSetSubjectInvalidSubject(): array { - return self::dataInvalidString(64); - } - - /** - * @param mixed $subject - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetSubjectInvalidSubject')] - public function testSetSubjectInvalidSubject($subject): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setSubject($subject, []); - } - - public static function dataSetParsedSubject(): array { - return self::dataValidString(false); - } - - /** - * @param string $subject - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedSubject')] - public function testSetParsedSubject($subject): void { - $this->assertSame('', $this->notification->getParsedSubject()); - $this->assertSame($this->notification, $this->notification->setParsedSubject($subject)); - $this->assertSame($subject, $this->notification->getParsedSubject()); - } - - public static function dataSetParsedSubjectInvalid(): array { - return self::dataInvalidString(false); - } - - /** - * @param mixed $subject - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedSubjectInvalid')] - public function testSetParsedSubjectInvalid($subject): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setParsedSubject($subject); - } - - public static function dataSetMessage(): array { - return [ - ['a', []], - [str_repeat('a', 64), [str_repeat('a', 160)]], - [str_repeat('a', 64), array_fill(0, 160, 'a')], - ]; - } - - /** - * @param string $message - * @param array $parameters - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetMessage')] - public function testSetMessage($message, $parameters): void { - $this->assertSame('', $this->notification->getMessage()); - $this->assertSame([], $this->notification->getMessageParameters()); - $this->assertSame($this->notification, $this->notification->setMessage($message, $parameters)); - $this->assertSame($message, $this->notification->getMessage()); - $this->assertSame($parameters, $this->notification->getMessageParameters()); - } - - public static function dataSetMessageInvalidMessage(): array { - return self::dataInvalidString(64); - } - - /** - * @param mixed $message - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetMessageInvalidMessage')] - public function testSetMessageInvalidMessage($message): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setMessage($message, []); - } - - public static function dataSetParsedMessage(): array { - return self::dataValidString(false); - } - - /** - * @param string $message - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedMessage')] - public function testSetParsedMessage($message): void { - $this->assertSame('', $this->notification->getParsedMessage()); - $this->assertSame($this->notification, $this->notification->setParsedMessage($message)); - $this->assertSame($message, $this->notification->getParsedMessage()); - } - - public static function dataSetParsedMessageInvalid(): array { - return self::dataInvalidString(false); - } - - /** - * @param mixed $message - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetParsedMessageInvalid')] - public function testSetParsedMessageInvalid($message): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setParsedMessage($message); - } - - public static function dataSetLink(): array { - return [ - ['http://example.tld/'], - ['https://example.tld/'], - ['https://example.tld/path/to/resource?query=1&other=2#fragment'], - // Maximum length (4000 chars total, including the scheme) - ['https://' . str_repeat('a', 4000 - 8)], - ]; - } - - /** - * @param string $link - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLink')] - public function testSetLink($link): void { - $this->assertSame('', $this->notification->getLink()); - $this->assertSame($this->notification, $this->notification->setLink($link)); - $this->assertSame($link, $this->notification->getLink()); - } - - public static function dataSetLinkInvalid(): array { - return [ - // Empty / too long - [''], - ['https://' . str_repeat('a', 4001 - 8)], - - // Disallowed schemes - ['javascript:alert(1)'], - ['JavaScript:alert(1)'], - ['data:text/html,'], - ['vbscript:msgbox("xss")'], - ['file:///etc/passwd'], - ['mailto:test@example.tld'], - ['ftp://example.tld/'], - ['ws://example.tld/'], - - // Relative urls - ['/relative/path'], - ['//protocol-relative.tld/'], - ['example.tld/path'], - ['test1'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLinkInvalid')] - public function testSetLinkInvalid(string $link): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setLink($link); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLink')] - public function testSetIcon(string $icon): void { - $this->assertSame('', $this->notification->getIcon()); - $this->assertSame($this->notification, $this->notification->setIcon($icon)); - $this->assertSame($icon, $this->notification->getIcon()); - } - - public static function dataSetIconInvalid(): array { - return self::dataInvalidString(4000); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLinkInvalid')] - public function testSetIconInvalid(string $icon): void { - $this->expectException(\InvalidArgumentException::class); - - $this->notification->setIcon($icon); - } - - public function testCreateAction(): void { - $action = $this->notification->createAction(); - $this->assertInstanceOf(IAction::class, $action); - } - - public function testAddAction(): void { - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->once()) - ->method('isValid') - ->willReturn(true); - $action->expects($this->never()) - ->method('isValidParsed'); - - $this->assertSame($this->notification, $this->notification->addAction($action)); - - $this->assertEquals([$action], $this->notification->getActions()); - $this->assertEquals([], $this->notification->getParsedActions()); - } - - public function testAddActionInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->once()) - ->method('isValid') - ->willReturn(false); - $action->expects($this->never()) - ->method('isValidParsed'); - - $this->notification->addAction($action); - } - - public function testAddActionSecondPrimary(): void { - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->exactly(2)) - ->method('isValid') - ->willReturn(true); - $action->expects($this->exactly(2)) - ->method('isPrimary') - ->willReturn(true); - - $this->assertSame($this->notification, $this->notification->addAction($action)); - - $this->expectException(\InvalidArgumentException::class); - $this->notification->addAction($action); - } - - public function testAddParsedAction(): void { - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->once()) - ->method('isValidParsed') - ->willReturn(true); - $action->expects($this->never()) - ->method('isValid'); - - $this->assertSame($this->notification, $this->notification->addParsedAction($action)); - - $this->assertEquals([$action], $this->notification->getParsedActions()); - $this->assertEquals([], $this->notification->getActions()); - } - - public function testAddParsedActionInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->once()) - ->method('isValidParsed') - ->willReturn(false); - $action->expects($this->never()) - ->method('isValid'); - - $this->notification->addParsedAction($action); - } - - public function testAddActionSecondParsedPrimary(): void { - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action = $this->createMock(IAction::class); - $action->expects($this->exactly(2)) - ->method('isValidParsed') - ->willReturn(true); - $action->expects($this->exactly(2)) - ->method('isPrimary') - ->willReturn(true); - - $this->assertSame($this->notification, $this->notification->addParsedAction($action)); - - $this->expectException(\InvalidArgumentException::class); - $this->notification->addParsedAction($action); - } - - public function testAddActionParsedPrimaryEnd(): void { - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action1 = $this->createMock(IAction::class); - $action1->expects($this->exactly(2)) - ->method('isValidParsed') - ->willReturn(true); - $action1->expects($this->exactly(2)) - ->method('isPrimary') - ->willReturn(false); - /** @var IAction|\PHPUnit\Framework\MockObject\MockObject $action */ - $action2 = $this->createMock(IAction::class); - $action2->expects($this->once()) - ->method('isValidParsed') - ->willReturn(true); - $action2->expects($this->once()) - ->method('isPrimary') - ->willReturn(true); - - $this->assertSame($this->notification, $this->notification->addParsedAction($action1)); - $this->assertSame($this->notification, $this->notification->addParsedAction($action2)); - $this->assertSame($this->notification, $this->notification->addParsedAction($action1)); - - $this->assertEquals([$action2, $action1, $action1], $this->notification->getParsedActions()); - } - - public static function dataIsValid(): array { - return [ - [false, '', false], - [true, '', false], - [false, 'a', false], - [true, 'a', true], - ]; - } - - /** - * - * @param bool $isValidCommon - * @param string $subject - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsValid')] - public function testIsValid($isValidCommon, $subject, $expected): void { - /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ - $notification = $this->getMockBuilder(Notification::class) - ->onlyMethods([ - 'isValidCommon', - 'getSubject', - 'getParsedSubject', - ]) - ->setConstructorArgs([$this->validator, $this->richTextFormatter]) - ->getMock(); - - $notification->expects($this->once()) - ->method('isValidCommon') - ->willReturn($isValidCommon); - - $notification->expects(!$isValidCommon ? $this->never() : $this->once()) - ->method('getSubject') - ->willReturn($subject); - - $notification->expects($this->never()) - ->method('getParsedSubject') - ->willReturn($subject); - - $this->assertEquals($expected, $notification->isValid()); - } - - /** - * - * @param bool $isValidCommon - * @param string $subject - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsValid')] - public function testIsParsedValid($isValidCommon, $subject, $expected): void { - /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ - $notification = $this->getMockBuilder(Notification::class) - ->onlyMethods([ - 'isValidCommon', - 'getParsedSubject', - 'getSubject', - ]) - ->setConstructorArgs([$this->validator, $this->richTextFormatter]) - ->getMock(); - - $notification->expects($this->once()) - ->method('isValidCommon') - ->willReturn($isValidCommon); - - $notification->expects(!$isValidCommon ? $this->never() : $this->once()) - ->method('getParsedSubject') - ->willReturn($subject); - - $notification->expects($this->never()) - ->method('getSubject') - ->willReturn($subject); - - $this->assertEquals($expected, $notification->isValidParsed()); - } - - public static function dataIsValidCommon(): array { - return [ - ['', '', 0, '', '', false], - ['app', '', 0, '', '', false], - ['app', 'user', 0, '', '', false], - ['app', 'user', time(), '', '', false], - ['app', 'user', time(), 'type', '', false], - ['app', 'user', time(), 'type', '42', true], - ]; - } - - /** - * - * @param string $app - * @param string $user - * @param int $timestamp - * @param string $objectType - * @param string $objectId - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsValidCommon')] - public function testIsValidCommon($app, $user, $timestamp, $objectType, $objectId, $expected): void { - /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ - $notification = $this->getMockBuilder(Notification::class) - ->onlyMethods([ - 'getApp', - 'getUser', - 'getDateTime', - 'getObjectType', - 'getObjectId', - ]) - ->setConstructorArgs([$this->validator, $this->richTextFormatter]) - ->getMock(); - - $notification->expects($this->any()) - ->method('getApp') - ->willReturn($app); - - $notification->expects($this->any()) - ->method('getUser') - ->willReturn($user); - - $dateTime = new \DateTime(); - $dateTime->setTimestamp($timestamp); - - $notification->expects($this->any()) - ->method('getDateTime') - ->willReturn($dateTime); - - $notification->expects($this->any()) - ->method('getObjectType') - ->willReturn($objectType); - - $notification->expects($this->any()) - ->method('getObjectId') - ->willReturn($objectId); - - $this->assertEquals($expected, $this->invokePrivate($notification, 'isValidCommon')); - } -} --- a/tests/lib/OCM/DiscoveryServiceTest.php +++ /dev/null @@ -1,157 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->context = Server::get(RegistrationContext::class); - $this->dispatcher = Server::get(IEventDispatcher::class); - $this->discoveryService = Server::get(OCMDiscoveryService::class); - $this->config = Server::get(IConfig::class); - - // reset $localProvider value between tests - $reflection = new ReflectionClass($this->discoveryService); - $localProvider = $reflection->getProperty('localProvider'); - $localProvider->setValue($this->discoveryService, null); - - $this->requestController = Server::get(OCMRequestController::class); - } - - public static function dataTestOCMRequest(): array { - return [ - ['/inexistant-path/', 404, null], - ['/ocm-capability-test/', 404, null], - ['/ocm-capability-test/get', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get', - 'args' => ['get'], - 'totalArgs' => 1, - 'typedArgs' => ['get'], - ] - ], - ['/ocm-capability-test/get/10/', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get/10', - 'args' => ['get', '10'], - 'totalArgs' => 2, - 'typedArgs' => ['get', '10'], - ] - ], - ['/ocm-capability-test/get/random/10/', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get/random/10', - 'args' => ['get', 'random', '10'], - 'totalArgs' => 3, - 'typedArgs' => ['get', 'random', 10], - ] - ], - ['/ocm-capability-test/get/random/10/1', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get/random/10/1', - 'args' => ['get', 'random', '10', '1'], - 'totalArgs' => 4, - 'typedArgs' => ['get', 'random', 10, true], - ] - ], - ['/ocm-capability-test/get/random/10/true', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get/random/10/true', - 'args' => ['get', 'random', '10', 'true'], - 'totalArgs' => 4, - 'typedArgs' => ['get', 'random', 10, true], - ] - ], - ['/ocm-capability-test/get/random/10/true/42', 200, - [ - 'capability' => 'ocm-capability-test', - 'path' => '/get/random/10/true/42', - 'args' => ['get', 'random', '10', 'true', '42'], - 'totalArgs' => 5, - 'typedArgs' => ['get', 'random', 10, true, 42], - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestOCMRequest')] - public function testOCMRequest(string $path, int $expectedStatus, ?array $expectedResult): void { - $this->context->for('ocm-request-app')->registerEventListener(OCMEndpointRequestEvent::class, OCMEndpointRequestTestEvent::class); - $this->context->delegateEventListenerRegistrations($this->dispatcher); - - $response = $this->requestController->manageOCMRequests($path); - $this->assertSame($expectedStatus, $response->getStatus()); - if ($expectedResult !== null) { - $this->assertSame($expectedResult, $response->getData()); - } - } - - public function testLocalBaseCapability(): void { - $local = $this->discoveryService->getLocalOCMProvider(); - $this->assertEmpty(array_diff(['notifications', 'shares'], $local->getCapabilities()->toArray())); - } - - public function testLocalCapabilitiesAdvertiseHttpSigByDefault(): void { - // `http-sig` is the OCM-spec flag signalling RFC 9421 support backed - // by the JWK Set published at the URL in `jwksUri`. Advertised - // whenever signing is not disabled outright. - $local = $this->discoveryService->getLocalOCMProvider(); - $this->assertTrue($local->hasCapability('http-sig')); - } - - public function testLocalDiscoveryAdvertisesJwksUri(): void { - // scheme follows the instance base URL - $local = $this->discoveryService->getLocalOCMProvider(); - $jwksUri = $local->getJwksUri(); - $baseUrl = Server::get(IURLGenerator::class)->getBaseUrl(); - $expectedScheme = str_starts_with($baseUrl, 'http://') ? 'http://' : 'https://'; - $this->assertStringStartsWith($expectedScheme, $jwksUri); - $this->assertStringContainsString('cloud_federation_api/api/v1/jwks', $jwksUri); - } - - public function testLocalAddedCapability(): void { - $this->context->for('ocm-capability-app')->registerEventListener(LocalOCMDiscoveryEvent::class, LocalOCMDiscoveryTestEvent::class); - $this->context->delegateEventListenerRegistrations($this->dispatcher); - $local = $this->discoveryService->getLocalOCMProvider(); - $this->assertEmpty(array_diff(['notifications', 'shares', 'ocm-capability-test'], $local->getCapabilities()->toArray())); - } - -} --- a/tests/lib/OCM/Listeners/LocalOCMDiscoveryTestEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - */ -class LocalOCMDiscoveryTestEvent implements IEventListener { - public function __construct( - ) { - } - - #[\Override] - public function handle(Event $event): void { - if (!($event instanceof LocalOCMDiscoveryEvent)) { - return; - } - - $event->addCapability('ocm-capability-test'); - } -} --- a/tests/lib/OCM/Listeners/OCMEndpointRequestTestEvent.php +++ /dev/null @@ -1,52 +0,0 @@ - */ -class OCMEndpointRequestTestEvent implements IEventListener { - public function __construct( - ) { - } - - #[\Override] - public function handle(Event $event): void { - if (!($event instanceof OCMEndpointRequestEvent)) { - return; - } - - if ($event->getPath() === '/') { - $event->setResponse(new Response(404)); - return; - } - - $event->setResponse(new DataResponse( - [ - 'capability' => $event->getRequestedCapability(), - 'path' => $event->getPath(), - 'args' => $event->getArgs(), - 'totalArgs' => $event->getArgsCount(), - 'typedArgs' => $event->getArgs( - ParamType::STRING, - ParamType::STRING, - ParamType::INT, - ParamType::BOOL, - ParamType::INT - ) - ] - )); - } -} --- a/tests/lib/OCM/OCMProviderTest.php +++ /dev/null @@ -1,98 +0,0 @@ -provider = new OCMProvider(); - } - - private function resource(string $name, array $shareTypes, array $protocols): IOCMResource { - $resource = $this->provider->createNewResourceType(); - $resource->setName($name) - ->setShareTypes($shareTypes) - ->setProtocols($protocols); - return $resource; - } - - public function testAddResourceTypeKeepsDistinctNames(): void { - $this->provider->addResourceType($this->resource('file', ['user'], ['webdav' => '/dav/'])); - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp' => []])); - - $this->assertCount(2, $this->provider->getResourceTypes()); - } - - public function testAddResourceTypeMergesSameName(): void { - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp' => []])); - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp-receive' => ['targets' => ['blank', 'iframe']]])); - - $resourceTypes = $this->provider->getResourceTypes(); - $this->assertCount(1, $resourceTypes); - $this->assertSame( - ['webapp' => [], 'webapp-receive' => ['targets' => ['blank', 'iframe']]], - $resourceTypes[0]->getProtocols(), - ); - } - - public function testAddResourceTypeDedupesShareTypes(): void { - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp' => []])); - $this->provider->addResourceType($this->resource('folder', ['user', 'group'], ['webapp-receive' => []])); - - $shareTypes = $this->provider->getResourceTypes()[0]->getShareTypes(); - $this->assertSame(['user', 'group'], $shareTypes); - // Deduplication must not leave key gaps, or shareTypes would - // serialize as a JSON object instead of an array. - $this->assertSame('["user","group"]', json_encode($shareTypes)); - } - - public function testAddResourceTypeMergeOverwritesSameProtocol(): void { - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp' => ['a' => 1]])); - $this->provider->addResourceType($this->resource('folder', ['user'], ['webapp' => ['b' => 2]])); - - $this->assertSame( - ['webapp' => ['b' => 2]], - $this->provider->getResourceTypes()[0]->getProtocols(), - ); - } - - public function testJwksUriImportedFromDiscoveryData(): void { - $this->provider->import([ - 'enabled' => true, - 'apiVersion' => '1.1.0', - 'endPoint' => 'https://cloud.example.org/ocm', - 'capabilities' => ['http-sig'], - 'jwksUri' => 'https://cloud.example.org/ocm/jwks', - ]); - - $this->assertSame('https://cloud.example.org/ocm/jwks', $this->provider->getJwksUri()); - } - - public function testJwksUriSerializedOnlyWhenSet(): void { - $this->provider->setEnabled(true) - ->setApiVersion('1.1.0') - ->setEndPoint('https://cloud.example.org/ocm'); - - $this->assertArrayNotHasKey('jwksUri', $this->provider->jsonSerialize()); - - $this->provider->setJwksUri('https://cloud.example.org/ocm/jwks'); - $this->assertSame( - 'https://cloud.example.org/ocm/jwks', - $this->provider->jsonSerialize()['jwksUri'], - ); - } -} --- a/tests/lib/OCM/OCMSignatoryManagerJwksTest.php +++ /dev/null @@ -1,368 +0,0 @@ -appConfig = $this->createMock(IAppConfig::class); - $this->signatureManager = $this->createMock(ISignatureManager::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->identityProofManager = $this->createMock(IdentityProofManager::class); - $this->clientService = $this->createMock(IClientService::class); - $this->config = $this->createMock(IConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->client = $this->createMock(IClient::class); - $this->discoveryService = $this->createMock(IOCMDiscoveryService::class); - - $this->clientService->method('newClient')->willReturn($this->client); - $this->overwriteService(IOCMDiscoveryService::class, $this->discoveryService); - - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createDistributed')->willReturn(new ArrayCache('')); - - $this->signatoryManager = new OCMSignatoryManager( - $this->appConfig, - $this->signatureManager, - $this->urlGenerator, - $this->identityProofManager, - $this->clientService, - $this->config, - $cacheFactory, - $this->logger, - ); - } - - #[\Override] - protected function tearDown(): void { - $this->restoreService(IOCMDiscoveryService::class); - parent::tearDown(); - } - - /** Remote discovery response advertising http-sig and $jwksUri. */ - private function primeDiscovery( - string $jwksUri = self::JWKS_URI, - array $capabilities = ['http-sig'], - bool $httpOnly = false, - ): void { - $provider = new OCMProvider(); - $provider->setCapabilities($capabilities); - $provider->setJwksUri($jwksUri); - $this->discoveryService->method('discover') - ->willReturnCallback(static function (string $remote) use ($httpOnly, $provider): OCMProvider { - if ($httpOnly && str_starts_with($remote, 'https://')) { - throw new OCMProviderException('HTTPS unavailable'); - } - return $provider; - }); - } - - public function testGetRemoteKeyFetchesAndMatchesByKid(): void { - $this->primeDiscovery(); - $kid = 'sender.example.org#key1'; - $jwks = [ - 'keys' => [ - $this->ecJwk('other'), - $this->ecJwk($kid), - ], - ]; - $this->respondWith($jwks); - - $key = $this->signatoryManager->getRemoteKey('sender.example.org', $kid); - $this->assertNotNull($key); - $this->assertSame('ES256', $key->getAlgorithm()); - } - - public function testGetRemoteKeyReturnsNullWhenKidMissing(): void { - $this->primeDiscovery(); - $this->respondWith(['keys' => [$this->ecJwk('unrelated')]]); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'other-kid')); - } - - public function testGetRemoteKeyReturnsNullOnHttpError(): void { - $this->primeDiscovery(); - $this->client->method('get')->willThrowException(new \RuntimeException('boom')); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyReturnsNullOnInvalidJson(): void { - $this->primeDiscovery(); - $response = $this->createMock(IResponse::class); - $response->method('getBody')->willReturn('not json'); - $this->client->method('get')->willReturn($response); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyReturnsNullWhenKeysMissing(): void { - $this->primeDiscovery(); - $this->respondWith(['no-keys-here' => []]); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyReturnsNullOnUnparseableJwk(): void { - $this->primeDiscovery(); - // JWK with kty=EC but no crv: parseKey rejects. - $this->respondWith(['keys' => [['kty' => 'EC', 'kid' => 'kid', 'alg' => 'ES256', 'x' => self::TEST_X, 'y' => self::TEST_Y]]]); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyFetchesFromAdvertisedJwksUri(): void { - $this->primeDiscovery(); - $this->client->expects($this->once()) - ->method('get') - ->with( - $this->equalTo(self::JWKS_URI), - $this->isType('array'), - ) - ->willReturn($this->jsonResponse(['keys' => []])); - - $this->signatoryManager->getRemoteKey('sender.example.org', 'kid'); - } - - public function testGetRemoteKeyRejectsMissingJwksUriWhenHttpSigAdvertised(): void { - // a peer advertising http-sig without a jwksUri is non-conformant - $this->primeDiscovery(jwksUri: ''); - $this->client->expects($this->never())->method('get'); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyRejectsHttpJwksUriFromHttpsPeer(): void { - // downgrade guard: http jwksUri from an https peer - $this->primeDiscovery(jwksUri: 'http://sender.example.org/ocm/jwks'); - $this->client->expects($this->never())->method('get'); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyAcceptsHttpJwksUriFromHttpPeer(): void { - // the spec's http fallback for testing setups - $this->primeDiscovery( - jwksUri: 'http://sender.example.org/ocm/jwks', - httpOnly: true, - ); - $kid = 'sender.example.org#key1'; - $this->client->expects($this->once()) - ->method('get') - ->with( - $this->equalTo('http://sender.example.org/ocm/jwks'), - $this->isType('array'), - ) - ->willReturn($this->jsonResponse(['keys' => [$this->ecJwk($kid)]])); - - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', $kid)); - } - - public function testGetRemoteKeyReturnsNullWhenDiscoveryFails(): void { - $this->discoveryService->method('discover') - ->willThrowException(new OCMProviderException('no discovery')); - $this->client->expects($this->never())->method('get'); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyRejectsJwkWithoutAlg(): void { - $this->primeDiscovery(); - $jwk = $this->ecJwk('kid'); - unset($jwk['alg']); - $this->respondWith(['keys' => [$jwk]]); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyRejectsJwkWithSymmetricAlg(): void { - $this->primeDiscovery(); - $jwk = $this->ecJwk('kid'); - $jwk['alg'] = 'HS256'; - $this->respondWith(['keys' => [$jwk]]); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyRejectsJwkAlgMismatchingKeyType(): void { - $this->primeDiscovery(); - // EC P-256 key claiming an Ed25519 algorithm - $jwk = $this->ecJwk('kid'); - $jwk['alg'] = 'Ed25519'; - $this->respondWith(['keys' => [$jwk]]); - $this->logger->expects($this->once())->method('warning'); - $this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid')); - } - - public function testGetRemoteKeyAcceptsFullySpecifiedEd25519Alg(): void { - $this->primeDiscovery(); - $this->respondWith(['keys' => [[ - 'kty' => 'OKP', - 'crv' => 'Ed25519', - 'kid' => 'kid', - 'alg' => 'Ed25519', - 'use' => 'sig', - 'x' => self::TEST_OKP_X, - ]]]); - - $key = $this->signatoryManager->getRemoteKey('sender.example.org', 'kid'); - $this->assertNotNull($key); - $this->assertSame('Ed25519', $key->getAlgorithm()); - } - - public function testGetRemoteKeyPassesSelfSignedFlagThrough(): void { - $this->primeDiscovery(); - $this->config->method('getSystemValueBool') - ->with('sharing.federation.allowSelfSignedCertificates') - ->willReturn(true); - - $this->client->expects($this->once()) - ->method('get') - ->with( - $this->anything(), - $this->callback(static fn (array $opts): bool => ($opts['verify'] ?? null) === false), - ) - ->willReturn($this->jsonResponse(['keys' => []])); - - $this->signatoryManager->getRemoteKey('sender.example.org', 'kid'); - } - - public function testJwksCachedAcrossCallsToTheSameOrigin(): void { - $this->primeDiscovery(); - $kid = 'sender.example.org#key1'; - $jwks = ['keys' => [$this->ecJwk($kid)]]; - $this->client->expects($this->once()) - ->method('get') - ->willReturn($this->jsonResponse($jwks)); - - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', $kid)); - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', $kid)); - } - - public function testCacheMissOnNewKidTriggersRefetchOnce(): void { - $this->primeDiscovery(); - $first = ['keys' => [$this->ecJwk('old')]]; - $second = ['keys' => [$this->ecJwk('new')]]; - $this->client->expects($this->exactly(2)) - ->method('get') - ->willReturnOnConsecutiveCalls( - $this->jsonResponse($first), - $this->jsonResponse($second), - ); - - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', 'old')); - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', 'new')); - } - - public function testGetRemoteKeyAcceptsHttpsJwksUriFromHttpPeer(): void { - // upgrade from an http-only peer is fine - $this->primeDiscovery( - httpOnly: true, - ); - $kid = 'sender.example.org#key1'; - $this->client->expects($this->once()) - ->method('get') - ->with( - $this->equalTo(self::JWKS_URI), - $this->isType('array'), - ) - ->willReturn($this->jsonResponse(['keys' => [$this->ecJwk($kid)]])); - - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', $kid)); - } - - public function testGetRemoteKeyAcceptsJwksUriOnDifferentHost(): void { - // the JWK Set may live on a different host than the peer - $this->primeDiscovery( - jwksUri: 'https://keys.example.net/ocm/jwks', - httpOnly: true, - ); - $kid = 'sender.example.org#key1'; - $this->client->expects($this->once()) - ->method('get') - ->with( - $this->equalTo('https://keys.example.net/ocm/jwks'), - $this->isType('array'), - ) - ->willReturn($this->jsonResponse(['keys' => [$this->ecJwk($kid)]])); - - $this->assertNotNull($this->signatoryManager->getRemoteKey('sender.example.org', $kid)); - } - - public function testGetLocalJwksUriPointsAtAppRoute(): void { - $this->urlGenerator->method('linkToRouteAbsolute') - ->willReturn('https://sender.example.org/index.php/apps/cloud_federation_api/api/v1/jwks'); - - $this->assertSame( - 'https://sender.example.org/index.php/apps/cloud_federation_api/api/v1/jwks', - $this->signatoryManager->getLocalJwksUri(), - ); - } - - private function respondWith(array $body): void { - $this->client->method('get')->willReturn($this->jsonResponse($body)); - } - - private function jsonResponse(array $body): IResponse { - $response = $this->createMock(IResponse::class); - $response->method('getBody')->willReturn(json_encode($body, JSON_THROW_ON_ERROR)); - return $response; - } - - /** @return array */ - private function ecJwk(string $kid): array { - return [ - 'kty' => 'EC', - 'crv' => 'P-256', - 'kid' => $kid, - 'alg' => 'ES256', - 'use' => 'sig', - 'x' => self::TEST_X, - 'y' => self::TEST_Y, - ]; - } -} --- a/tests/lib/OCM/OCMSignatoryManagerRotationTest.php +++ /dev/null @@ -1,283 +0,0 @@ - in-memory backing store for IAppConfig core/* */ - private array $appConfigStore = []; - /** @var array in-memory backing store for IdentityProofManager appkeys */ - private array $appKeyStore = []; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->appConfig = $this->createMock(IAppConfig::class); - $this->identityProofManager = $this->createMock(IdentityProofManager::class); - - $this->wireAppConfig(); - $this->wireIdentityProofManager(); - - $signatureManager = $this->createMock(ISignatureManager::class); - $signatureManager->method('generateKeyIdFromConfig') - ->willThrowException(new IdentityNotFoundException('no configured identity')); - $urlGenerator = $this->createMock(IURLGenerator::class); - $urlGenerator->method('getAbsoluteURL') - ->willReturnCallback(static fn (string $path): string => 'https://alice.example' . $path); - - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createDistributed')->willReturn(new ArrayCache('')); - - $this->signatoryManager = new OCMSignatoryManager( - $this->appConfig, - $signatureManager, - $urlGenerator, - $this->identityProofManager, - $this->stubClientService(), - $this->createMock(IConfig::class), - $cacheFactory, - $this->createMock(LoggerInterface::class), - ); - } - - public function testJwksBootstrapsActiveKeyOnFirstFetch(): void { - // Fresh instance: first JWKS hit must provision the active key. - $jwks = $this->signatoryManager->getLocalJwks(); - $this->assertCount(1, $jwks); - $this->assertSame('https://alice.example/ocm#ecdsa-p256-sha256-1', $jwks[0]['kid']); - - // And the bootstrapped key is the active one for outbound signing. - $signatory = $this->signatoryManager->getLocalJwksSignatory(); - $this->assertSame($jwks[0]['kid'], $signatory->getKeyId()); - } - - public function testFirstCallProvisionsActiveKey(): void { - $signatory = $this->signatoryManager->getLocalJwksSignatory(); - $this->assertNotNull($signatory); - $this->assertSame('https://alice.example/ocm#ecdsa-p256-sha256-1', $signatory->getKeyId()); - - $jwks = $this->signatoryManager->getLocalJwks(); - $this->assertCount(1, $jwks); - $this->assertSame($signatory->getKeyId(), $jwks[0]['kid']); - - $listed = $this->signatoryManager->listJwksKeys(); - $this->assertSame([['poolId' => 1, 'kid' => $signatory->getKeyId(), 'slot' => 'active']], $listed); - } - - public function testStageDoesNotChangeActiveSignerButPublishesNewJwk(): void { - $initial = $this->signatoryManager->getLocalJwksSignatory(); - $staged = $this->signatoryManager->stageJwksKey(); - $this->assertNotSame($initial->getKeyId(), $staged->getKeyId()); - - // Active signer is unchanged. - $this->assertSame($initial->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId()); - - // JWKS now advertises both kids, active first then pending. - $jwks = $this->signatoryManager->getLocalJwks(); - $this->assertSame([$initial->getKeyId(), $staged->getKeyId()], array_column($jwks, 'kid')); - } - - public function testStageRefusesIfPendingAlreadyExists(): void { - $this->signatoryManager->stageJwksKey(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/pending JWKS key already exists/'); - $this->signatoryManager->stageJwksKey(); - } - - public function testActivatePromotesPendingAndDemotesActive(): void { - $first = $this->signatoryManager->getLocalJwksSignatory(); - $staged = $this->signatoryManager->stageJwksKey(); - $this->signatoryManager->activateStagedJwksKey(); - - // New signer is the formerly-staged key. - $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId()); - - // JWKS still advertises the former active key as retiring so peers - // verifying in-flight signatures with its kid don't fail. - $kids = array_column($this->signatoryManager->getLocalJwks(), 'kid'); - $this->assertContains($first->getKeyId(), $kids); - $this->assertContains($staged->getKeyId(), $kids); - } - - public function testActivateRefusesIfRetiringStillPopulated(): void { - $this->signatoryManager->getLocalJwksSignatory(); - $this->signatoryManager->stageJwksKey(); - $this->signatoryManager->activateStagedJwksKey(); - // Retiring slot is now populated; staging again is allowed but - // activating must refuse until the admin explicitly retires the old - // key. - $this->signatoryManager->stageJwksKey(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/retiring JWKS key still exists/'); - $this->signatoryManager->activateStagedJwksKey(); - } - - public function testActivateRefusesWithoutPendingKey(): void { - $this->signatoryManager->getLocalJwksSignatory(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/no pending JWKS key/'); - $this->signatoryManager->activateStagedJwksKey(); - } - - public function testRetireRemovesRetiringKeyFromJwks(): void { - $first = $this->signatoryManager->getLocalJwksSignatory(); - $staged = $this->signatoryManager->stageJwksKey(); - $this->signatoryManager->activateStagedJwksKey(); - $this->signatoryManager->retireJwksKey(); - - $kids = array_column($this->signatoryManager->getLocalJwks(), 'kid'); - $this->assertSame([$staged->getKeyId()], $kids); - // listJwksKeys also drops the retired pool. - $listed = $this->signatoryManager->listJwksKeys(); - $this->assertCount(1, $listed); - $this->assertSame($staged->getKeyId(), $listed[0]['kid']); - $this->assertNotContains($first->getKeyId(), array_column($listed, 'kid')); - } - - public function testRetireRefusesWhenNothingToRetire(): void { - $this->signatoryManager->getLocalJwksSignatory(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageMatches('/no retiring JWKS key/'); - $this->signatoryManager->retireJwksKey(); - } - - public function testKidStaysStableThroughLifecycle(): void { - $first = $this->signatoryManager->getLocalJwksSignatory(); - $staged = $this->signatoryManager->stageJwksKey(); - // kid for the staged key must stay the same once it is activated; - // peers that cached it during the stage window must still resolve it. - $this->signatoryManager->activateStagedJwksKey(); - $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId()); - - $this->signatoryManager->retireJwksKey(); - $this->signatoryManager->stageJwksKey(); - // And every newly minted kid must differ from prior ones, no pool - // counter rewinding. - $kids = array_column($this->signatoryManager->listJwksKeys(), 'kid'); - $this->assertNotContains($first->getKeyId(), $kids); - $this->assertSame($kids, array_unique($kids)); - } - - public function testSignerReturnsNullWhenIdentityCannotBeDerived(): void { - // Replace the signature manager with one that cannot derive an - // identity at all; provisioning the first key should fail loudly so - // the admin gets a clear message instead of a corrupt half-state. - $signatureManager = $this->createMock(ISignatureManager::class); - $signatureManager->method('generateKeyIdFromConfig') - ->willThrowException(new IdentityNotFoundException('no configured identity')); - $urlGenerator = $this->createMock(IURLGenerator::class); - // getAbsoluteURL() yields no host, so the kid's identity cannot be - // resolved; provisioning must fail loudly rather than corrupt state. - $urlGenerator->method('getAbsoluteURL')->willReturn(''); - - $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createDistributed')->willReturn(new ArrayCache('')); - - $manager = new OCMSignatoryManager( - $this->appConfig, - $signatureManager, - $urlGenerator, - $this->identityProofManager, - $this->stubClientService(), - $this->createMock(IConfig::class), - $cacheFactory, - $this->createMock(LoggerInterface::class), - ); - - $this->expectException(\RuntimeException::class); - $manager->getLocalJwksSignatory(); - } - - private function wireAppConfig(): void { - $this->appConfig->method('hasKey')->willReturnCallback( - fn (string $app, string $key): bool => $app === 'core' && array_key_exists($key, $this->appConfigStore) - ); - $this->appConfig->method('getValueInt')->willReturnCallback( - fn (string $app, string $key, int $default = 0): int => (int)($this->appConfigStore[$key] ?? $default) - ); - $this->appConfig->method('setValueInt')->willReturnCallback( - function (string $app, string $key, int $value): bool { - $this->appConfigStore[$key] = (string)$value; - return true; - } - ); - $this->appConfig->method('getValueString')->willReturnCallback( - fn (string $app, string $key, string $default = '') => $this->appConfigStore[$key] ?? $default - ); - $this->appConfig->method('setValueString')->willReturnCallback( - function (string $app, string $key, string $value): bool { - $this->appConfigStore[$key] = $value; - return true; - } - ); - $this->appConfig->method('getValueBool')->willReturn(false); - $this->appConfig->method('deleteKey')->willReturnCallback( - function (string $app, string $key): void { - unset($this->appConfigStore[$key]); - } - ); - } - - private function wireIdentityProofManager(): void { - $this->identityProofManager->method('hasAppKey')->willReturnCallback( - fn (string $app, string $name): bool => isset($this->appKeyStore[$app . '/' . $name]) - ); - $this->identityProofManager->method('generateEcdsaP256AppKey')->willReturnCallback( - function (string $app, string $name): Key { - $res = openssl_pkey_new([ - 'private_key_type' => OPENSSL_KEYTYPE_EC, - 'curve_name' => 'prime256v1', - ]); - openssl_pkey_export($res, $privatePem); - $publicPem = openssl_pkey_get_details($res)['key']; - $key = new Key($publicPem, $privatePem); - $this->appKeyStore[$app . '/' . $name] = $key; - return $key; - } - ); - $this->identityProofManager->method('getAppKey')->willReturnCallback( - fn (string $app, string $name): Key => $this->appKeyStore[$app . '/' . $name] - ); - $this->identityProofManager->method('deleteAppKey')->willReturnCallback( - function (string $app, string $name): bool { - $existed = isset($this->appKeyStore[$app . '/' . $name]); - unset($this->appKeyStore[$app . '/' . $name]); - return $existed; - } - ); - } - - private function stubClientService(): IClientService&MockObject { - $service = $this->createMock(IClientService::class); - $service->method('newClient')->willReturn($this->createMock(IClient::class)); - return $service; - } -} --- a/tests/lib/OCM/Rfc9421SignatoryManagerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -delegate = $this->createMock(OCMSignatoryManager::class); - $this->wrapper = new Rfc9421SignatoryManager($this->delegate); - } - - public function testGetOptionsForcesRfc9421Format(): void { - $this->delegate->method('getOptions')->willReturn([ - 'algorithm' => 'rsa-sha512', - 'rfc9421.format' => false, - ]); - - $options = $this->wrapper->getOptions(); - $this->assertTrue($options['rfc9421.format']); - $this->assertSame('rsa-sha512', $options['algorithm']); - } - - public function testGetLocalSignatoryReturnsJwksKey(): void { - $signatory = $this->createMock(Signatory::class); - $this->delegate->method('getLocalJwksSignatory')->willReturn($signatory); - - $this->assertSame($signatory, $this->wrapper->getLocalSignatory()); - } - - public function testGetLocalSignatoryThrowsWhenJwksKeyUnavailable(): void { - $this->delegate->method('getLocalJwksSignatory')->willReturn(null); - - $this->expectException(IdentityNotFoundException::class); - $this->wrapper->getLocalSignatory(); - } - - public function testProviderIdDelegated(): void { - $this->delegate->method('getProviderId')->willReturn('ocm'); - $this->assertSame('ocm', $this->wrapper->getProviderId()); - } - - public function testRemoteSignatoryDelegated(): void { - $signatory = $this->createMock(Signatory::class); - $this->delegate->expects($this->once()) - ->method('getRemoteSignatory') - ->with('sender.example.org') - ->willReturn($signatory); - $this->assertSame($signatory, $this->wrapper->getRemoteSignatory('sender.example.org')); - } - - public function testRemoteKeyDelegated(): void { - $key = $this->createMock(Key::class); - $this->delegate->expects($this->once()) - ->method('getRemoteKey') - ->with('sender.example.org', 'kid-1') - ->willReturn($key); - $this->assertSame($key, $this->wrapper->getRemoteKey('sender.example.org', 'kid-1')); - } -} --- a/tests/lib/OCS/ApiHelperTest.php +++ /dev/null @@ -1,49 +0,0 @@ -getMockBuilder(IRequest::class) - ->disableOriginalConstructor() - ->getMock(); - $request - ->expects($this->once()) - ->method('getScriptName') - ->willReturn($scriptName); - - $this->assertEquals($expected, $this->invokePrivate(new ApiHelper, 'isV2', [$request])); - } -} --- a/tests/lib/OCS/DiscoveryServiceTest.php +++ /dev/null @@ -1,82 +0,0 @@ -cacheFactory = $this->getMockBuilder(ICacheFactory::class)->getMock(); - $this->clientService = $this->getMockBuilder(IClientService::class)->getMock(); - - $this->discoveryService = new DiscoveryService( - $this->cacheFactory, - $this->clientService - ); - } - - /** - * - * @param string $url - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsSafeUrl')] - public function testIsSafeUrl($url, $expected): void { - $result = $this->invokePrivate($this->discoveryService, 'isSafeUrl', [$url]); - $this->assertSame($expected, $result); - } - - public static function dataTestIsSafeUrl(): array { - return [ - ['api/ocs/v1.php/foo', true], - ['/api/ocs/v1.php/foo', true], - ['api/ocs/v1.php/foo/', true], - ['api/ocs/v1.php/foo-bar/', true], - ['api/ocs/v1:php/foo', false], - ['api/ocs//foo', false], - ]; - } - - /** - * - * @param array $decodedServices - * @param string $service - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetEndpoints')] - public function testGetEndpoints($decodedServices, $service, $expected): void { - $result = $this->invokePrivate($this->discoveryService, 'getEndpoints', [$decodedServices, $service]); - $this->assertSame($expected, $result); - } - - public static function dataTestGetEndpoints(): array { - return [ - [['services' => ['myService' => ['endpoints' => []]]], 'myService', []], - [['services' => ['myService' => ['endpoints' => ['foo' => '/bar']]]], 'myService', ['foo' => '/bar']], - [['services' => ['myService' => ['endpoints' => ['foo' => '/bar']]]], 'anotherService', []], - [['services' => ['myService' => ['endpoints' => ['foo' => '/barrequest = $this->getMockBuilder('\\OCP\\IRequest')->getMock(); - $this->appManager = $this->getMockBuilder('\\OCP\\App\\IAppManager')->getMock(); - $this->ocsProvider = new Provider('ocs_provider', $this->request, $this->appManager); - } - - public function testBuildProviderListWithoutAnythingEnabled(): void { - $this->appManager - ->expects($this->exactly(4)) - ->method('isEnabledForUser') - ->willReturnMap([ - ['files_sharing', null, false], - ['federation', null, false], - ['activity', null, false], - ['provisioning_api', null, false], - ]); - - $expected = new JSONResponse( - [ - 'version' => 2, - 'services' => [ - 'PRIVATE_DATA' => [ - 'version' => 1, - 'endpoints' => [ - 'store' => '/ocs/v2.php/privatedata/setattribute', - 'read' => '/ocs/v2.php/privatedata/getattribute', - 'delete' => '/ocs/v2.php/privatedata/deleteattribute', - ], - ], - ], - ] - ); - - $this->assertEquals($expected, $this->ocsProvider->buildProviderList()); - } - - public function testBuildProviderListWithSharingEnabled(): void { - $this->appManager - ->expects($this->exactly(4)) - ->method('isEnabledForUser') - ->willReturnMap([ - ['files_sharing', null, true], - ['federation', null, false], - ['activity', null, false], - ['provisioning_api', null, false], - ]); - - $expected = new JSONResponse( - [ - 'version' => 2, - 'services' => [ - 'PRIVATE_DATA' => [ - 'version' => 1, - 'endpoints' => [ - 'store' => '/ocs/v2.php/privatedata/setattribute', - 'read' => '/ocs/v2.php/privatedata/getattribute', - 'delete' => '/ocs/v2.php/privatedata/deleteattribute', - ], - ], - 'FEDERATED_SHARING' => [ - 'version' => 1, - 'endpoints' => [ - 'share' => '/ocs/v2.php/cloud/shares', - 'webdav' => '/public.php/webdav/', - ], - ], - 'SHARING' => [ - 'version' => 1, - 'endpoints' => [ - 'share' => '/ocs/v2.php/apps/files_sharing/api/v1/shares', - ], - ], - ], - ] - ); - - $this->assertEquals($expected, $this->ocsProvider->buildProviderList()); - } - - public function testBuildProviderListWithFederationEnabled(): void { - $this->appManager - ->expects($this->exactly(4)) - ->method('isEnabledForUser') - ->willReturnMap([ - ['files_sharing', null, false], - ['federation', null, true], - ['activity', null, false], - ['provisioning_api', null, false], - ]); - - $expected = new JSONResponse( - [ - 'version' => 2, - 'services' => [ - 'PRIVATE_DATA' => [ - 'version' => 1, - 'endpoints' => [ - 'store' => '/ocs/v2.php/privatedata/setattribute', - 'read' => '/ocs/v2.php/privatedata/getattribute', - 'delete' => '/ocs/v2.php/privatedata/deleteattribute', - ], - ], - 'FEDERATED_SHARING' => [ - 'version' => 1, - 'endpoints' => [ - 'shared-secret' => '/ocs/v2.php/cloud/shared-secret', - 'system-address-book' => '/remote.php/dav/addressbooks/system/system/system', - 'carddav-user' => 'system' - ], - ], - ], - ] - ); - - $this->assertEquals($expected, $this->ocsProvider->buildProviderList()); - } - - public function testBuildProviderListWithEverythingEnabled(): void { - $this->appManager - ->expects($this->any()) - ->method('isEnabledForUser') - ->willReturn(true); - - $expected = new JSONResponse( - [ - 'version' => 2, - 'services' => [ - 'PRIVATE_DATA' => [ - 'version' => 1, - 'endpoints' => [ - 'store' => '/ocs/v2.php/privatedata/setattribute', - 'read' => '/ocs/v2.php/privatedata/getattribute', - 'delete' => '/ocs/v2.php/privatedata/deleteattribute', - ], - ], - 'FEDERATED_SHARING' => [ - 'version' => 1, - 'endpoints' => [ - 'share' => '/ocs/v2.php/cloud/shares', - 'webdav' => '/public.php/webdav/', - 'shared-secret' => '/ocs/v2.php/cloud/shared-secret', - 'system-address-book' => '/remote.php/dav/addressbooks/system/system/system', - 'carddav-user' => 'system' - ], - ], - 'SHARING' => [ - 'version' => 1, - 'endpoints' => [ - 'share' => '/ocs/v2.php/apps/files_sharing/api/v1/shares', - ], - ], - 'ACTIVITY' => [ - 'version' => 1, - 'endpoints' => [ - 'list' => '/ocs/v2.php/cloud/activity', - ], - ], - 'PROVISIONING' => [ - 'version' => 1, - 'endpoints' => [ - 'user' => '/ocs/v2.php/cloud/users', - 'groups' => '/ocs/v2.php/cloud/groups', - 'apps' => '/ocs/v2.php/cloud/apps', - ], - ], - ], - ] - ); - - $this->assertEquals($expected, $this->ocsProvider->buildProviderList()); - } -} --- a/tests/lib/OpenMetrics/ExporterManagerTest.php +++ /dev/null @@ -1,23 +0,0 @@ -assertInstanceOf(ExporterManager::class, $exporter); - foreach ($exporter->export() as $metric) { - $this->assertInstanceOf(IMetricFamily::class, $metric); - }; - } -} --- a/tests/lib/OpenMetrics/Exporters/ActiveSessionsTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertLabelsAre([ - ['time' => 'Last 5 minutes'], - ['time' => 'Last 15 minutes'], - ['time' => 'Last hour'], - ['time' => 'Last day'], - ]); - } -} --- a/tests/lib/OpenMetrics/Exporters/ActiveUsersTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertLabelsAre([ - ['time' => 'Last 5 minutes'], - ['time' => 'Last 15 minutes'], - ['time' => 'Last hour'], - ['time' => 'Last day'], - ]); - } -} --- a/tests/lib/OpenMetrics/Exporters/AppEnabledTest.php +++ /dev/null @@ -1,53 +0,0 @@ - '0.1.2', - 'appB' => '1.2.3 beta 4', - 'appC' => '6.2.1', - 'appD' => '3.141.5', - ]; - private array $installedAppsList = [ - 'appA', 'appB', 'appD' - ]; - - #[\Override] - protected function getExporter():IMetricFamily { - $this->appManager = $this->createMock(IAppManager::class); - $this->appManager->method('getAppInstalledVersions') - ->with(false) - ->willReturn($this->appList); - $this->appManager->method('getEnabledApps') - ->willReturn($this->installedAppsList); - - return new AppEnabled($this->appManager); - } - - public function testMetrics(): void { - $this->assertCount(4, $this->metrics); - foreach ($this->appList as $appId => $appVersion) { - $metricForApp = array_find($this->metrics, function (Metric $metric) use ($appId) { - return $metric->label('app_id') === $appId; - }); - - $expectedMetricValue = in_array($appId, $this->installedAppsList) ? 1 : 0; - $this->assertEquals($expectedMetricValue, $metricForApp->value); - $this->assertSame(['app_id' => $appId, 'version' => $appVersion], $metricForApp->labels); - } - } -} --- a/tests/lib/OpenMetrics/Exporters/ExporterTestCase.php +++ /dev/null @@ -1,77 +0,0 @@ -exporter = $this->getExporter(); - $this->metrics = iterator_to_array($this->exporter->metrics()); - } - - public function testNotEmptyData(): void { - $this->assertNotEmpty($this->metrics); - } - - public function testValidExporterName(): void { - $exporterName = $this->exporter->name(); - $this->assertMatchesRegularExpression('/^[a-z_:][a-z0-9_:]*$/i', $exporterName, ); - - $unit = $this->exporter->unit(); - if ($unit === '') { - return; - } - // Unit name must follow metric name format - $this->assertMatchesRegularExpression('/^[a-z_:][a-z0-9_:]*$/i', $unit); - // Unit name must be a suffix in exporter name - $this->assertMatchesRegularExpression( - '/(^|_)' . $unit . '$/', - $exporterName, - 'Metric name "' . $exporterName . '" must contains unit "' . $unit . '" as a suffix', - ); - } - - public function testValidLabelKey(): void { - $labelNames = []; - foreach ($this->metrics as $metric) { - foreach ($metric->labels as $label => $value) { - $labelNames[$label] = $label; - } - } - - if (empty($labelNames)) { - $this->expectNotToPerformAssertions(); - return; - } - foreach ($labelNames as $label) { - $this->assertMatchesRegularExpression('/^[a-z_][a-z0-9_]*$/i', $label); - } - - } - - protected function assertLabelsAre(array $expectedLabels): void { - $foundLabels = []; - foreach ($this->metrics as $metric) { - $foundLabels[] = $metric->labels; - } - - $this->assertSame($foundLabels, $expectedLabels); - } -} --- a/tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php +++ /dev/null @@ -1,30 +0,0 @@ -systemConfig = $this->createMock(SystemConfig::class); - $this->serverVersion = $this->createMock(ServerVersion::class); - $this->serverVersion->method('getHumanVersion')->willReturn('33.13.17 Gold'); - $this->serverVersion->method('getVersion')->willReturn([33, 13, 17]); - $this->serverVersion->method('getBuild')->willReturn('dev'); - - return new InstanceInfo($this->systemConfig, $this->serverVersion); - } - - public function testMetrics(): void { - $this->assertCount(1, $this->metrics); - $metric = array_pop($this->metrics); - $this->assertSame([ - 'full_version' => '33.13.17 Gold', - 'major_version' => '33', - 'build' => 'dev', - 'installed' => '0', - ], $metric->labels); - } -} --- a/tests/lib/OpenMetrics/Exporters/LogLevelTest.php +++ /dev/null @@ -1,22 +0,0 @@ - 42, - 'backend B' => 51, - 'backend C' => 0, - ]; - - #[\Override] - protected function getExporter():IMetricFamily { - $this->userManager = $this->createMock(IUserManager::class); - $this->userManager->method('countUsers') - ->with(true) - ->willReturn($this->backendList); - return new UsersByBackend($this->userManager); - } - - public function testMetrics(): void { - foreach ($this->metrics as $metric) { - $this->assertEquals($this->backendList[$metric->label('backend')], $metric->value); - } - } -} --- a/tests/lib/Preview/BackgroundCleanupJobTest.php +++ /dev/null @@ -1,146 +0,0 @@ -userId = $this->getUniqueID(); - $user = $this->createUser($this->userId, $this->userId); - - $storage = new Temporary([]); - $this->registerMount($this->userId, $storage, ''); - - $this->loginAsUser($this->userId); - $this->logout(); - $this->loginAsUser($this->userId); - - $appManager = Server::get(IAppManager::class); - $this->trashEnabled = $appManager->isEnabledForUser('files_trashbin', $user); - $appManager->disableApp('files_trashbin'); - - $this->connection = Server::get(IDBConnection::class); - $this->previewManager = Server::get(IPreview::class); - $this->rootFolder = Server::get(IRootFolder::class); - $this->mimeTypeLoader = Server::get(IMimeTypeLoader::class); - $this->timeFactory = Server::get(ITimeFactory::class); - $this->previewService = Server::get(PreviewService::class); - } - - #[\Override] - protected function tearDown(): void { - if ($this->trashEnabled) { - $appManager = Server::get(IAppManager::class); - $appManager->enableApp('files_trashbin'); - } - - $this->logout(); - - foreach ($this->previewService->getAvailablePreviewsForFile(5) as $preview) { - $this->previewService->deletePreview($preview); - } - - parent::tearDown(); - } - - private function setup11Previews(): array { - $userFolder = $this->rootFolder->getUserFolder($this->userId); - - $files = []; - foreach (range(0, 10) as $i) { - $file = $userFolder->newFile($i . '.txt'); - $file->putContent('hello world!'); - $this->previewManager->getPreview($file); - $files[] = $file; - } - - return $files; - } - - private function countPreviews(PreviewService $previewService, array $fileIds): int { - $previews = $previewService->getAvailablePreviews($fileIds); - return array_reduce($previews, fn (int $result, array $previews) => $result + count($previews), 0); - } - - public function testCleanupSystemCron(): void { - $files = $this->setup11Previews(); - $fileIds = array_map(fn (File $f): int => $f->getId(), $files); - - $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds)); - $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewService, true); - $job->run([]); - - foreach ($files as $file) { - $file->delete(); - } - - $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds)); - $job->run([]); - - $this->assertSame(0, $this->countPreviews($this->previewService, $fileIds)); - } - - public function testCleanupAjax(): void { - if ($this->connection->getShardDefinition('filecache')) { - $this->markTestSkipped('ajax cron is not supported for sharded setups'); - } - $files = $this->setup11Previews(); - $fileIds = array_map(fn (File $f): int => $f->getId(), $files); - - $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds)); - $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewService, false); - $job->run([]); - - foreach ($files as $file) { - $file->delete(); - } - - $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds)); - $job->run([]); - - $this->assertSame(1, $this->countPreviews($this->previewService, $fileIds)); - $job->run([]); - - $this->assertSame(0, $this->countPreviews($this->previewService, $fileIds)); - } -} --- a/tests/lib/Preview/GeneratorTest.php +++ /dev/null @@ -1,526 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->previewManager = $this->createMock(IPreview::class); - $this->helper = $this->createMock(GeneratorHelper::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->previewMapper = $this->createMock(PreviewMapper::class); - $this->storageFactory = $this->createMock(StorageFactory::class); - $this->migrationService = $this->createMock(PreviewMigrationService::class); - - $this->generator = new Generator( - $this->config, - $this->appConfig, - $this->previewManager, - $this->helper, - $this->eventDispatcher, - $this->logger, - $this->previewMapper, - $this->storageFactory, - $this->migrationService, - ); - } - - private function getFile(int $fileId, string $mimeType, bool $hasVersion = false): File { - $mountPoint = $this->createMock(IMountPoint::class); - $mountPoint->method('getNumericStorageId')->willReturn(42); - if ($hasVersion) { - $file = $this->createMock(VersionedPreviewFile::class); - $file->method('getPreviewVersion')->willReturn('abc'); - } else { - $file = $this->createMock(File::class); - } - $file->method('isReadable') - ->willReturn(true); - $file->method('getMimeType') - ->willReturn($mimeType); - $file->method('getId') - ->willReturn($fileId); - $file->method('getMountPoint') - ->willReturn($mountPoint); - return $file; - } - - #[TestWith([true])] - #[TestWith([false])] - public function testGetCachedPreview(bool $hasPreview): void { - $file = $this->getFile(42, 'myMimeType', $hasPreview); - - $this->previewManager->method('isMimeSupported') - ->with($this->equalTo('myMimeType')) - ->willReturn(true); - - $maxPreview = new Preview(); - $maxPreview->setWidth(1000); - $maxPreview->setHeight(1000); - $maxPreview->setMax(true); - $maxPreview->setSize(1000); - $maxPreview->setCropped(false); - $maxPreview->setStorageId(1); - $maxPreview->setVersion($hasPreview ? 'abc' : null); - $maxPreview->setMimeType('image/png'); - - $previewFile = new Preview(); - $previewFile->setWidth(256); - $previewFile->setHeight(256); - $previewFile->setMax(false); - $previewFile->setSize(1000); - $previewFile->setVersion($hasPreview ? 'abc' : null); - $previewFile->setCropped(false); - $previewFile->setStorageId(1); - $previewFile->setMimeType('image/png'); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => [ - $maxPreview, - $previewFile, - ]]); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); - - $result = $this->generator->getPreview($file, 100, 100); - $this->assertSame($hasPreview ? 'abc-256-256.png' : '256-256.png', $result->getName()); - $this->assertSame(1000, $result->getSize()); - } - - #[TestWith([true])] - #[TestWith([false])] - public function testGetNewPreview(bool $hasVersion): void { - $file = $this->getFile(42, 'myMimeType', $hasVersion); - - $this->previewManager->method('isMimeSupported') - ->with($this->equalTo('myMimeType')) - ->willReturn(true); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => []]); - - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $this->config->method('getSystemValueInt') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $invalidProvider = $this->createMock(IProviderV2::class); - $invalidProvider->method('isAvailable') - ->willReturn(true); - $unavailableProvider = $this->createMock(IProviderV2::class); - $unavailableProvider->method('isAvailable') - ->willReturn(false); - $validProvider = $this->createMock(IProviderV2::class); - $validProvider->method('isAvailable') - ->with($file) - ->willReturn(true); - - $this->previewManager->method('getProviders') - ->willReturn([ - '/image\/png/' => ['wrongProvider'], - '/myMimeType/' => ['brokenProvider', 'invalidProvider', 'unavailableProvider', 'validProvider'], - ]); - - $this->helper->method('getProvider') - ->willReturnCallback(function ($provider) use ($invalidProvider, $validProvider, $unavailableProvider) { - if ($provider === 'wrongProvider') { - $this->fail('Wrongprovider should not be constructed!'); - } elseif ($provider === 'brokenProvider') { - return false; - } elseif ($provider === 'invalidProvider') { - return $invalidProvider; - } elseif ($provider === 'validProvider') { - return $validProvider; - } elseif ($provider === 'unavailableProvider') { - return $unavailableProvider; - } - $this->fail('Unexpected provider requested'); - }); - - $image = $this->createMock(IImage::class); - $image->method('width')->willReturn(2048); - $image->method('height')->willReturn(2048); - $image->method('valid')->willReturn(true); - $image->method('dataMimeType')->willReturn('image/png'); - - $this->helper->method('getThumbnail') - ->willReturnCallback(function ($provider, $file, $x, $y) use ($invalidProvider, $validProvider, $image): false|IImage { - if ($provider === $validProvider) { - return $image; - } else { - return false; - } - }); - - $image->method('data') - ->willReturn('my data'); - - $this->previewMapper->method('insert') - ->willReturnCallback(fn (Preview $preview): Preview => $preview); - - $this->previewMapper->method('update') - ->willReturnCallback(fn (Preview $preview): Preview => $preview); - - $this->storageFactory->method('writePreview') - ->willReturnCallback(function (Preview $preview, mixed $data) use ($hasVersion): int { - $data = stream_get_contents($data); - if ($hasVersion) { - switch ($preview->getName()) { - case 'abc-2048-2048-max.png': - $this->assertSame('my data', $data); - return 1000; - case 'abc-256-256.png': - $this->assertSame('my resized data', $data); - return 1000; - } - } else { - switch ($preview->getName()) { - case '2048-2048-max.png': - $this->assertSame('my data', $data); - return 1000; - case '256-256.png': - $this->assertSame('my resized data', $data); - return 1000; - } - } - $this->fail('file name is wrong:' . $preview->getName()); - }); - - $image = $this->getMockImage(2048, 2048, 'my resized data'); - $this->helper->method('getImage') - ->willReturn($image); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); - - $result = $this->generator->getPreview($file, 100, 100); - $this->assertSame($hasVersion ? 'abc-256-256.png' : '256-256.png', $result->getName()); - $this->assertSame(1000, $result->getSize()); - } - - public function testMigrateOldPreview(): void { - $file = $this->getFile(42, 'myMimeType', false); - - $maxPreview = new Preview(); - $maxPreview->setWidth(1000); - $maxPreview->setHeight(1000); - $maxPreview->setMax(true); - $maxPreview->setSize(1000); - $maxPreview->setCropped(false); - $maxPreview->setStorageId(1); - $maxPreview->setVersion(null); - $maxPreview->setMimeType('image/png'); - - $previewFile = new Preview(); - $previewFile->setWidth(256); - $previewFile->setHeight(256); - $previewFile->setMax(false); - $previewFile->setSize(1000); - $previewFile->setVersion(null); - $previewFile->setCropped(false); - $previewFile->setStorageId(1); - $previewFile->setMimeType('image/png'); - - $this->previewManager->method('isMimeSupported') - ->with($this->equalTo('myMimeType')) - ->willReturn(true); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => []]); - - $this->config->method('getSystemValueString') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $this->config->method('getSystemValueInt') - ->willReturnCallback(function ($key, $default) { - return $default; - }); - - $this->appConfig->method('getValueBool') - ->willReturnCallback(fn ($app, $key, $default) => match ($key) { - ConfigLexicon::ON_DEMAND_PREVIEW_MIGRATION => true, - 'previewMovedDone' => false, - }); - - $this->migrationService->expects($this->exactly(1)) - ->method('migrateFileId') - ->willReturn([$maxPreview, $previewFile]); - - $result = $this->generator->getPreview($file, 100, 100); - $this->assertSame('256-256.png', $result->getName()); - $this->assertSame(1000, $result->getSize()); - } - - public function testInvalidMimeType(): void { - $this->expectException(NotFoundException::class); - - $file = $this->getFile(42, 'invalidType'); - - $this->previewManager->method('isMimeSupported') - ->with('invalidType') - ->willReturn(false); - - $maxPreview = new Preview(); - $maxPreview->setWidth(2048); - $maxPreview->setHeight(2048); - $maxPreview->setMax(true); - $maxPreview->setSize(1000); - $maxPreview->setVersion(null); - $maxPreview->setMimetype('image/png'); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => [ - $maxPreview, - ]]); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); - - $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); - } - - public function testReturnCachedPreviewsWithoutCheckingSupportedMimetype(): void { - $file = $this->getFile(42, 'myMimeType'); - - $maxPreview = new Preview(); - $maxPreview->setWidth(2048); - $maxPreview->setHeight(2048); - $maxPreview->setMax(true); - $maxPreview->setSize(1000); - $maxPreview->setVersion(null); - $maxPreview->setMimeType('image/png'); - - $previewFile = new Preview(); - $previewFile->setWidth(1024); - $previewFile->setHeight(512); - $previewFile->setMax(false); - $previewFile->setSize(1000); - $previewFile->setCropped(true); - $previewFile->setVersion(null); - $previewFile->setMimeType('image/png'); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => [ - $maxPreview, - $previewFile, - ]]); - - $this->previewManager->expects($this->never()) - ->method('isMimeSupported'); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType')); - - $result = $this->generator->getPreview($file, 1024, 512, true, IPreview::MODE_COVER, 'invalidType'); - $this->assertSame('1024-512-crop.png', $result->getName()); - } - - public function testNoProvider(): void { - $file = $this->getFile(42, 'myMimeType'); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => []]); - - $this->previewManager->method('getProviders') - ->willReturn([]); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, 100, 100, false, IPreview::MODE_FILL, null)); - - $this->expectException(NotFoundException::class); - $this->generator->getPreview($file, 100, 100); - } - - private function getMockImage(int $width, int $height, string $data = '') { - $image = $this->createMock(IImage::class); - $image->method('height')->willReturn($width); - $image->method('width')->willReturn($height); - $image->method('valid')->willReturn(true); - $image->method('dataMimeType')->willReturn('image/png'); - $image->method('data')->willReturn($data); - - $image->method('resizeCopy')->willReturnCallback(function ($size) use ($data) { - return $this->getMockImage($size, $size, $data); - }); - $image->method('preciseResizeCopy')->willReturnCallback(function ($width, $height) use ($data) { - return $this->getMockImage($width, $height, $data); - }); - $image->method('cropCopy')->willReturnCallback(function ($x, $y, $width, $height) use ($data) { - return $this->getMockImage($width, $height, $data); - }); - - return $image; - } - - public static function dataSize(): array { - return [ - [1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512], - [1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024], - [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], - [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], - - [1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512], - [1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024], - - [1024, 2048, 250, 1100, true, IPreview::MODE_COVER, 256, 1126], - [1024, 1100, 250, 1100, true, IPreview::MODE_COVER, 250, 1100], - - [1024, 2048, 4096, 2048, false, IPreview::MODE_FILL, 1024, 2048], - [1024, 2048, 4096, 2048, false, IPreview::MODE_COVER, 1024, 2048], - - [2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256], - [2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512], - [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], - [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], - - [2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512], - [2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256], - - [2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512], - [2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512], - - //Test minimum size - [2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32], - [2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32], - [2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64], - [2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64], - ]; - } - - #[DataProvider('dataSize')] - public function testCorrectSize(int $maxX, int $maxY, int $reqX, int $reqY, bool $crop, string $mode, int $expectedX, int $expectedY): void { - $file = $this->getFile(42, 'myMimeType'); - - $this->previewManager->method('isMimeSupported') - ->with($this->equalTo('myMimeType')) - ->willReturn(true); - - $maxPreview = new Preview(); - $maxPreview->setWidth($maxX); - $maxPreview->setHeight($maxY); - $maxPreview->setMax(true); - $maxPreview->setSize(1000); - $maxPreview->setVersion(null); - $maxPreview->setMimeType('image/png'); - - $this->assertSame($maxPreview->getName(), $maxX . '-' . $maxY . '-max.png'); - $this->assertSame($maxPreview->getMimeType(), 'image/png'); - - $this->previewMapper->method('getAvailablePreviews') - ->with($this->equalTo([42])) - ->willReturn([42 => [ - $maxPreview, - ]]); - - $filename = $expectedX . '-' . $expectedY; - if ($crop) { - $filename .= '-crop'; - } - $filename .= '.png'; - - $image = $this->getMockImage($maxX, $maxY); - $this->helper->method('getImage') - ->willReturn($image); - - $this->previewMapper->method('insert') - ->willReturnCallback(function (Preview $preview) use ($filename): Preview { - $this->assertSame($preview->getName(), $filename); - return $preview; - }); - - $this->previewMapper->method('update') - ->willReturnCallback(fn (Preview $preview): Preview => $preview); - - $this->storageFactory->method('writePreview') - ->willReturn(1000); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new BeforePreviewFetchedEvent($file, $reqX, $reqY, $crop, $mode, null)); - - $result = $this->generator->getPreview($file, $reqX, $reqY, $crop, $mode); - if ($expectedX === $maxX && $expectedY === $maxY) { - $this->assertSame($maxPreview->getName(), $result->getName()); - } else { - $this->assertSame($filename, $result->getName()); - } - } - - public function testUnreadbleFile(): void { - $file = $this->createMock(File::class); - $file->method('isReadable') - ->willReturn(false); - - $this->expectException(NotFoundException::class); - - $this->generator->getPreview($file, 100, 100, false); - } -} --- a/tests/lib/Preview/HEICTest.php +++ /dev/null @@ -1,47 +0,0 @@ -markTestSkipped('ImageMagick is not HEIC aware. Skipping tests'); - } - - $fileName = 'testimage.heic'; - $sourcePath = \OC::$SERVERROOT . '/tests/data/' . $fileName; - - // queryFormats() only reports that the HEIC coder is registered, not that - // ImageMagick can actually decode a HEIC file: the libheif delegate may be - // missing or the coder may be disabled by ImageMagick's policy.xml. In that - // case decoding throws, the provider returns null and the tests fail instead - // of being skipped. Verify a real decode before running the tests. - try { - (new \Imagick())->readImage($sourcePath . '[0]'); - } catch (\ImagickException $e) { - $this->markTestSkipped('ImageMagick cannot decode HEIC in this environment: ' . $e->getMessage() . '. Skipping tests'); - } - - parent::setUp(); - - $this->imgPath = $this->prepareTestFile($fileName, $sourcePath); - $this->width = 1680; - $this->height = 1050; - $this->provider = new HEIC; - } -} --- a/tests/lib/Preview/ImageTest.php +++ /dev/null @@ -1,31 +0,0 @@ -imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - $this->width = 1680; - $this->height = 1050; - $this->provider = new JPEG(); - } -} --- a/tests/lib/Preview/MP3Test.php +++ /dev/null @@ -1,31 +0,0 @@ -imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - $this->width = 200; - $this->height = 200; - $this->provider = new MP3; - } -} --- a/tests/lib/Preview/MovieBrokenStuckFfmpegTest.php +++ /dev/null @@ -1,20 +0,0 @@ -findBinaryPath('ffmpeg'); - - if (is_string($movieBinary)) { - parent::setUp(); - - $this->imgPath = $this->prepareTestFile($this->fileName, \OC::$SERVERROOT . '/tests/data/' . $this->fileName); - $this->provider = new Movie(['movieBinary' => $movieBinary]); - } else { - $this->markTestSkipped('No Movie provider present'); - } - } -} --- a/tests/lib/Preview/MovieTestRemoteFile.php +++ /dev/null @@ -1,72 +0,0 @@ -findBinaryPath('ffmpeg'); - if (is_string($movieBinary)) { - parent::setUp(); - $this->imgPath = $this->prepareTestFile($this->fileName, \OC::$SERVERROOT . '/tests/data/' . $this->fileName); - $this->provider = new Movie(['movieBinary' => $movieBinary]); - } else { - $this->markTestSkipped('No Movie provider present'); - } - } - - #[\Override] - #[\PHPUnit\Framework\Attributes\DataProvider('dimensionsDataProvider')] - public function testGetThumbnail($widthAdjustment, $heightAdjustment): void { - $ratio = round($this->width / $this->height, 2); - $this->maxWidth = $this->width - $widthAdjustment; - $this->maxHeight = $this->height - $heightAdjustment; - $file = new File(Server::get(IRootFolder::class), $this->rootView, $this->imgPath); - // Create mock remote file to be passed - $remoteStorage = $this->createMock(Storage::class); - $remoteStorage->method('isLocal') - ->willReturn(false); - $mockRemoteVideo = $this->createMock(File::class); - $mockRemoteVideo->method('getStorage') - ->willReturn($remoteStorage); - $mockRemoteVideo->method('getSize') - ->willReturn($file->getSize()); - $mockRemoteVideo->method('fopen') - ->with('r') - ->willreturn($file->fopen('r')); - $remotePreview = $this->provider->getThumbnail($mockRemoteVideo, $this->maxWidth, $this->maxHeight, $this->scalingUp); - $localPreview = $this->provider->getThumbnail($file, $this->maxWidth, $this->maxHeight, $this->scalingUp); - $this->assertNotFalse($remotePreview); - $this->assertTrue($remotePreview->valid()); - $this->assertEquals($remotePreview->data(), $localPreview->data()); - } -} --- a/tests/lib/Preview/OfficeTest.php +++ /dev/null @@ -1,41 +0,0 @@ -findBinaryPath('libreoffice'); - $openofficeBinary = $libreofficeBinary === false ? $binaryFinder->findBinaryPath('openoffice') : false; - - if ($libreofficeBinary !== false || $openofficeBinary !== false) { - parent::setUp(); - - $fileName = 'testimage.odt'; - $this->imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - $this->width = 595; - $this->height = 842; - $this->provider = new OpenDocument; - } else { - $this->markTestSkipped('No Office provider present'); - } - } -} --- a/tests/lib/Preview/PostscriptTest.php +++ /dev/null @@ -1,39 +0,0 @@ -markTestSkipped('Imagick does not support postscript.'); - } - if (Server::get(BinaryFinder::class)->findBinaryPath('gs') === false) { - // Imagick forwards postscript rendering to Ghostscript but does not report this in queryFormats - $this->markTestSkipped('Ghostscript is not installed.'); - } - - parent::setUp(); - - $fileName = 'testimage.eps'; - $this->imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - $this->width = 2400; - $this->height = 1707; - $this->provider = new Postscript; - } -} --- a/tests/lib/Preview/PreviewMapperTest.php +++ /dev/null @@ -1,176 +0,0 @@ -previewMapper = Server::get(PreviewMapper::class); - $this->connection = Server::get(IDBConnection::class); - $this->snowflake = Server::get(ISnowflakeGenerator::class); - $this->mimeTypeLoader = Server::get(IMimeTypeLoader::class); - - $qb = $this->connection->getQueryBuilder(); - $qb->delete('preview_locations')->executeStatement(); - - $qb = $this->connection->getQueryBuilder(); - $qb->delete('preview_versions')->executeStatement(); - - $qb = $this->connection->getQueryBuilder(); - $qb->delete('previews')->executeStatement(); - } - - #[\Override] - public function tearDown(): void { - $this->previewMapper->deleteAll(); - parent::tearDown(); - } - - public function testGetAvailablePreviews(): void { - // Empty - $this->assertEquals([], $this->previewMapper->getAvailablePreviews([])); - - // No preview available - $this->assertEquals([42 => []], $this->previewMapper->getAvailablePreviews([42])); - - $this->createPreviewForFileId(42); - $previews = $this->previewMapper->getAvailablePreviews([42]); - $this->assertNotEmpty($previews[42]); - $this->assertNull($previews[42][0]->getLocationId()); - $this->assertNull($previews[42][0]->getBucketName()); - $this->assertNull($previews[42][0]->getObjectStoreName()); - - $this->createPreviewForFileId(43, 2); - $previews = $this->previewMapper->getAvailablePreviews([43]); - $this->assertNotEmpty($previews[43]); - $this->assertEquals('preview-2', $previews[43][0]->getBucketName()); - $this->assertEquals('default', $previews[43][0]->getObjectStoreName()); - } - - private function createPreviewForFileId(int $fileId, ?int $bucket = null, int $size = 100, ?string $version = null, bool $cropped = true): string { - $locationId = null; - if ($bucket) { - $qb = $this->connection->getQueryBuilder(); - $locationId = $this->snowflake->nextId(); - $qb->insert('preview_locations') - ->values([ - 'id' => $locationId, - 'bucket_name' => $qb->createNamedParameter('preview-' . $bucket), - 'object_store_name' => $qb->createNamedParameter('default'), - ]); - $qb->executeStatement(); - } - $preview = new Preview(); - $preview->generateId(); - $preview->setFileId($fileId); - $preview->setStorageId(1); - $preview->setCropped($cropped); - $preview->setMax(true); - $preview->setWidth($size); - $preview->setSourceMimeType('image/jpeg'); - $preview->setHeight($size); - $preview->setSize(100); - $preview->setMtime(time()); - $preview->setMimetype('image/jpeg'); - $preview->setEtag('abcdefg'); - $preview->setVersion($version); - - if ($locationId !== null) { - $preview->setLocationId($locationId); - } - $this->previewMapper->insert($preview); - - return $preview->id; - } - - /** - * The previews table is joined with preview_versions, which also has a - * file_id column, so the condition has to be qualified with the alias. - */ - public function testGetByFileId(): void { - $fileId = 4242; - $this->createPreviewForFileId($fileId); - $this->createPreviewForFileId($fileId, size: 256); - $this->createPreviewForFileId(4243); - - $previews = iterator_to_array($this->previewMapper->getByFileId($fileId)); - - $this->assertCount(2, $previews); - foreach ($previews as $preview) { - $this->assertSame($fileId, $preview->getFileId()); - } - } - - /** - * Same ambiguity, reached through the specification lookup that - * Generator::savePreview() uses to recover from a unique constraint - * violation. It passes the cropped flag as a PHP bool, and false is the - * common case, so both values have to be covered. - */ - #[\PHPUnit\Framework\Attributes\TestWith([false])] - #[\PHPUnit\Framework\Attributes\TestWith([true])] - public function testGetPreviewForSpecification(bool $cropped): void { - $fileId = 4244; - $previewId = $this->createPreviewForFileId($fileId, cropped: $cropped); - - $preview = $this->previewMapper->getPreviewForSpecification([ - 'file_id' => $fileId, - 'width' => 100, - 'height' => 100, - 'mimetype_id' => $this->mimeTypeLoader->getId('image/jpeg'), - 'cropped' => $cropped, - 'version_id' => '-1', - ]); - - $this->assertNotNull($preview); - $this->assertEquals($previewId, $preview->getId()); - } - - /** - * version lives in the joined preview_versions table, so it has to keep - * resolving to that alias rather than to the previews table. - */ - public function testGetPreviewForSpecificationOnJoinedColumn(): void { - $fileId = 4245; - $previewId = $this->createPreviewForFileId($fileId, version: '1000'); - - $preview = $this->previewMapper->getPreviewForSpecification([ - 'file_id' => $fileId, - 'version' => '1000', - ]); - - $this->assertNotNull($preview); - $this->assertEquals($previewId, $preview->getId()); - } - - public function testLargeIdInsertRetrieve(): void { - $fileId = PHP_INT_MAX; - $originalPreviewId = $this->createPreviewForFileId($fileId); - - $dbPreview = $this->previewMapper->getAvailablePreviews([$fileId])[$fileId][0]; - $this->assertEquals($originalPreviewId, $dbPreview->id); - $this->assertEquals($fileId, $dbPreview->getFileId()); - } -} --- a/tests/lib/Preview/PreviewMigrationJobTest.php +++ /dev/null @@ -1,346 +0,0 @@ -previewAppData = Server::get(IAppDataFactory::class)->get('preview'); - $this->previewMapper = Server::get(PreviewMapper::class); - $this->config = Server::get(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->appConfig->expects($this->any()) - ->method('getValueBool') - ->willReturn(false); - $this->appConfig->expects($this->any()) - ->method('setValueBool') - ->willReturn(true); - $this->storageFactory = Server::get(StorageFactory::class); - $this->previewService = Server::get(PreviewService::class); - $this->db = Server::get(IDBConnection::class); - - $qb = $this->db->getQueryBuilder(); - $qb->delete('filecache') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5))) - ->executeStatement(); - - $qb = $this->db->getQueryBuilder(); - $qb->insert('filecache') - ->values([ - 'fileid' => $qb->createNamedParameter(5), - 'storage' => $qb->createNamedParameter(1), - 'path' => $qb->createNamedParameter('test/abc'), - 'path_hash' => $qb->createNamedParameter(md5('test')), - 'parent' => $qb->createNamedParameter(0), - 'name' => $qb->createNamedParameter('abc'), - 'mimetype' => $qb->createNamedParameter(42), - 'size' => $qb->createNamedParameter(1000), - 'mtime' => $qb->createNamedParameter(1000), - 'storage_mtime' => $qb->createNamedParameter(1000), - 'encrypted' => $qb->createNamedParameter(0), - 'unencrypted_size' => $qb->createNamedParameter(0), - 'etag' => $qb->createNamedParameter('abcdefg'), - 'permissions' => $qb->createNamedParameter(0), - 'checksum' => $qb->createNamedParameter('abcdefg'), - ])->executeStatement(); - - $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class); - $this->mimeTypeDetector->method('detectPath')->willReturn('image/png'); - $this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class); - $this->mimeTypeLoader->method('getId')->with('image/png')->willReturn(42); - $this->mimeTypeLoader->method('getMimetypeById')->with(42)->willReturn('image/png'); - $this->logger = $this->createMock(LoggerInterface::class); - } - - #[\Override] - public function tearDown(): void { - foreach ($this->previewAppData->getDirectoryListing() as $folder) { - $folder->delete(); - } - $this->previewService->deleteAll(); - - $qb = $this->db->getQueryBuilder(); - $qb->delete('filecache') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5))) - ->executeStatement(); - parent::tearDown(); - } - - #[TestDox('Test the migration from the legacy flat hierarchy to the new database format')] - public function testMigrationLegacyPath(): void { - $folder = $this->previewAppData->newFolder('5'); - $folder->newFile('64-64-crop.jpg', 'abcdefg'); - $folder->newFile('128-128-crop.png', 'abcdefg'); - $this->assertEquals(1, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(2, count($folder->getDirectoryListing())); - $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - - $job = $this->createJob(); - $this->invokePrivate($job, 'run', [[]]); - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - } - - private static function getInternalFolder(string $name): string { - return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name; - } - - private function createJob(): PreviewMigrationJob { - return new PreviewMigrationJob( - Server::get(ITimeFactory::class), - $this->appConfig, - $this->config, - Server::get(IRootFolder::class), - new PreviewMigrationService( - $this->config, - Server::get(IRootFolder::class), - $this->logger, - $this->mimeTypeDetector, - $this->mimeTypeLoader, - Server::get(IDBConnection::class), - $this->previewMapper, - $this->storageFactory, - Server::get(IAppDataFactory::class), - ), - $this->logger, - ); - } - - private function insertFilecacheRow(string $path, string $etag): int { - $qb = $this->db->getQueryBuilder(); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->createNamedParameter(1), - 'path' => $qb->createNamedParameter($path), - 'path_hash' => $qb->createNamedParameter(md5($path)), - 'parent' => $qb->createNamedParameter(0), - 'name' => $qb->createNamedParameter(basename($path)), - 'mimetype' => $qb->createNamedParameter(42), - 'size' => $qb->createNamedParameter(1000), - 'mtime' => $qb->createNamedParameter(1000), - 'storage_mtime' => $qb->createNamedParameter(1000), - 'encrypted' => $qb->createNamedParameter(0), - 'unencrypted_size' => $qb->createNamedParameter(0), - 'etag' => $qb->createNamedParameter($etag), - 'permissions' => $qb->createNamedParameter(0), - 'checksum' => $qb->createNamedParameter($etag), - ])->executeStatement(); - return $qb->getLastInsertId(); - } - - private function deleteFilecacheRow(int $fileId): void { - $qb = $this->db->getQueryBuilder(); - $qb->delete('filecache') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId))) - ->executeStatement(); - } - - #[TestDox('A single run must migrate multiple different fileids in one pass, mixing both folder structures')] - public function testMigrationMultipleFileIds(): void { - $otherFileId = $this->insertFilecacheRow('test/def', 'xyz123'); - - try { - $flatFolder = $this->previewAppData->newFolder('5'); - $flatFolder->newFile('64-64-crop.jpg', 'abcdefg'); - - $hierFolder = $this->previewAppData->newFolder(self::getInternalFolder((string)$otherFileId)); - $hierFolder->newFile('128-128.png', 'abcdefg'); - - $this->invokePrivate($this->createJob(), 'run', [[]]); - - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(1, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - $this->assertEquals(1, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile($otherFileId)))); - } finally { - $this->deleteFilecacheRow($otherFileId); - } - } - - #[TestDox('Re-migrating a preview that already exists must skip the insert and still clean up the stale filecache row')] - public function testMigrationSkipsDuplicatePreview(): void { - $folder = $this->previewAppData->newFolder('5'); - $folder->newFile('64-64-crop.jpg', 'abcdefg'); - - // Simulate a preview that was already migrated by an earlier, interrupted run: - // same fileid/width/height/mimetype/cropped/version as what this migration - // would produce for the file created above, so the insert below hits the - // `previews_file_uniq_idx` unique constraint. - $existing = Preview::fromPath('5/64-64-crop.jpg', $this->mimeTypeDetector); - $this->assertNotFalse($existing); - $existing->setFileId(5); - $existing->setStorageId(1); - $existing->setSourceMimeType('image/png'); - $existing->setEtag('abcdefg'); - $existing->setSize(7); - $existing->setMtime(1000); - $existing->setEncrypted(false); - $existing->generateId(); - $this->previewMapper->insert($existing); - - $this->invokePrivate($this->createJob(), 'run', [[]]); - - // No duplicate preview row was inserted, but the legacy folder and its stale - // filecache row were still cleaned up. - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(1, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - } - - #[TestDox('An orphaned preview folder whose source file no longer exists must be deleted, not migrated')] - public function testMigrationDeletesOrphanedPreview(): void { - $orphanFileId = 9999998; - $folder = $this->previewAppData->newFolder((string)$orphanFileId); - $folder->newFile('64-64-crop.jpg', 'abcdefg'); - - $this->invokePrivate($this->createJob(), 'run', [[]]); - - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile($orphanFileId)))); - } - - #[TestDox('run() must complete without error when there is nothing to migrate')] - public function testMigrationWithoutAnyPreviews(): void { - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - - $this->invokePrivate($this->createJob(), 'run', [[]]); - - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - } - - #[TestDox("Test the migration from the 'new' nested hierarchy to the database format")] - public function testMigrationPath(): void { - $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5)); - $folder->newFile('64-64-crop.jpg', 'abcdefg'); - $folder->newFile('128-128-crop.png', 'abcdefg'); - - $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5)); - $this->assertEquals(2, count($folder->getDirectoryListing())); - $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - - $job = new PreviewMigrationJob( - Server::get(ITimeFactory::class), - $this->appConfig, - $this->config, - Server::get(IRootFolder::class), - new PreviewMigrationService( - $this->config, - Server::get(IRootFolder::class), - $this->logger, - $this->mimeTypeDetector, - $this->mimeTypeLoader, - Server::get(IDBConnection::class), - $this->previewMapper, - $this->storageFactory, - Server::get(IAppDataFactory::class), - ), - $this->logger, - ); - $this->invokePrivate($job, 'run', [[]]); - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - $this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - } - - #[TestDox("Test the migration from the 'new' nested hierarchy to the database format")] - public function testMigrationPathWithVersion(): void { - $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5)); - // No version - $folder->newFile('128-128-crop.png', 'abcdefg'); - $folder->newFile('256-256-max.png', 'abcdefg'); - $folder->newFile('128-128.png', 'abcdefg'); - - // Version 1000 - $folder->newFile('1000-128-128-crop.png', 'abcdefg'); - $folder->newFile('1000-256-256-max.png', 'abcdefg'); - $folder->newFile('1000-128-128.png', 'abcdefg'); - - // Version 1001 - $folder->newFile('1001-128-128-crop.png', 'abcdefg'); - $folder->newFile('1001-256-256-max.png', 'abcdefg'); - $folder->newFile('1001-128-128.png', 'abcdefg'); - - $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5)); - $this->assertEquals(9, count($folder->getDirectoryListing())); - $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - - $job = new PreviewMigrationJob( - Server::get(ITimeFactory::class), - $this->appConfig, - $this->config, - Server::get(IRootFolder::class), - new PreviewMigrationService( - $this->config, - Server::get(IRootFolder::class), - $this->logger, - $this->mimeTypeDetector, - $this->mimeTypeLoader, - Server::get(IDBConnection::class), - $this->previewMapper, - $this->storageFactory, - Server::get(IAppDataFactory::class), - ), - $this->logger, - ); - $this->invokePrivate($job, 'run', [[]]); - $previews = iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)); - $this->assertEquals(9, count($previews)); - $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); - - $nameVersionMapping = []; - foreach ($previews as $preview) { - $nameVersionMapping[$preview->getName($this->mimeTypeLoader)] = $preview->getVersion(); - } - - $this->assertEquals([ - '1000-128-128-crop.png' => 1000, - '1000-128-128.png' => 1000, - '1000-256-256-max.png' => 1000, - '1001-128-128-crop.png' => 1001, - '1001-128-128.png' => 1001, - '1001-256-256-max.png' => 1001, - '128-128-crop.png' => null, - '128-128.png' => null, - '256-256-max.png' => null, - ], $nameVersionMapping); - } -} --- a/tests/lib/Preview/PreviewServiceTest.php +++ /dev/null @@ -1,62 +0,0 @@ -previewService = Server::get(PreviewService::class); - $this->previewMapper = Server::get(PreviewMapper::class); - $this->previewService->deleteAll(); - } - - #[\Override] - public function tearDown(): void { - $this->previewService->deleteAll(); - parent::tearDown(); - } - - public function testGetAvailableFileIds(): void { - foreach (range(1, 20) as $i) { - $preview = new Preview(); - $preview->generateId(); - $preview->setFileId($i % 10); - $preview->setStorageId(1); - $preview->setWidth($i); - $preview->setHeight($i); - $preview->setMax(true); - $preview->setSourceMimeType('image/jpeg'); - $preview->setCropped(true); - $preview->setEncrypted(false); - $preview->setMimetype('image/jpeg'); - $preview->setEtag('abc'); - $preview->setMtime((new \DateTime())->getTimestamp()); - $preview->setSize(0); - $this->previewMapper->insert($preview); - } - - $files = iterator_to_array($this->previewService->getAvailableFileIds()); - $this->assertCount(1, $files); - $this->assertCount(10, $files[0]['fileIds']); - } -} --- a/tests/lib/Preview/Provider.php +++ /dev/null @@ -1,178 +0,0 @@ -clearBackends(); - $backend = new \Test\Util\User\Dummy(); - $userManager->registerBackend($backend); - - $userId = $this->getUniqueID(); - $backend->createUser($userId, $userId); - $this->loginAsUser($userId); - - $this->storage = new Temporary([]); - Filesystem::mount($this->storage, [], '/' . $userId . '/'); - - $this->rootView = new View(''); - $this->rootView->mkdir('/' . $userId); - $this->rootView->mkdir('/' . $userId . '/files'); - - $this->userId = $userId; - } - - #[\Override] - protected function tearDown(): void { - $this->logout(); - - parent::tearDown(); - } - - public static function dimensionsDataProvider() { - return [ - [-rand(5, 100), -rand(5, 100)], - [rand(5, 100), rand(5, 100)], - [-rand(5, 100), rand(5, 100)], - [rand(5, 100), -rand(5, 100)], - ]; - } - - /** - * Launches all the tests we have - * - * - * @param int $widthAdjustment - * @param int $heightAdjustment - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dimensionsDataProvider')] - #[\PHPUnit\Framework\Attributes\RequiresPhpExtension('imagick')] - public function testGetThumbnail($widthAdjustment, $heightAdjustment): void { - $ratio = round($this->width / $this->height, 2); - $this->maxWidth = $this->width - $widthAdjustment; - $this->maxHeight = $this->height - $heightAdjustment; - - // Testing code - /*print_r("w $this->width "); - print_r("h $this->height "); - print_r("r $ratio ");*/ - - $preview = $this->getPreview($this->provider); - // The TXT provider uses the max dimensions to create its canvas, - // so the ratio will always be the one of the max dimension canvas - if (!$this->provider instanceof TXT) { - $this->doesRatioMatch($preview, $ratio); - } - $this->doesPreviewFit($preview); - } - - /** - * Adds the test file to the filesystem - * - * @param string $fileName name of the file to create - * @param string $fileContent path to file to use for test - * - * @return string - */ - protected function prepareTestFile($fileName, $fileContent) { - $imgData = file_get_contents($fileContent); - $imgPath = '/' . $this->userId . '/files/' . $fileName; - $this->rootView->file_put_contents($imgPath, $imgData); - - $scanner = $this->storage->getScanner(); - $scanner->scan(''); - - return $imgPath; - } - - /** - * Retrieves a max size thumbnail can be created - * - * @return bool|IImage - */ - private function getPreview(ProviderV2 $provider) { - $file = new File(Server::get(IRootFolder::class), $this->rootView, $this->imgPath); - $preview = $provider->getThumbnail($file, $this->maxWidth, $this->maxHeight, $this->scalingUp); - - $this->assertNotEquals(false, $preview); - $this->assertEquals(true, $preview->valid()); - - return $preview; - } - - /** - * Checks if the preview ratio matches the original ratio - * - * @param IImage $preview - * @param int $ratio - */ - private function doesRatioMatch($preview, $ratio) { - $previewRatio = round($preview->width() / $preview->height(), 2); - $this->assertEquals($ratio, $previewRatio); - } - - /** - * Tests if a max size preview of smaller dimensions can be created - * - * @param IImage $preview - */ - private function doesPreviewFit($preview) { - $maxDimRatio = round($this->maxWidth / $this->maxHeight, 2); - $previewRatio = round($preview->width() / $preview->height(), 2); - - // Testing code - /*print_r("mw $this->maxWidth "); - print_r("mh $this->maxHeight "); - print_r("mr $maxDimRatio "); - $pw = $preview->width(); - $ph = $preview->height(); - print_r("pw $pw "); - print_r("ph $ph "); - print_r("pr $previewRatio ");*/ - - if ($maxDimRatio < $previewRatio) { - $this->assertLessThanOrEqual($this->maxWidth, $preview->width()); - $this->assertLessThan($this->maxHeight, $preview->height()); - } elseif ($maxDimRatio > $previewRatio) { - $this->assertLessThan($this->maxWidth, $preview->width()); - $this->assertLessThanOrEqual($this->maxHeight, $preview->height()); - } else { // Original had to be resized - $this->assertLessThanOrEqual($this->maxWidth, $preview->width()); - $this->assertLessThanOrEqual($this->maxHeight, $preview->height()); - } - } -} --- a/tests/lib/Preview/SVGTest.php +++ /dev/null @@ -1,110 +0,0 @@ -queryFormats('SVG')) === 1) { - parent::setUp(); - - $fileName = 'testimagelarge.svg'; - $this->imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - $this->width = 3000; - $this->height = 2000; - $this->provider = new SVG; - } else { - $this->markTestSkipped('No SVG provider present'); - } - } - - public static function dataGetThumbnailSVGHref(): array { - return [ - ['href'], - [' href'], - ["\nhref"], - ['xlink:href'], - [' xlink:href'], - ["\nxlink:href"], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetThumbnailSVGHref')] - #[\PHPUnit\Framework\Attributes\RequiresPhpExtension('imagick')] - public function testGetThumbnailSVGHref(string $content): void { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, ' - -'); - rewind($handle); - - $file = $this->createMock(File::class); - $file->method('fopen') - ->willReturn($handle); - - self::assertNull($this->provider->getThumbnail($file, 512, 512)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetThumbnailSVGHrefNamespace')] - #[\PHPUnit\Framework\Attributes\RequiresPhpExtension('imagick')] - public function testGetThumbnailSvgHrefNamespace(string $namespace): void { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, ' - -'); - rewind($handle); - - $file = $this->createMock(File::class); - $file->method('fopen') - ->willReturn($handle); - - self::assertNull($this->provider->getThumbnail($file, 512, 512)); - } - - public static function dataGetThumbnailSVGHrefNamespace(): array { - return [ - ['xlink'], - ['foo'], - ['_foo'], - ['fo_12'], - ['foo-bar'], - ['Fo_B1-ar'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetThumbnailSvgEncoded')] - #[\PHPUnit\Framework\Attributes\RequiresPhpExtension('imagick')] - public function testGetThumbnailSvgEncoded(string $content): void { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, $content); - rewind($handle); - - $file = $this->createMock(File::class); - $file->method('fopen') - ->willReturn($handle); - self::assertNull($this->provider->getThumbnail($file, 512, 512)); - } - - public static function dataGetThumbnailSvgEncoded(): array { - return [ - 'iso-2022-jp' => ["\n\n\n"], - ]; - } -} --- a/tests/lib/Preview/Storage/LocalPreviewStorageTest.php +++ /dev/null @@ -1,331 +0,0 @@ -tmpDir = sys_get_temp_dir() . '/nc_preview_test_' . uniqid(); - mkdir($this->tmpDir, 0777, true); - - $this->config = $this->createMock(IConfig::class); - $this->config->method('getSystemValueString') - ->with('datadirectory', $this->anything()) - ->willReturn($this->tmpDir); - - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->rootFolder->method('getAppDataDirectoryName')->willReturn('appdata_test'); - - $this->previewMapper = $this->createMock(PreviewMapper::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->connection = $this->createMock(IDBConnection::class); - $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->mimeTypeLoader = $this->createMock(IMimeTypeLoader::class); - - $this->mimeTypeDetector->method('detectPath')->willReturn('image/jpeg'); - $this->mimeTypeLoader->method('getMimetypeById')->willReturn('image/jpeg'); - - $this->storage = new LocalPreviewStorage( - $this->config, - $this->previewMapper, - $this->appConfig, - $this->connection, - $this->mimeTypeDetector, - $this->logger, - $this->mimeTypeLoader, - $this->rootFolder, - ); - } - - #[\Override] - protected function tearDown(): void { - $this->removeDir($this->tmpDir); - parent::tearDown(); - } - - private function removeDir(string $path): void { - if (!is_dir($path)) { - return; - } - foreach (scandir($path) as $entry) { - if ($entry === '.' || $entry === '..') { - continue; - } - $full = $path . '/' . $entry; - is_dir($full) ? $this->removeDir($full) : unlink($full); - } - rmdir($path); - } - - /** - * Create a preview file in the legacy flat directory format so the scan - * code will attempt to move it to the new nested path. - * Returns the absolute path to the created file. - */ - private function createFlatPreviewFile(int $fileId, string $previewName): string { - $dir = $this->tmpDir . '/appdata_test/preview/' . $fileId; - mkdir($dir, 0777, true); - $path = $dir . '/' . $previewName; - file_put_contents($path, 'fake preview data'); - return $path; - } - - /** - * Build a mock IQueryBuilder chain and configure it to return the given - * rows from executeQuery()->fetchAssociative(). - */ - private function buildQueryBuilderMock(array $rows): IQueryBuilder&MockObject { - $exprMock = $this->createMock(IExpressionBuilder::class); - $exprMock->method('in')->willReturn('1=1'); - - $callIndex = 0; - $resultMock = $this->createMock(IResult::class); - $resultMock->method('fetchAssociative') - ->willReturnCallback(static function () use ($rows, &$callIndex) { - return $rows[$callIndex++] ?? false; - }); - - $qbMock = $this->createMock(ITypedQueryBuilder::class); - $qbMock->method('selectColumns')->willReturnSelf(); - $qbMock->method('from')->willReturnSelf(); - $qbMock->method('where')->willReturnSelf(); - $qbMock->method('andWhere')->willReturnSelf(); - $qbMock->method('runAcrossAllShards')->willReturnSelf(); - $qbMock->method('executeQuery')->willReturn($resultMock); - $qbMock->method('expr')->willReturn($exprMock); - $qbMock->method('createNamedParameter')->willReturn(':param'); - - return $qbMock; - } - - /** - * Configure appConfig so migration is considered done, meaning - * checkForFileCache = false (no legacy path-hash queries). - */ - private function setMigrationDone(): void { - $this->appConfig->method('getValueBool') - ->with('core', 'previewMovedDone') - ->willReturn(true); - } - - /** - * When fewer previews than SCAN_BATCH_SIZE exist, scan() must still open - * and commit a transaction for the tail batch. - * - * Before the fix: commit() was never called for the tail batch, leaving the - * transaction open. - */ - public function testScanCommitsFinalBatch(): void { - $this->setMigrationDone(); - $this->createFlatPreviewFile(self::FILE_ID, '1024-1024.jpg'); - - $filecacheRow = [ - 'fileid' => (string)self::FILE_ID, - 'storage' => '42', - 'etag' => 'abc', - 'mimetype' => '6', - ]; - $this->connection->method('getTypedQueryBuilder') - ->willReturn($this->buildQueryBuilderMock([$filecacheRow])); - - // Outer batch transaction + one inner savepoint for the insert. - $this->connection->expects($this->exactly(2))->method('beginTransaction'); - $this->connection->expects($this->exactly(2))->method('commit'); - $this->connection->expects($this->never())->method('rollBack'); - - $count = $this->storage->scan(); - - $this->assertSame(1, $count); - } - - /** - * When previewMapper->insert() throws a unique-constraint violation, scan() - * must roll back only the inner savepoint and continue, leaving the outer - * transaction intact so its final commit() succeeds. - * - * Before the fix: the plain catch swallowed the PHP exception but left the - * PostgreSQL transaction in an aborted state, so all subsequent queries - * (including commit()) failed with "current transaction is aborted". - */ - public function testScanHandlesUniqueConstraintViolation(): void { - $this->setMigrationDone(); - $this->createFlatPreviewFile(self::FILE_ID, '1024-1024.jpg'); - - $filecacheRow = [ - 'fileid' => (string)self::FILE_ID, - 'storage' => '42', - 'etag' => 'abc', - 'mimetype' => '6', - ]; - $this->connection->method('getTypedQueryBuilder') - ->willReturn($this->buildQueryBuilderMock([$filecacheRow])); - - $ucvException = new class('duplicate key') extends DBException { - #[\Override] - public function getReason(): int { - return self::REASON_UNIQUE_CONSTRAINT_VIOLATION; - } - }; - $this->previewMapper->method('insert')->willThrowException($ucvException); - - // Inner savepoint is rolled back; outer batch transaction is committed. - $this->connection->expects($this->exactly(2))->method('beginTransaction'); - $this->connection->expects($this->once())->method('commit'); - $this->connection->expects($this->exactly(1))->method('rollBack'); - - $count = $this->storage->scan(); - - // Even when the DB row already exists the preview file still counts. - $this->assertSame(1, $count); - } - - /** - * A non-UCE exception from previewMapper->insert() must be re-thrown after - * rolling back both the inner savepoint and the outer batch transaction. - */ - public function testScanRethrowsUnexpectedInsertException(): void { - $this->setMigrationDone(); - $this->createFlatPreviewFile(self::FILE_ID, '1024-1024.jpg'); - - $filecacheRow = [ - 'fileid' => (string)self::FILE_ID, - 'storage' => '42', - 'etag' => 'abc', - 'mimetype' => '6', - ]; - $this->connection->method('getTypedQueryBuilder') - ->willReturn($this->buildQueryBuilderMock([$filecacheRow])); - - $driverException = new class('some driver error') extends DBException { - #[\Override] - public function getReason(): int { - return self::REASON_DRIVER; - } - }; - $this->previewMapper->method('insert')->willThrowException($driverException); - - // Inner savepoint rolled back; outer batch also rolled back via rethrow. - $this->connection->expects($this->exactly(2))->method('beginTransaction'); - $this->connection->expects($this->never())->method('commit'); - $this->connection->expects($this->exactly(2))->method('rollBack'); - - $this->expectException(DBException::class); - $this->storage->scan(); - } - - /** - * fetchFilecacheByFileIds() must return a row for every file ID returned by - * the query, not just one. Before the fix, the foreach loop iterated over - * the key-value pairs of the first row, so previews for all but the first - * file ID were silently deleted (filecache row not found → unlink). - */ - public function testScanFetchesAllFilecacheRows(): void { - $this->setMigrationDone(); - - $fileIds = [1, 2, 3]; - foreach ($fileIds as $id) { - $this->createFlatPreviewFile($id, '1024-1024.jpg'); - } - - $filecacheRows = array_map(static fn (int $id) => [ - 'fileid' => (string)$id, - 'storage' => '42', - 'etag' => 'abc', - 'mimetype' => '6', - ], $fileIds); - - $this->connection->method('getTypedQueryBuilder') - ->willReturn($this->buildQueryBuilderMock($filecacheRows)); - - // 1 outer batch transaction + 3 inner savepoints (one per preview insert). - $this->connection->expects($this->exactly(4))->method('beginTransaction'); - $this->connection->expects($this->exactly(4))->method('commit'); - $this->connection->expects($this->never())->method('rollBack'); - - $count = $this->storage->scan(); - - $this->assertSame(3, $count); - } - - private function makePreview(int $fileId = self::FILE_ID): Preview { - $preview = new Preview(); - $preview->setFileId($fileId); - $preview->setWidth(1024); - $preview->setHeight(768); - $preview->setCropped(false); - $preview->setMax(true); - $preview->setMimetype('image/jpeg'); - - return $preview; - } - - /** - * Losing the insert race must not remove the local file: the path is derived - * from the preview specification, so it is the same file the preview that - * won the race now points at. - */ - public function testDeleteUnreferencedPreviewKeepsTheSharedFile(): void { - $preview = $this->makePreview(); - $this->storage->writePreview($preview, 'preview data'); - - $this->storage->deleteUnreferencedPreview($preview); - - $this->assertSame('preview data', stream_get_contents($this->storage->readPreview($preview))); - } - - /** - * Deleting a preview for real still has to remove the file. - */ - public function testDeletePreviewRemovesTheFile(): void { - $preview = $this->makePreview(); - $this->storage->writePreview($preview, 'preview data'); - - $this->storage->deletePreview($preview); - - $this->expectException(NotFoundException::class); - $this->storage->readPreview($preview); - } -} --- a/tests/lib/Preview/TXTTest.php +++ /dev/null @@ -1,32 +0,0 @@ -imgPath = $this->prepareTestFile($fileName, \OC::$SERVERROOT . '/tests/data/' . $fileName); - // Arbitrary width and length which won't be used to calculate the ratio - $this->width = 500; - $this->height = 200; - $this->provider = new TXT; - } -} --- a/tests/lib/Profile/Actions/FediverseActionTest.php +++ /dev/null @@ -1,187 +0,0 @@ -accountManager = $this->createMock(IAccountManager::class); - $this->l10n = $this->createMock(IL10N::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - - $factory = $this->createMock(IFactory::class); - $factory->method('get') - ->with('lib') - ->willReturn($this->l10n); - - $this->action = new FediverseAction( - $this->accountManager, - $factory, - $this->urlGenerator, - ); - } - - public function testGetActionId(): void { - self::assertEquals( - IAccountManager::PROPERTY_FEDIVERSE, - $this->action->getId(), - ); - } - - public function testGetAppId(): void { - self::assertEquals( - 'core', - $this->action->getAppId(), - ); - } - - public function testGetDisplayId(): void { - $this->l10n->expects(self::once()) - ->method('t') - ->with('Fediverse') - ->willReturn('Translated fediverse'); - - self::assertEquals( - 'Translated fediverse', - $this->action->getDisplayId(), - ); - } - - public function testGetPriority(): void { - self::assertEquals( - 50, - $this->action->getPriority(), - ); - } - - public function testGetIcon(): void { - $this->urlGenerator->expects(self::once()) - ->method('getAbsoluteURL') - ->with('the-image-path') - ->willReturn('resolved-image-path'); - - $this->urlGenerator->expects(self::once()) - ->method('imagePath') - ->with('core', 'actions/mastodon.svg') - ->willReturn('the-image-path'); - - self::assertEquals( - 'resolved-image-path', - $this->action->getIcon(), - ); - } - - public static function dataGetTitle(): array { - return [ - ['the-user@example.com'], - ['@the-user@example.com'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetTitle')] - public function testGetTitle(string $value): void { - $property = $this->createMock(IAccountProperty::class); - $property->method('getValue') - ->willReturn($value); - - $user = $this->createMock(IUser::class); - - $account = $this->createMock(IAccount::class); - $account->method('getProperty') - ->with(IAccountManager::PROPERTY_FEDIVERSE) - ->willReturn($property); - - $this->accountManager->method('getAccount') - ->with($user) - ->willReturn($account); - - $this->l10n->expects(self::once()) - ->method('t') - ->with(self::anything(), ['@the-user@example.com']) - ->willReturn('Translated title'); - - // Preload and test - $this->action->preload($user); - self::assertEquals( - 'Translated title', - $this->action->getTitle(), - ); - } - - public static function dataGetTarget(): array { - return [ - ['', null], - [null, null], - ['user@example.com', 'https://example.com/@user'], - ['@user@example.com', 'https://example.com/@user'], - ['@user@social.example.com', 'https://social.example.com/@user'], - // invalid stuff - ['@user', null], - ['@example.com', null], - ['@@example.com', null], - // evil stuff - ['user@example.com/evil.exe', null], - ['@user@example.com/evil.exe', null], - ['user/evil.exe@example.com', null], - ['@user/evil.exe@example.com', null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetTarget')] - public function testGetTarget(?string $value, ?string $expected): void { - $user = $this->createMock(IUser::class); - - $account = $this->createMock(IAccount::class); - $this->accountManager->method('getAccount') - ->with($user) - ->willReturn($account); - - if ($value === null) { - // Property does not exist so throw - $account->method('getProperty') - ->with(IAccountManager::PROPERTY_FEDIVERSE) - ->willThrowException(new PropertyDoesNotExistException(IAccountManager::PROPERTY_FEDIVERSE)); - } else { - $property = $this->createMock(IAccountProperty::class); - $property->method('getValue') - ->willReturn($value); - $account->method('getProperty') - ->willReturn($property); - } - - // Preload and test - $this->action->preload($user); - self::assertEquals( - $expected, - $this->action->getTarget(), - ); - } -} --- a/tests/lib/Repair/CleanTagsTest.php +++ /dev/null @@ -1,196 +0,0 @@ -outputMock = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->userManager = $this->getMockBuilder(IUserManager::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->connection = Server::get(IDBConnection::class); - $this->repair = new CleanTags($this->connection, $this->userManager); - $this->cleanUpTables(); - } - - #[\Override] - protected function tearDown(): void { - $this->cleanUpTables(); - - parent::tearDown(); - } - - protected function cleanUpTables() { - $qb = $this->connection->getQueryBuilder(); - $qb->delete('vcategory') - ->executeStatement(); - - $qb->delete('vcategory_to_object') - ->executeStatement(); - - $qb->delete('filecache') - ->runAcrossAllShards() - ->executeStatement(); - } - - public function testRun(): void { - $cat1 = $this->addTagCategory('TestRepairCleanTags', 'files'); // Retained - $cat2 = $this->addTagCategory('TestRepairCleanTags2', 'files'); // Deleted: Category will be empty - $this->addTagCategory('TestRepairCleanTags3', 'files'); // Deleted: Category is empty - $cat3 = $this->addTagCategory('TestRepairCleanTags', 'contacts'); // Retained - - $this->addTagEntry($this->getFileID(), $cat2, 'files'); // Retained - $this->addTagEntry($this->getFileID() + 1, $cat1, 'files'); // Deleted: File is NULL - $this->addTagEntry(9999999, $cat3, 'contacts'); // Retained - $this->addTagEntry($this->getFileID(), $cat3 + 1, 'files'); // Deleted: Category is NULL - - $this->assertEntryCount('vcategory_to_object', 4, 'Assert tag entries count before repair step'); - $this->assertEntryCount('vcategory', 4, 'Assert tag categories count before repair step'); - - self::invokePrivate($this->repair, 'deleteOrphanFileEntries', [$this->outputMock]); - $this->assertEntryCount('vcategory_to_object', 3, 'Assert tag entries count after cleaning file entries'); - $this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning file entries'); - - self::invokePrivate($this->repair, 'deleteOrphanTagEntries', [$this->outputMock]); - $this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning tag entries'); - $this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning tag entries'); - - self::invokePrivate($this->repair, 'deleteOrphanCategoryEntries', [$this->outputMock]); - $this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning category entries'); - $this->assertEntryCount('vcategory', 2, 'Assert tag categories count after cleaning category entries'); - - $this->addTagCategory('TestRepairCleanTags', 'contacts', 'userExists'); // Retained - $this->assertEntryCount('vcategory', 3, 'Assert tag categories count before cleaning categories by users'); - - $this->userManager->expects($this->exactly(2)) - ->method('userExists') - ->willReturnMap([ - ['userExists', true], - ['TestRepairCleanTags', false], - ]); - - self::invokePrivate($this->repair, 'deleteOrphanTags', [$this->outputMock]); - $this->assertEntryCount('vcategory', 1, 'Assert tag categories count after cleaning categories by users'); - } - - /** - * @param string $tableName - * @param int $expected - * @param string $message - */ - protected function assertEntryCount($tableName, $expected, $message = '') { - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select($qb->func()->count('*')) - ->from($tableName) - ->executeQuery(); - - $this->assertEquals($expected, $result->fetchOne(), $message); - } - - /** - * Adds a new tag category to the database - * - * @param string $category - * @param string $type - * @param string $user - * @return int - */ - protected function addTagCategory($category, $type, $user = 'TestRepairCleanTags') { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('vcategory') - ->values([ - 'uid' => $qb->createNamedParameter($user), - 'category' => $qb->createNamedParameter($category), - 'type' => $qb->createNamedParameter($type), - ]) - ->executeStatement(); - - return $qb->getLastInsertId(); - } - - /** - * Adds a new tag entry to the database - * @param int $objectId - * @param int $category - * @param string $type - */ - protected function addTagEntry($objectId, $category, $type) { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('vcategory_to_object') - ->values([ - 'objid' => $qb->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), - 'categoryid' => $qb->createNamedParameter($category, IQueryBuilder::PARAM_INT), - 'type' => $qb->createNamedParameter($type), - ]) - ->executeStatement(); - } - - /** - * Gets the last fileid from the file cache - * @return int - */ - protected function getFileID() { - if ($this->createdFile !== null) { - return $this->createdFile; - } - - $qb = $this->connection->getQueryBuilder(); - - // We create a new file entry and delete it after the test again - $fileName = $this->getUniqueID('TestRepairCleanTags', 12); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT), - 'path' => $qb->createNamedParameter($fileName), - 'path_hash' => $qb->createNamedParameter(md5($fileName)), - ]) - ->executeStatement(); - $fileName = $this->getUniqueID('TestRepairCleanTags', 12); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT), - 'path' => $qb->createNamedParameter($fileName), - 'path_hash' => $qb->createNamedParameter(md5($fileName)), - ]) - ->executeStatement(); - - $this->createdFile = $qb->getLastInsertId(); - return $this->createdFile; - } -} --- a/tests/lib/Repair/ClearFrontendCachesTest.php +++ /dev/null @@ -1,51 +0,0 @@ -outputMock = $this->createMock(IOutput::class); - - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->jsCombiner = $this->createMock(JSCombiner::class); - - $this->repair = new ClearFrontendCaches($this->cacheFactory, $this->jsCombiner); - } - - public function testRun(): void { - $imagePathCache = $this->createMock(ICache::class); - $imagePathCache->expects($this->once()) - ->method('clear') - ->with(''); - $this->jsCombiner->expects($this->once()) - ->method('resetCache'); - $this->cacheFactory->expects($this->once()) - ->method('createDistributed') - ->with('imagePath') - ->willReturn($imagePathCache); - - $this->repair->run($this->outputMock); - } -} --- a/tests/lib/Repair/ClearGeneratedAvatarCacheTest.php +++ /dev/null @@ -1,62 +0,0 @@ -avatarManager = $this->createMock(AvatarManager::class); - $this->config = $this->createMock(IConfig::class); - $this->jobList = $this->createMock(IJobList::class); - - $this->repair = new ClearGeneratedAvatarCache($this->config, $this->avatarManager, $this->jobList); - } - - public static function shouldRunDataProvider(): array { - return [ - ['11.0.0.0', true], - ['15.0.0.3', true], - ['13.0.5.2', true], - ['12.0.0.0', true], - ['26.0.0.1', true], - ['15.0.0.2', true], - ['13.0.0.0', true], - ['27.0.0.5', false] - ]; - } - - /** - * - * @param string $from - * @param boolean $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('shouldRunDataProvider')] - public function testShouldRun($from, $expected): void { - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('version', '0.0.0.0') - ->willReturn($from); - - $this->assertEquals($expected, $this->invokePrivate($this->repair, 'shouldRun')); - } -} --- a/tests/lib/Repair/NC29/SanitizeAccountPropertiesJobTest.php +++ /dev/null @@ -1,120 +0,0 @@ -userManager = $this->createMock(IUserManager::class); - $this->accountManager = $this->createMock(IAccountManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->job = new SanitizeAccountPropertiesJob( - $this->createMock(ITimeFactory::class), - $this->userManager, - $this->accountManager, - $this->logger, - ); - } - - public function testParallel() { - self::assertFalse($this->job->getAllowParallelRuns()); - } - - public function testRun(): void { - $users = [ - $this->createMock(IUser::class), - $this->createMock(IUser::class), - $this->createMock(IUser::class), - ]; - $this->userManager - ->expects(self::once()) - ->method('callForSeenUsers') - ->willReturnCallback(fn ($fn) => array_map($fn, $users)); - - $property = $this->createMock(IAccountProperty::class); - $property->expects(self::once())->method('getName')->willReturn(IAccountManager::PROPERTY_PHONE); - $property->expects(self::once())->method('getScope')->willReturn(IAccountManager::SCOPE_LOCAL); - - $account1 = $this->createMock(IAccount::class); - $account1->expects(self::once()) - ->method('getProperty') - ->with(IAccountManager::PROPERTY_PHONE) - ->willReturn($property); - $account1->expects(self::once()) - ->method('setProperty') - ->with(IAccountManager::PROPERTY_PHONE, '', IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); - $account1->expects(self::once()) - ->method('jsonSerialize') - ->willReturn([ - IAccountManager::PROPERTY_DISPLAYNAME => [], - IAccountManager::PROPERTY_PHONE => [], - ]); - - $account2 = $this->createMock(IAccount::class); - $account2->expects(self::never()) - ->method('getProperty'); - $account2->expects(self::once()) - ->method('jsonSerialize') - ->willReturn([ - IAccountManager::PROPERTY_DISPLAYNAME => [], - IAccountManager::PROPERTY_PHONE => [], - ]); - - $account3 = $this->createMock(IAccount::class); - $account3->expects(self::never()) - ->method('getProperty'); - $account3->expects(self::once()) - ->method('jsonSerialize') - ->willReturn([ - IAccountManager::PROPERTY_DISPLAYNAME => [], - ]); - - $this->accountManager - ->expects(self::exactly(3)) - ->method('getAccount') - ->willReturnMap([ - [$users[0], $account1], - [$users[1], $account2], - [$users[2], $account3], - ]); - $valid = false; - $this->accountManager->expects(self::exactly(3)) - ->method('updateAccount') - ->willReturnCallback(function (IAccount $account) use (&$account1, &$valid): void { - if (!$valid && $account === $account1) { - $valid = true; - throw new InvalidArgumentException(IAccountManager::PROPERTY_PHONE); - } - }); - - self::invokePrivate($this->job, 'run', [null]); - } -} --- a/tests/lib/Repair/NC29/SanitizeAccountPropertiesTest.php +++ /dev/null @@ -1,47 +0,0 @@ -jobList = $this->createMock(IJobList::class); - - $this->repairStep = new SanitizeAccountProperties($this->jobList); - } - - public function testGetName(): void { - self::assertStringContainsString('Validate account properties', $this->repairStep->getName()); - } - - public function testRun(): void { - $this->jobList->expects(self::once()) - ->method('add') - ->with(SanitizeAccountPropertiesJob::class, null); - - $output = $this->createMock(IOutput::class); - $output->expects(self::once()) - ->method('info') - ->with(self::matchesRegularExpression('/queued background/i')); - - $this->repairStep->run($output); - } -} --- a/tests/lib/Repair/OldGroupMembershipSharesTest.php +++ /dev/null @@ -1,135 +0,0 @@ -groupManager = $this->getMockBuilder(IGroupManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->connection = Server::get(IDBConnection::class); - - $this->deleteAllShares(); - } - - #[\Override] - protected function tearDown(): void { - $this->deleteAllShares(); - - parent::tearDown(); - } - - protected function deleteAllShares() { - $qb = $this->connection->getQueryBuilder(); - $qb->delete('share')->executeStatement(); - } - - public function testRun(): void { - $repair = new OldGroupMembershipShares( - $this->connection, - $this->groupManager - ); - - $this->groupManager->expects($this->exactly(2)) - ->method('isInGroup') - ->willReturnMap([ - ['member', 'group', true], - ['not-a-member', 'group', false], - ]); - - $parent = $this->createShare(IShare::TYPE_GROUP, 'group', null); - $group2 = $this->createShare(IShare::TYPE_GROUP, 'group2', $parent); - $user1 = $this->createShare(IShare::TYPE_USER, 'user1', $parent); - - // \OC\Share\Constant::$shareTypeGroupUserUnique === 2 - $member = $this->createShare(2, 'member', $parent); - $notAMember = $this->createShare(2, 'not-a-member', $parent); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('id') - ->from('share') - ->orderBy('id', 'ASC') - ->executeQuery(); - $rows = $result->fetchAllAssociative(); - $this->assertEquals([['id' => $parent], ['id' => $group2], ['id' => $user1], ['id' => $member], ['id' => $notAMember]], $rows); - $result->closeCursor(); - - /** @var IOutput | \PHPUnit\Framework\MockObject\MockObject $outputMock */ - $outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') - ->disableOriginalConstructor() - ->getMock(); - - $repair->run($outputMock); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('id') - ->from('share') - ->orderBy('id', 'ASC') - ->executeQuery(); - $rows = $result->fetchAllAssociative(); - $this->assertEquals([['id' => $parent], ['id' => $group2], ['id' => $user1], ['id' => $member]], $rows); - $result->closeCursor(); - } - - /** - * @param string $shareType - * @param string $shareWith - * @param null|int $parent - * @return int - */ - protected function createShare($shareType, $shareWith, $parent) { - $qb = $this->connection->getQueryBuilder(); - $shareValues = [ - 'share_type' => $qb->expr()->literal($shareType), - 'share_with' => $qb->expr()->literal($shareWith), - 'uid_owner' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('folder'), - 'item_source' => $qb->expr()->literal(123), - 'item_target' => $qb->expr()->literal('/123'), - 'file_source' => $qb->expr()->literal(123), - 'file_target' => $qb->expr()->literal('/test'), - 'permissions' => $qb->expr()->literal(1), - 'stime' => $qb->expr()->literal(time()), - 'expiration' => $qb->expr()->literal('2015-09-25 00:00:00'), - ]; - - if ($parent) { - $shareValues['parent'] = $qb->expr()->literal($parent); - } - - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values($shareValues) - ->executeStatement(); - - return $qb->getLastInsertId(); - } -} --- a/tests/lib/Repair/Owncloud/CleanPreviewsBackgroundJobTest.php +++ /dev/null @@ -1,238 +0,0 @@ -rootFolder = $this->createMock(IRootFolder::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->jobList = $this->createMock(IJobList::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->userManager = $this->createMock(IUserManager::class); - - $this->userManager->expects($this->any())->method('userExists')->willReturn(true); - - $this->job = new CleanPreviewsBackgroundJob( - $this->rootFolder, - $this->logger, - $this->jobList, - $this->timeFactory, - $this->userManager - ); - } - - public function testCleanupPreviewsUnfinished(): void { - $userFolder = $this->createMock(Folder::class); - $userRoot = $this->createMock(Folder::class); - $thumbnailFolder = $this->createMock(Folder::class); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('myuid')) - ->willReturn($userFolder); - - $userFolder->method('getParent')->willReturn($userRoot); - - $userRoot->method('get') - ->with($this->equalTo('thumbnails')) - ->willReturn($thumbnailFolder); - - $previewFolder1 = $this->createMock(Folder::class); - - $previewFolder1->expects($this->once()) - ->method('delete'); - - $thumbnailFolder->method('getDirectoryListing') - ->willReturn([$previewFolder1]); - $thumbnailFolder->expects($this->never()) - ->method('delete'); - - $this->timeFactory->method('getTime')->willReturnOnConsecutiveCalls(100, 200); - - $this->jobList->expects($this->once()) - ->method('add') - ->with( - $this->equalTo(CleanPreviewsBackgroundJob::class), - $this->equalTo(['uid' => 'myuid']) - ); - - $loggerCalls = []; - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnCallback(function () use (&$loggerCalls): void { - $loggerCalls[] = func_get_args(); - }); - - $this->job->run(['uid' => 'myuid']); - self::assertEquals([ - ['Started preview cleanup for myuid', []], - ['New preview cleanup scheduled for myuid', []], - ], $loggerCalls); - } - - public function testCleanupPreviewsFinished(): void { - $userFolder = $this->createMock(Folder::class); - $userRoot = $this->createMock(Folder::class); - $thumbnailFolder = $this->createMock(Folder::class); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('myuid')) - ->willReturn($userFolder); - - $userFolder->method('getParent')->willReturn($userRoot); - - $userRoot->method('get') - ->with($this->equalTo('thumbnails')) - ->willReturn($thumbnailFolder); - - $previewFolder1 = $this->createMock(Folder::class); - - $previewFolder1->expects($this->once()) - ->method('delete'); - - $thumbnailFolder->method('getDirectoryListing') - ->willReturn([$previewFolder1]); - - $this->timeFactory->method('getTime')->willReturnOnConsecutiveCalls(100, 101); - - $this->jobList->expects($this->never()) - ->method('add'); - - $loggerCalls = []; - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnCallback(function () use (&$loggerCalls): void { - $loggerCalls[] = func_get_args(); - }); - - $thumbnailFolder->expects($this->once()) - ->method('delete'); - - $this->job->run(['uid' => 'myuid']); - self::assertEquals([ - ['Started preview cleanup for myuid', []], - ['Preview cleanup done for myuid', []], - ], $loggerCalls); - } - - public function testNoUserFolder(): void { - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('myuid')) - ->willThrowException(new NotFoundException()); - - $loggerCalls = []; - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnCallback(function () use (&$loggerCalls): void { - $loggerCalls[] = func_get_args(); - }); - - $this->job->run(['uid' => 'myuid']); - self::assertEquals([ - ['Started preview cleanup for myuid', []], - ['Preview cleanup done for myuid', []], - ], $loggerCalls); - } - - public function testNoThumbnailFolder(): void { - $userFolder = $this->createMock(Folder::class); - $userRoot = $this->createMock(Folder::class); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('myuid')) - ->willReturn($userFolder); - - $userFolder->method('getParent')->willReturn($userRoot); - - $userRoot->method('get') - ->with($this->equalTo('thumbnails')) - ->willThrowException(new NotFoundException()); - - $loggerCalls = []; - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnCallback(function () use (&$loggerCalls): void { - $loggerCalls[] = func_get_args(); - }); - - $this->job->run(['uid' => 'myuid']); - self::assertEquals([ - ['Started preview cleanup for myuid', []], - ['Preview cleanup done for myuid', []], - ], $loggerCalls); - } - - public function testNotPermittedToDelete(): void { - $userFolder = $this->createMock(Folder::class); - $userRoot = $this->createMock(Folder::class); - $thumbnailFolder = $this->createMock(Folder::class); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('myuid')) - ->willReturn($userFolder); - - $userFolder->method('getParent')->willReturn($userRoot); - - $userRoot->method('get') - ->with($this->equalTo('thumbnails')) - ->willReturn($thumbnailFolder); - - $previewFolder1 = $this->createMock(Folder::class); - - $previewFolder1->expects($this->once()) - ->method('delete') - ->willThrowException(new NotPermittedException()); - - $thumbnailFolder->method('getDirectoryListing') - ->willReturn([$previewFolder1]); - - $this->timeFactory->method('getTime')->willReturnOnConsecutiveCalls(100, 101); - - $this->jobList->expects($this->never()) - ->method('add'); - - $thumbnailFolder->expects($this->once()) - ->method('delete') - ->willThrowException(new NotPermittedException()); - - $loggerCalls = []; - $this->logger->expects($this->exactly(2)) - ->method('info') - ->willReturnCallback(function () use (&$loggerCalls): void { - $loggerCalls[] = func_get_args(); - }); - - $this->job->run(['uid' => 'myuid']); - self::assertEquals([ - ['Started preview cleanup for myuid', []], - ['Preview cleanup done for myuid', []], - ], $loggerCalls); - } -} --- a/tests/lib/Repair/Owncloud/CleanPreviewsTest.php +++ /dev/null @@ -1,106 +0,0 @@ -jobList = $this->createMock(IJobList::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->repair = new CleanPreviews( - $this->jobList, - $this->userManager, - $this->appConfig - ); - } - - public function testGetName(): void { - $this->assertSame('Add preview cleanup background jobs', $this->repair->getName()); - } - - public function testRun(): void { - $user1 = $this->createMock(IUser::class); - $user1->method('getUID') - ->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID') - ->willReturn('user2'); - - $this->userManager->expects($this->once()) - ->method('callForSeenUsers') - ->willReturnCallback(function (\Closure $function) use (&$user1, $user2): void { - $function($user1); - $function($user2); - }); - - $jobListCalls = []; - $this->jobList->expects($this->exactly(2)) - ->method('add') - ->willReturnCallback(function () use (&$jobListCalls): void { - $jobListCalls[] = func_get_args(); - }); - - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with( - $this->equalTo('core'), - $this->equalTo('previewsCleanedUp'), - )->willReturn(false); - $this->appConfig->expects($this->once()) - ->method('setValueBool') - ->with( - $this->equalTo('core'), - $this->equalTo('previewsCleanedUp'), - $this->equalTo(true) - ); - - $this->repair->run($this->createMock(IOutput::class)); - $this->assertEqualsCanonicalizing([ - [CleanPreviewsBackgroundJob::class, ['uid' => 'user1']], - [CleanPreviewsBackgroundJob::class, ['uid' => 'user2']], - ], $jobListCalls); - } - - public function testRunAlreadyDone(): void { - $this->userManager->expects($this->never()) - ->method($this->anything()); - - $this->jobList->expects($this->never()) - ->method($this->anything()); - - $this->appConfig->expects($this->once()) - ->method('getValueBool') - ->with( - $this->equalTo('core'), - $this->equalTo('previewsCleanedUp'), - )->willReturn(true); - $this->appConfig->expects($this->never()) - ->method('setValueBool'); - - $this->repair->run($this->createMock(IOutput::class)); - } -} --- a/tests/lib/Repair/Owncloud/UpdateLanguageCodesTest.php +++ /dev/null @@ -1,157 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->config = $this->createMock(IConfig::class); - } - - public function testRun(): void { - $users = [ - ['userid' => 'user1', 'configvalue' => 'fi_FI'], - ['userid' => 'user2', 'configvalue' => 'de'], - ['userid' => 'user3', 'configvalue' => 'fi'], - ['userid' => 'user4', 'configvalue' => 'ja'], - ['userid' => 'user5', 'configvalue' => 'bg_BG'], - ['userid' => 'user6', 'configvalue' => 'ja'], - ['userid' => 'user7', 'configvalue' => 'th_TH'], - ['userid' => 'user8', 'configvalue' => 'th_TH'], - ]; - - // insert test data - $qb = $this->connection->getQueryBuilder(); - $qb->insert('preferences') - ->values([ - 'userid' => $qb->createParameter('userid'), - 'appid' => $qb->createParameter('appid'), - 'configkey' => $qb->createParameter('configkey'), - 'configvalue' => $qb->createParameter('configvalue'), - ]); - foreach ($users as $user) { - $qb->setParameters([ - 'userid' => $user['userid'], - 'appid' => 'core', - 'configkey' => 'lang', - 'configvalue' => $user['configvalue'], - ])->executeStatement(); - } - - // check if test data is written to DB - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['userid', 'configvalue']) - ->from('preferences') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter('core'))) - ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang'))) - ->orderBy('userid') - ->executeQuery(); - - $rows = $result->fetchAllAssociative(); - $result->closeCursor(); - - $this->assertSame($users, $rows, 'Asserts that the entries are the ones from the test data set'); - - $expectedOutput = [ - ['Changed 1 setting(s) from "bg_BG" to "bg" in preferences table.'], - ['Changed 0 setting(s) from "cs_CZ" to "cs" in preferences table.'], - ['Changed 1 setting(s) from "fi_FI" to "fi" in preferences table.'], - ['Changed 0 setting(s) from "hu_HU" to "hu" in preferences table.'], - ['Changed 0 setting(s) from "nb_NO" to "nb" in preferences table.'], - ['Changed 0 setting(s) from "sk_SK" to "sk" in preferences table.'], - ['Changed 2 setting(s) from "th_TH" to "th" in preferences table.'], - ]; - $outputMessages = []; - /** @var IOutput&MockObject $outputMock */ - $outputMock = $this->createMock(IOutput::class); - $outputMock->expects($this->exactly(7)) - ->method('info') - ->willReturnCallback(function () use (&$outputMessages): void { - $outputMessages[] = func_get_args(); - }); - - $this->config->expects($this->once()) - ->method('getSystemValueString') - ->with('version', '0.0.0') - ->willReturn('12.0.0.13'); - - // run repair step - $repair = new UpdateLanguageCodes($this->connection, $this->config); - $repair->run($outputMock); - - // check if test data is correctly modified in DB - $qb = $this->connection->getQueryBuilder(); - $result = $qb->select(['userid', 'configvalue']) - ->from('preferences') - ->where($qb->expr()->eq('appid', $qb->createNamedParameter('core'))) - ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang'))) - ->orderBy('userid') - ->executeQuery(); - - $rows = $result->fetchAllAssociative(); - $result->closeCursor(); - - // value has changed for one user - $users[0]['configvalue'] = 'fi'; - $users[4]['configvalue'] = 'bg'; - $users[6]['configvalue'] = 'th'; - $users[7]['configvalue'] = 'th'; - $this->assertSame($users, $rows, 'Asserts that the entries are updated correctly.'); - - // remove test data - foreach ($users as $user) { - $qb = $this->connection->getQueryBuilder(); - $qb->delete('preferences') - ->where($qb->expr()->eq('userid', $qb->createNamedParameter($user['userid']))) - ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter('core'))) - ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang'))) - ->andWhere($qb->expr()->eq('configvalue', $qb->createNamedParameter($user['configvalue']), IQueryBuilder::PARAM_STR)) - ->executeStatement(); - } - self::assertEquals($expectedOutput, $outputMessages); - } - - public function testSecondRun(): void { - /** @var IOutput&MockObject $outputMock */ - $outputMock = $this->createMock(IOutput::class); - $outputMock->expects($this->never()) - ->method('info'); - - $this->config->expects($this->once()) - ->method('getSystemValueString') - ->with('version', '0.0.0') - ->willReturn('12.0.0.14'); - - // run repair step - $repair = new UpdateLanguageCodes($this->connection, $this->config); - $repair->run($outputMock); - } -} --- a/tests/lib/Repair/RepairCollationTest.php +++ /dev/null @@ -1,87 +0,0 @@ -connection = Server::get(ConnectionAdapter::class); - if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_MYSQL) { - $this->markTestSkipped('Test only relevant on MySql'); - } - - $this->logger = $this->createMock(LoggerInterface::class); - $this->config = Server::get(IConfig::class); - $dbPrefix = $this->config->getSystemValueString('dbtableprefix'); - $this->tableName = $this->getUniqueID($dbPrefix . '_collation_test'); - $this->connection->prepare("CREATE TABLE $this->tableName(text VARCHAR(16)) COLLATE utf8_unicode_ci")->execute(); - - $this->repair = new TestCollationRepair($this->config, $this->logger, $this->connection, false); - } - - #[\Override] - protected function tearDown(): void { - $this->connection = Server::get(ConnectionAdapter::class); - if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL) { - // tear down only needed on MySQL - $this->connection->getInner()->createSchemaManager()->dropTable($this->tableName); - } - parent::tearDown(); - } - - public function testCollationConvert(): void { - $tables = $this->repair->getAllNonUTF8BinTables($this->connection); - $this->assertGreaterThanOrEqual(1, count($tables)); - - $outputMock = $this->getMockBuilder(IOutput::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->repair->run($outputMock); - - $tables = $this->repair->getAllNonUTF8BinTables($this->connection); - $this->assertCount(0, $tables); - } -} --- a/tests/lib/Repair/RepairDavSharesTest.php +++ /dev/null @@ -1,179 +0,0 @@ -output = $this->createMock(IOutput::class); - - $this->config = $this->createMock(IConfig::class); - $this->dbc = $this->createMock(IDBConnection::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->repair = new RepairDavShares( - $this->config, - $this->dbc, - $this->groupManager, - $this->logger - ); - } - - public function testRun(): void { - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('version', '0.0.0') - ->willReturn('20.0.2'); - - $this->output->expects($this->atLeastOnce()) - ->method('info'); - - $existingGroups = [ - 'Innocent', - 'Wants Repair', - 'Well förmed', - 'family+friends', - 'family friends', - ]; - - $shareResultData = [ - [ - // No update, nothing to escape - 'id' => 0, - 'principaluri' => 'principals/groups/Innocent', - ], - [ - // Update - 'id' => 1, - 'principaluri' => 'principals/groups/Wants Repair', - ], - [ - // No update, already proper - 'id' => 2, - 'principaluri' => 'principals/groups/Well+f%C3%B6rmed', - ], - [ - // No update, unknown group - 'id' => 3, - 'principaluri' => 'principals/groups/Not known', - ], - [ - // No update, unknown group - 'id' => 4, - 'principaluri' => 'principals/groups/Also%2F%2FNot%23Known', - ], - [ - // No update, group exists in both forms - 'id' => 5, - 'principaluri' => 'principals/groups/family+friends', - ], - [ - // No update, already proper - 'id' => 6, - 'principaluri' => 'principals/groups/family%2Bfriends', - ], - [ - // Update - 'id' => 7, - 'principaluri' => 'principals/groups/family friends', - ], - ]; - - $shareResults = $this->createMock(IResult::class); - $shareResults->expects($this->any()) - ->method('fetchAssociative') - ->willReturnCallback(function () use (&$shareResultData) { - return array_pop($shareResultData) ?? false; - }); - - $expressionBuilder = $this->createMock(IExpressionBuilder::class); - - $selectMock = $this->createMock(IQueryBuilder::class); - $selectMock->expects($this->any()) - ->method('expr') - ->willReturn($expressionBuilder); - $selectMock->expects($this->once()) - ->method('select') - ->willReturnSelf(); - $selectMock->expects($this->once()) - ->method('from') - ->willReturnSelf(); - $selectMock->expects($this->once()) - ->method('where') - ->willReturnSelf(); - $selectMock->expects($this->once()) - ->method('executeQuery') - ->willReturn($shareResults); - - $updateCalls = []; - $updateMock = $this->createMock(IQueryBuilder::class); - $updateMock->expects($this->any()) - ->method('expr') - ->willReturn($expressionBuilder); - $updateMock->expects($this->once()) - ->method('update') - ->willReturnSelf(); - $updateMock->expects($this->any()) - ->method('set') - ->willReturnSelf(); - $updateMock->expects($this->once()) - ->method('where') - ->willReturnSelf(); - $updateMock->expects($this->exactly(4)) - ->method('setParameter') - ->willReturnCallback(function () use (&$updateCalls, &$updateMock) { - $updateCalls[] = func_get_args(); - return $updateMock; - }); - $updateMock->expects($this->exactly(2)) - ->method('executeStatement'); - - $this->dbc->expects($this->atLeast(2)) - ->method('getQueryBuilder') - ->willReturnOnConsecutiveCalls($selectMock, $updateMock); - - $this->groupManager->expects($this->any()) - ->method('groupExists') - ->willReturnCallback(function (string $gid) use ($existingGroups) { - return in_array($gid, $existingGroups, true); - }); - - $this->repair->run($this->output); - self::assertEquals([ - ['updatedPrincipalUri', 'principals/groups/' . urlencode('family friends'), null], - ['shareId', 7, null], - ['updatedPrincipalUri', 'principals/groups/' . urlencode('Wants Repair'), null], - ['shareId', 1, null] - ], $updateCalls); - } -} --- a/tests/lib/Repair/RepairInvalidSharesTest.php +++ /dev/null @@ -1,272 +0,0 @@ -getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $config->expects($this->any()) - ->method('getSystemValueString') - ->with('version') - ->willReturn('12.0.0.0'); - - $this->connection = Server::get(IDBConnection::class); - $this->deleteAllShares(); - - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->repair = new RepairInvalidShares($config, $this->connection, $this->appConfig); - } - - #[\Override] - protected function tearDown(): void { - $this->deleteAllShares(); - - parent::tearDown(); - } - - protected function deleteAllShares() { - $qb = $this->connection->getQueryBuilder(); - $qb->delete('share')->executeStatement(); - } - - /** - * Test remove shares where the parent share does not exist anymore - */ - public function testSharesNonExistingParent(): void { - $qb = $this->connection->getQueryBuilder(); - $shareValues = [ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('recipientuser1'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('folder'), - 'item_source' => $qb->expr()->literal(123), - 'item_target' => $qb->expr()->literal('/123'), - 'file_source' => $qb->expr()->literal(123), - 'file_target' => $qb->expr()->literal('/test'), - 'permissions' => $qb->expr()->literal(1), - 'stime' => $qb->expr()->literal(time()), - 'expiration' => $qb->expr()->literal('2015-09-25 00:00:00') - ]; - - // valid share - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values($shareValues) - ->executeStatement(); - $parent = $qb->getLastInsertId(); - - // share with existing parent - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values(array_merge($shareValues, [ - 'parent' => $qb->expr()->literal($parent), - ]))->executeStatement(); - $validChild = $qb->getLastInsertId(); - - // share with non-existing parent - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values(array_merge($shareValues, [ - 'parent' => $qb->expr()->literal($parent + 100), - ]))->executeStatement(); - $invalidChild = $qb->getLastInsertId(); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('id') - ->from('share') - ->orderBy('id', 'ASC') - ->executeQuery(); - $rows = $result->fetchAllAssociative(); - $this->assertEquals([['id' => $parent], ['id' => $validChild], ['id' => $invalidChild]], $rows); - $result->closeCursor(); - - /** @var IOutput | \PHPUnit\Framework\MockObject\MockObject $outputMock */ - $outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') - ->disableOriginalConstructor() - ->getMock(); - - $this->repair->run($outputMock); - - $query = $this->connection->getQueryBuilder(); - $result = $query->select('id') - ->from('share') - ->orderBy('id', 'ASC') - ->executeQuery(); - $rows = $result->fetchAllAssociative(); - $this->assertEquals([['id' => $parent], ['id' => $validChild]], $rows); - $result->closeCursor(); - } - - public static function trailingSlashProvider(): array { - return [ - // trailing slash left behind by renaming a parent folder of a moved share - ['/rename_folder/First_share_the_file.odt/', '/rename_folder/First_share_the_file.odt'], - ['/shared_folder/', '/shared_folder'], - // unchanged - ['/rename_folder/First_share_the_file.odt', '/rename_folder/First_share_the_file.odt'], - ['/', '/'], - ]; - } - - /** - * Test stripping trailing slashes from the share target - */ - private function addShareWithTarget(string $fileTarget): void { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('recipientuser1'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('folder'), - 'item_source' => $qb->expr()->literal(123), - 'item_target' => $qb->expr()->literal('/123'), - 'file_source' => $qb->expr()->literal(123), - 'file_target' => $qb->expr()->literal($fileTarget), - 'permissions' => $qb->expr()->literal(31), - 'stime' => $qb->expr()->literal(time()), - ]) - ->executeStatement(); - } - - private function getSingleFileTarget(): string { - $results = $this->connection->getQueryBuilder() - ->select('file_target') - ->from('share') - ->executeQuery() - ->fetchAllAssociative(); - - $this->assertCount(1, $results); - - return $results[0]['file_target']; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('trailingSlashProvider')] - public function testRemoveTrailingSlashFromFileTarget(string $fileTarget, string $expectedFileTarget): void { - $this->addShareWithTarget($fileTarget); - - $this->appConfig->method('getValueBool') - ->with('core', ConfigLexicon::SHARE_REPAIR_REMOVED_TRAILING_SLASHES, false, true) - ->willReturn(false); - $this->appConfig->expects($this->once()) - ->method('setValueBool') - ->with('core', ConfigLexicon::SHARE_REPAIR_REMOVED_TRAILING_SLASHES, true, true); - - $this->repair->run($this->createMock(IOutput::class)); - - $this->assertSame($expectedFileTarget, $this->getSingleFileTarget()); - } - - public function testRemoveTrailingSlashFromFileTargetSkippedWhenAlreadyRun(): void { - $this->addShareWithTarget('/rename_folder/First_share_the_file.odt/'); - - $this->appConfig->method('getValueBool') - ->with('core', ConfigLexicon::SHARE_REPAIR_REMOVED_TRAILING_SLASHES, false, true) - ->willReturn(true); - $this->appConfig->expects($this->never()) - ->method('setValueBool'); - - $this->repair->run($this->createMock(IOutput::class)); - - $this->assertSame('/rename_folder/First_share_the_file.odt/', $this->getSingleFileTarget()); - } - - public static function fileSharePermissionsProvider(): array { - return [ - // unchanged for folder - [ - 'folder', - 31, - 31, - ], - // unchanged for read-write + share - [ - 'file', - Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE, - Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE, - ], - // fixed for all perms - [ - 'file', - Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE | Constants::PERMISSION_SHARE, - Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE, - ], - ]; - } - - /** - * Test adjusting file share permissions - */ - #[\PHPUnit\Framework\Attributes\DataProvider('fileSharePermissionsProvider')] - public function testFileSharePermissions($itemType, $testPerms, $expectedPerms): void { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'uid_owner' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal($itemType), - 'item_source' => $qb->expr()->literal(123), - 'item_target' => $qb->expr()->literal('/123'), - 'file_source' => $qb->expr()->literal(123), - 'file_target' => $qb->expr()->literal('/test'), - 'permissions' => $qb->expr()->literal($testPerms), - 'stime' => $qb->expr()->literal(time()), - ]) - ->executeStatement(); - - /** @var IOutput | \PHPUnit\Framework\MockObject\MockObject $outputMock */ - $outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') - ->disableOriginalConstructor() - ->getMock(); - - $this->repair->run($outputMock); - - $results = $this->connection->getQueryBuilder() - ->select('*') - ->from('share') - ->orderBy('permissions', 'ASC') - ->executeQuery() - ->fetchAllAssociative(); - - $this->assertCount(1, $results); - - $updatedShare = $results[0]; - - $this->assertEquals($expectedPerms, $updatedShare['permissions']); - } -} --- a/tests/lib/Repair/RepairMimeTypesTest.php +++ /dev/null @@ -1,295 +0,0 @@ -mimetypeLoader = Server::get(IMimeTypeLoader::class); - $this->db = Server::get(IDBConnection::class); - - $config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $config->method('getSystemValueString') - ->with('version') - ->willReturn('11.0.0.0'); - - $appConfig = $this->getMockBuilder(IAppConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $appConfig->method('getValueString') - ->with('files', 'mimetype_version') - ->willReturn('11.0.0.0'); - - $this->storage = new Temporary([]); - $this->storage->getScanner()->scan(''); - - $this->repair = new RepairMimeTypes( - $config, - $appConfig, - Server::get(IDBConnection::class), - ); - } - - #[\Override] - protected function tearDown(): void { - $this->storage->getCache()->clear(); - - $qb = $this->db->getQueryBuilder(); - $qb->delete('storages') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($this->storage->getId()))); - $qb->executeStatement(); - - $this->clearMimeTypes(); - - parent::tearDown(); - } - - private function clearMimeTypes() { - $qb = $this->db->getQueryBuilder(); - $qb->delete('mimetypes'); - $qb->executeStatement(); - - $this->mimetypeLoader->reset(); - } - - private function addEntries($entries) { - // create files for the different extensions, this - // will also automatically create the corresponding mime types - foreach ($entries as $entry) { - $this->storage->getCache()->put( - $entry[0], - [ - 'size' => 0, - 'mtime' => 0, - 'mimetype' => $entry[1] - ] - ); - } - } - - private function checkEntries($entries) { - foreach ($entries as $entry) { - $data = $this->storage->getCache()->get($entry[0]); - $this->assertEquals($entry[1], $data['mimetype']); - } - } - - private function renameMimeTypes($currentMimeTypes, $fixedMimeTypes) { - $this->addEntries($currentMimeTypes); - - /** @var IOutput | \PHPUnit\Framework\MockObject\MockObject $outputMock */ - $outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') - ->disableOriginalConstructor() - ->getMock(); - - $this->repair->run($outputMock); - - // force mimetype reload - $this->mimetypeLoader->reset(); - - $this->checkEntries($fixedMimeTypes); - } - - /** - * Test renaming the additional image mime types - */ - public function testRenameImageTypes(): void { - $currentMimeTypes = [ - ['test.jp2', 'application/octet-stream'], - ['test.webp', 'application/octet-stream'], - ]; - - $fixedMimeTypes = [ - ['test.jp2', 'image/jp2'], - ['test.webp', 'image/webp'], - ]; - - $this->renameMimeTypes($currentMimeTypes, $fixedMimeTypes); - } - - /** - * Test renaming the richdocuments additional office mime types - */ - public function testRenameWindowsProgramTypes(): void { - $currentMimeTypes = [ - ['test.htaccess', 'application/octet-stream'], - ['.htaccess', 'application/octet-stream'], - ['test.bat', 'application/octet-stream'], - ['test.cmd', 'application/octet-stream'], - ]; - - $fixedMimeTypes = [ - ['test.htaccess', 'text/plain'], - ['.htaccess', 'text/plain'], - ['test.bat', 'application/x-msdos-program'], - ['test.cmd', 'application/cmd'], - ]; - - $this->renameMimeTypes($currentMimeTypes, $fixedMimeTypes); - } - - /** - * Test that nothing happens and no error happens when all mimetypes are - * already correct and no old ones exist.. - */ - public function testDoNothingWhenOnlyNewFiles(): void { - $currentMimeTypes = [ - ['test.doc', 'application/msword'], - ['test.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], - ['test.xls', 'application/vnd.ms-excel'], - ['test.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], - ['test.ppt', 'application/vnd.ms-powerpoint'], - ['test.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], - ['test.apk', 'application/vnd.android.package-archive'], - ['test.ttf', 'application/font-sfnt'], - ['test.otf', 'application/font-sfnt'], - ['test.pfb', 'application/x-font'], - ['test.eps', 'application/postscript'], - ['test.ps', 'application/postscript'], - ['test.arw', 'image/x-dcraw'], - ['test.cr2', 'image/x-dcraw'], - ['test.dcr', 'image/x-dcraw'], - ['test.dng', 'image/x-dcraw'], - ['test.erf', 'image/x-dcraw'], - ['test.iiq', 'image/x-dcraw'], - ['test.k25', 'image/x-dcraw'], - ['test.kdc', 'image/x-dcraw'], - ['test.mef', 'image/x-dcraw'], - ['test.nef', 'image/x-dcraw'], - ['test.orf', 'image/x-dcraw'], - ['test.pef', 'image/x-dcraw'], - ['test.raf', 'image/x-dcraw'], - ['test.rw2', 'image/x-dcraw'], - ['test.srf', 'image/x-dcraw'], - ['test.sr2', 'image/x-dcraw'], - ['test.xrf', 'image/x-dcraw'], - ['test.DNG', 'image/x-dcraw'], - ['test.jp2', 'image/jp2'], - ['test.jps', 'image/jpeg'], - ['test.MPO', 'image/jpeg'], - ['test.webp', 'image/webp'], - ['test.conf', 'text/plain'], - ['test.cnf', 'text/plain'], - ['test.yaml', 'application/yaml'], - ['test.yml', 'application/yaml'], - ['test.java', 'text/x-java-source'], - ['test.class', 'application/java'], - ['test.hpp', 'text/x-h'], - ['test.rss', 'application/rss+xml'], - ['test.rtf', 'text/rtf'], - ['test.lwp', 'application/vnd.lotus-wordpro'], - ['test.one', 'application/msonenote'], - ['test.vsd', 'application/vnd.visio'], - ['test.wpd', 'application/vnd.wordperfect'], - ['test.htaccess', 'text/plain'], - ['.htaccess', 'text/plain'], - ['test.bat', 'application/x-msdos-program'], - ['test.cmd', 'application/cmd'], - ]; - - $fixedMimeTypes = [ - ['test.doc', 'application/msword'], - ['test.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], - ['test.xls', 'application/vnd.ms-excel'], - ['test.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], - ['test.ppt', 'application/vnd.ms-powerpoint'], - ['test.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'], - ['test.apk', 'application/vnd.android.package-archive'], - ['test.ttf', 'application/font-sfnt'], - ['test.otf', 'application/font-sfnt'], - ['test.pfb', 'application/x-font'], - ['test.eps', 'application/postscript'], - ['test.ps', 'application/postscript'], - ['test.arw', 'image/x-dcraw'], - ['test.cr2', 'image/x-dcraw'], - ['test.dcr', 'image/x-dcraw'], - ['test.dng', 'image/x-dcraw'], - ['test.erf', 'image/x-dcraw'], - ['test.iiq', 'image/x-dcraw'], - ['test.k25', 'image/x-dcraw'], - ['test.kdc', 'image/x-dcraw'], - ['test.mef', 'image/x-dcraw'], - ['test.nef', 'image/x-dcraw'], - ['test.orf', 'image/x-dcraw'], - ['test.pef', 'image/x-dcraw'], - ['test.raf', 'image/x-dcraw'], - ['test.rw2', 'image/x-dcraw'], - ['test.srf', 'image/x-dcraw'], - ['test.sr2', 'image/x-dcraw'], - ['test.xrf', 'image/x-dcraw'], - ['test.DNG', 'image/x-dcraw'], - ['test.jp2', 'image/jp2'], - ['test.jps', 'image/jpeg'], - ['test.MPO', 'image/jpeg'], - ['test.webp', 'image/webp'], - ['test.conf', 'text/plain'], - ['test.cnf', 'text/plain'], - ['test.yaml', 'application/yaml'], - ['test.yml', 'application/yaml'], - ['test.java', 'text/x-java-source'], - ['test.class', 'application/java'], - ['test.hpp', 'text/x-h'], - ['test.rss', 'application/rss+xml'], - ['test.rtf', 'text/rtf'], - ['test.lwp', 'application/vnd.lotus-wordpro'], - ['test.one', 'application/msonenote'], - ['test.vsd', 'application/vnd.visio'], - ['test.wpd', 'application/vnd.wordperfect'], - ['test.htaccess', 'text/plain'], - ['.htaccess', 'text/plain'], - ['test.bat', 'application/x-msdos-program'], - ['test.cmd', 'application/cmd'], - ]; - - $this->renameMimeTypes($currentMimeTypes, $fixedMimeTypes); - } - - /** - * Test that mime type renaming does not affect folders - */ - public function testDoNotChangeFolderMimeType(): void { - $currentMimeTypes = [ - ['test.conf', 'httpd/unix-directory'], - ['test.cnf', 'httpd/unix-directory'], - ]; - - $fixedMimeTypes = [ - ['test.conf', 'httpd/unix-directory'], - ['test.cnf', 'httpd/unix-directory'], - ]; - - $this->renameMimeTypes($currentMimeTypes, $fixedMimeTypes); - } -} --- a/tests/lib/Repair/RepairSanitizeSystemTagsTest.php +++ /dev/null @@ -1,211 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->output = $this->createMock(IOutput::class); - $this->repair = new RepairSanitizeSystemTags($this->connection); - - $this->cleanUpTables(); - } - - #[\Override] - protected function tearDown(): void { - $this->cleanUpTables(); - parent::tearDown(); - } - - private function cleanUpTables(): void { - $this->connection->getQueryBuilder()->delete('systemtag_object_mapping')->executeStatement(); - $this->connection->getQueryBuilder()->delete('systemtag')->executeStatement(); - } - - public function testMigrationsAvailableFalseWhenAllClean(): void { - $this->addTag('Clean'); - $this->addTag('Also clean'); - - $this->assertFalse($this->repair->migrationsAvailable()); - } - - public function testMigrationsAvailableTrueWhenNameNeedsSanitizing(): void { - $this->addTag('Clean'); - $this->addTag(' Needs trimming '); - - $this->assertTrue($this->repair->migrationsAvailable()); - } - - public function testMigrationsAvailableIgnoresFalsyName(): void { - // A PHP-falsy name ('0') must not stop the scan before later dirty tags are - // seen. We avoid '' here because Oracle stores it as NULL, which the notnull - // name column rejects. - $this->addTag('0'); - $this->addTag('Needs trimming'); - - $this->assertTrue($this->repair->migrationsAvailable()); - } - - public function testRunSanitizesSingleTag(): void { - // Leading/trailing whitespace is trimmed and inner runs collapse to a single space - $id = $this->addTag(' My Tag '); - - $this->repair->run($this->output); - - $this->assertSame('My Tag', $this->getTagName($id)); - $this->assertFalse($this->repair->migrationsAvailable()); - } - - public function testRunMergesDuplicatesAndReassignsMappings(): void { - // Both sanitize to 'Report card', same visibility/editable, so they must merge. - // The variants differ by an inner double space, which is significant in the DB - // unique index (unlike trailing spaces, which MySQL/Oracle ignore). - $keep = $this->addTag('Report card'); - $dup = $this->addTag('Report card'); - - // The kept tag is the one with the most object mappings - $this->addMapping($keep, '1'); - $this->addMapping($keep, '2'); - $this->addMapping($dup, '3'); - - $this->repair->run($this->output); - - // Only one tag remains, named 'Report card' - $this->assertSame(1, $this->countTags()); - $this->assertSame('Report card', $this->getTagName($keep)); - $this->assertNull($this->getTagName($dup)); - - // The duplicate's mapping was reassigned to the kept tag - $this->assertSame(3, $this->countMappings($keep)); - } - - public function testRunKeepsTagWithMostMappings(): void { - $few = $this->addTag('Photo album'); - $many = $this->addTag('Photo album'); - - $this->addMapping($few, '1'); - $this->addMapping($many, '2'); - $this->addMapping($many, '3'); - - $this->repair->run($this->output); - - $this->assertNull($this->getTagName($few)); - $this->assertSame('Photo album', $this->getTagName($many)); - $this->assertSame(3, $this->countMappings($many)); - } - - public function testRunDeletesConflictingMappings(): void { - $a = $this->addTag('Draft doc'); - $b = $this->addTag('Draft doc'); - - // Object '1' is mapped to both tags. After the merge that conflict must be - // deduplicated: the surviving tag ends up with objects '1' and '2' only once each. - $this->addMapping($a, '1'); - $this->addMapping($b, '1'); - $this->addMapping($b, '2'); - - $this->repair->run($this->output); - - // A single tag remains and holds exactly two mappings (no duplicated object '1') - $this->assertSame(1, $this->countTags()); - $this->assertSame(2, $this->countAllMappings()); - } - - public function testRunSkipsMergeWhenVisibilityDiffers(): void { - // Same sanitized name but different visibility → must not merge, only warn - $a = $this->addTag('Shared note', visibility: 1); - $b = $this->addTag('Shared note', visibility: 0); - - $this->output->expects($this->atLeastOnce())->method('warning'); - - $this->repair->run($this->output); - - $this->assertSame(2, $this->countTags()); - $this->assertNotNull($this->getTagName($a)); - $this->assertNotNull($this->getTagName($b)); - } - - private function addTag(string $name, int $visibility = 1, int $editable = 1): int { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('systemtag') - ->values([ - 'name' => $qb->createNamedParameter($name), - 'visibility' => $qb->createNamedParameter($visibility, IQueryBuilder::PARAM_INT), - 'editable' => $qb->createNamedParameter($editable, IQueryBuilder::PARAM_INT), - ]) - ->executeStatement(); - - return $qb->getLastInsertId(); - } - - private function addMapping(int $tagId, string $objectId, string $objectType = 'files'): void { - $qb = $this->connection->getQueryBuilder(); - $qb->insert('systemtag_object_mapping') - ->values([ - 'objectid' => $qb->createNamedParameter($objectId), - 'objecttype' => $qb->createNamedParameter($objectType), - 'systemtagid' => $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_INT), - ]) - ->executeStatement(); - } - - private function getTagName(int $id): ?string { - $qb = $this->connection->getQueryBuilder(); - $name = $qb->select('name') - ->from('systemtag') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->executeQuery() - ->fetchOne(); - - return $name === false ? null : $name; - } - - private function countTags(): int { - $qb = $this->connection->getQueryBuilder(); - return (int)$qb->select($qb->func()->count('*')) - ->from('systemtag') - ->executeQuery() - ->fetchOne(); - } - - private function countMappings(int $tagId): int { - $qb = $this->connection->getQueryBuilder(); - return (int)$qb->select($qb->func()->count('*')) - ->from('systemtag_object_mapping') - ->where($qb->expr()->eq('systemtagid', $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_INT))) - ->executeQuery() - ->fetchOne(); - } - - private function countAllMappings(): int { - $qb = $this->connection->getQueryBuilder(); - return (int)$qb->select($qb->func()->count('*')) - ->from('systemtag_object_mapping') - ->executeQuery() - ->fetchOne(); - } -} --- a/tests/lib/RepairTest.php +++ /dev/null @@ -1,142 +0,0 @@ -warning) { - $out->warning('Simulated warning'); - } else { - $out->info('Simulated info'); - } - } -} - -class RepairTest extends TestCase { - private Repair $repair; - - /** @var string[] */ - private array $outputArray = []; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - $dispatcher = Server::get(IEventDispatcher::class); - $this->repair = new Repair($dispatcher, $this->createMock(LoggerInterface::class)); - - $dispatcher->addListener(RepairWarningEvent::class, function (RepairWarningEvent $event): void { - $this->outputArray[] = 'warning: ' . $event->getMessage(); - }); - $dispatcher->addListener(RepairInfoEvent::class, function (RepairInfoEvent $event): void { - $this->outputArray[] = 'info: ' . $event->getMessage(); - }); - $dispatcher->addListener(RepairStepEvent::class, function (RepairStepEvent $event): void { - $this->outputArray[] = 'step: ' . $event->getStepName(); - }); - $dispatcher->addListener(RepairErrorEvent::class, function (RepairErrorEvent $event): void { - $this->outputArray[] = 'error: ' . $event->getMessage(); - }); - } - - public function testRunRepairStep(): void { - $this->repair->addStep(new TestRepairStep(false)); - $this->repair->run(); - - $this->assertEquals( - [ - 'step: Test Name', - 'info: Simulated info', - ], - $this->outputArray - ); - } - - public function testRunRepairStepThatFail(): void { - $this->repair->addStep(new TestRepairStep(true)); - $this->repair->run(); - - $this->assertEquals( - [ - 'step: Test Name', - 'warning: Simulated warning', - ], - $this->outputArray - ); - } - - public function testRunRepairStepsWithException(): void { - $mock = $this->createMock(TestRepairStep::class); - $mock->expects($this->any()) - ->method('run') - ->willThrowException(new \Exception('Exception text')); - $mock->expects($this->any()) - ->method('getName') - ->willReturn('Exception Test'); - - $this->repair->addStep($mock); - $this->repair->addStep(new TestRepairStep(false)); - - $thrown = false; - try { - $this->repair->run(); - } catch (\Exception $e) { - $thrown = true; - } - - $this->assertFalse($thrown); - // jump out after exception - $this->assertEquals( - [ - 'step: Exception Test', - 'error: Exception text', - 'step: Test Name', - 'info: Simulated info', - ], - $this->outputArray - ); - } - - public function testRunRepairStepsContinueAfterWarning(): void { - $this->repair->addStep(new TestRepairStep(true)); - $this->repair->addStep(new TestRepairStep(false)); - $this->repair->run(); - - $this->assertEquals( - [ - 'step: Test Name', - 'warning: Simulated warning', - 'step: Test Name', - 'info: Simulated info', - ], - $this->outputArray - ); - } -} --- a/tests/lib/RichObjectStrings/DefinitionsTest.php +++ /dev/null @@ -1,75 +0,0 @@ -definitions as $type => $definition) { - $testsuite[] = [$type, $definition]; - } - return $testsuite; - } - - public function testGetDefinitionNotExisting(): void { - $this->expectException(InvalidObjectExeption::class); - $this->expectExceptionMessage('Object type is undefined'); - - $definitions = new Definitions(); - $definitions->getDefinition('NotExistingType'); - } - - /** - * @param string $type - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetDefinition')] - public function testGetDefinition($type, array $expected): void { - $definitions = new Definitions(); - $definition = $definitions->getDefinition($type); - - $this->assertEquals($expected, $definition); - $this->assertArrayHasKey('author', $definition); - $this->assertNotEquals('', $definition['author'], 'Author of definition must not be empty'); - $this->assertArrayHasKey('app', $definition); - $this->assertNotEquals('', $definition['app'], 'App of definition must not be empty'); - $this->assertArrayHasKey('since', $definition); - $this->assertNotEmpty($definition['since'], 'Since of definition must not be empty'); - $this->assertArrayHasKey('parameters', $definition); - $this->assertTrue(is_array($definition['parameters']), 'Parameters of definition must be of type array'); - $this->assertNotEmpty($definition['parameters'], 'Parameters of definition must not be empty'); - - $this->assertArrayHasKey('id', $definition['parameters'], 'Parameter ID must be defined'); - $this->assertArrayHasKey('name', $definition['parameters'], 'Parameter name must be defined'); - - foreach ($definition['parameters'] as $parameter => $data) { - $this->validateParameter($parameter, $data); - } - } - - public function validateParameter($parameter, $data) { - $this->assertTrue(is_array($data), 'Parameter ' . $parameter . ' is invalid'); - $this->assertArrayHasKey('since', $data); - $this->assertNotEmpty($data['since'], 'Since of parameter ' . $parameter . ' must not be empty'); - $this->assertArrayHasKey('required', $data); - $this->assertTrue(is_bool($data['required']), 'Required of parameter ' . $parameter . ' must be a boolean'); - if ($parameter === 'id' || $parameter === 'name') { - $this->assertTrue($data['required'], 'Parameter ' . $parameter . ' must be required'); - } - - $this->assertArrayHasKey('description', $data); - $this->assertNotEquals('', $data['description'], 'Description of parameter ' . $parameter . ' must not be empty'); - $this->assertArrayHasKey('example', $data); - $this->assertNotEquals('', $data['example'], 'Example of parameter ' . $parameter . ' must not be empty'); - } -} --- a/tests/lib/RichObjectStrings/ValidatorTest.php +++ /dev/null @@ -1,103 +0,0 @@ -validate('test', []); - $v->validate('test {string1} test {foo} test {bar}.', [ - 'string1' => [ - 'type' => 'user', - 'id' => 'johndoe', - 'name' => 'John Doe', - ], - 'foo' => [ - 'type' => 'user-group', - 'id' => 'sample', - 'name' => 'Sample Group', - ], - 'bar' => [ - 'type' => 'file', - 'id' => '42', - 'name' => 'test.txt', - 'path' => 'path/to/test.txt', - ], - ]); - $this->addToAssertionCount(2); - - $this->expectException(InvalidObjectExeption::class); - - $this->expectExceptionMessage('Object for placeholder string1 is invalid, value 123 for key key is not a string'); - $v->validate('test {string1} test.', [ - 'string1' => [ - 'type' => 'user', - 'id' => 'johndoe', - 'name' => 'John Doe', - 'key' => 123, - ], - ]); - - $this->expectExceptionMessage('Object for placeholder string1 is invalid, key 456 is not a string'); - $v->validate('test {string1} test.', [ - 'string1' => [ - 'type' => 'user', - 'id' => 'johndoe', - 'name' => 'John Doe', - 456 => 'value', - ], - ]); - } - - public static function dataValidateParameterKeys(): array { - return [ - 'not a string' => ['key' => 0, 'throws' => 'Parameter key is invalid'], - '@ is not allowed' => ['key' => 'user@0', 'throws' => 'Parameter key is invalid'], - '? is not allowed' => ['key' => 'user?0', 'throws' => 'Parameter key is invalid'], - 'slash is not allowed' => ['key' => 'user/0', 'throws' => 'Parameter key is invalid'], - 'backslash is not allowed' => ['key' => 'user\\0', 'throws' => 'Parameter key is invalid'], - 'hash is not allowed' => ['key' => 'user#0', 'throws' => 'Parameter key is invalid'], - 'space is not allowed' => ['key' => 'user 0', 'throws' => 'Parameter key is invalid'], - 'has to start with letter, but is number' => ['key' => '0abc', 'throws' => 'Parameter key is invalid'], - 'has to start with letter, but is dot' => ['key' => '.abc', 'throws' => 'Parameter key is invalid'], - 'has to start with letter, but is slash' => ['key' => '-abc', 'throws' => 'Parameter key is invalid'], - 'has to start with letter, but is underscore' => ['key' => '_abc', 'throws' => 'Parameter key is invalid'], - ['key' => 'user-0', 'throws' => null], - ['key' => 'user_0', 'throws' => null], - ['key' => 'user.0', 'throws' => null], - ['key' => 'a._-0', 'throws' => null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataValidateParameterKeys')] - public function testValidateParameterKeys(mixed $key, ?string $throws): void { - - if ($throws !== null) { - $this->expectExceptionMessage($throws); - } - - $v = new Validator(new Definitions()); - $v->validate('{' . $key . '}', [ - $key => [ - 'type' => 'highlight', - 'id' => 'identifier', - 'name' => 'Display name', - ], - ]); - - if ($throws === null) { - $this->addToAssertionCount(1); - } - } -} --- a/tests/lib/Route/RouterTest.php +++ /dev/null @@ -1,79 +0,0 @@ -createMock(LoggerInterface::class); - $logger->method('info') - ->willReturnCallback( - function (string $message, array $data): void { - $this->fail('Unexpected info log: ' . (string)($data['exception'] ?? $message)); - } - ); - - $this->appManager = $this->createMock(IAppManager::class); - - $this->router = new Router( - $logger, - $this->createMock(IRequest::class), - $this->createMock(IConfig::class), - $this->createMock(IEventLogger::class), - $this->createMock(ContainerInterface::class), - $this->appManager, - ); - } - - public function testHeartbeat(): void { - $this->assertEquals('/index.php/heartbeat', $this->router->generate('heartbeat')); - } - - public function testGenerateConsecutively(): void { - $this->appManager->expects(self::atLeastOnce()) - ->method('cleanAppId') - ->willReturnArgument(0); - $this->appManager->expects(self::atLeastOnce()) - ->method('getAppPath') - ->willReturnCallback(fn (string $appid): string => \OC::$SERVERROOT . '/apps/' . $appid); - $this->appManager->expects(self::atLeastOnce()) - ->method('isAppLoaded') - ->willReturn(true); - - $this->assertEquals('/index.php/apps/files/', $this->router->generate('files.view.index')); - - // the OCS route is the prefixed one for the AppFramework - see /ocs/v1.php for routing details - $this->assertEquals('/index.php/ocsapp/apps/dav/api/v1/direct', $this->router->generate('ocs.dav.direct.getUrl')); - - // test caching - $this->assertEquals('/index.php/apps/files/', $this->router->generate('files.view.index')); - } -} --- a/tests/lib/Search/SearchComposerTest.php +++ /dev/null @@ -1,291 +0,0 @@ -bootstrapCoordinator = $this->createMock(Coordinator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->searchComposer = new SearchComposer( - $this->bootstrapCoordinator, - $this->container, - $this->urlGenerator, - $this->logger, - $this->appConfig - ); - - $this->setupUrlGenerator(); - } - - private function setupUrlGenerator(): void { - $this->urlGenerator->method('imagePath') - ->willReturnCallback(function ($appId, $imageName) { - return "/apps/$appId/img/$imageName"; - }); - } - - private function setupEmptyRegistrationContext(): void { - $this->bootstrapCoordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn(null); - } - - private function setupAppConfigForAllowedProviders(array $allowedProviders = []): void { - $this->appConfig->method('getValueArray') - ->with('core', 'unified_search.providers_allowed') - ->willReturn($allowedProviders); - } - - /** - * @param array $providerConfigs - * @return array{registrations: ServiceRegistration[], providers: IProvider[]} - */ - private function createMockProvidersAndRegistrations(array $providerConfigs): array { - $registrations = []; - $providers = []; - $containerMap = []; - - foreach ($providerConfigs as $providerId => $config) { - // Create registration mock - $registration = $this->createMock(ServiceRegistration::class); - $registration->method('getService')->willReturn($config['service']); - $registration->method('getAppId')->willReturn($config['appId']); - $registrations[] = $registration; - - // Create provider mock - $providerClass = $config['isInApp'] ?? false ? IInAppSearch::class : IProvider::class; - $provider = $this->createMock($providerClass); - $provider->method('getId')->willReturn($providerId); - $provider->method('getName')->willReturn("Provider $providerId"); - $provider->method('getOrder')->willReturn($config['order']); - - $providers[$providerId] = $provider; - $containerMap[] = [$config['service'], $provider]; - } - - $this->container->expects($this->exactly(count($providerConfigs))) - ->method('get') - ->willReturnMap($containerMap); - - return ['registrations' => $registrations, 'providers' => $providers]; - } - - private function setupRegistrationContextWithProviders(array $registrations): void { - $registrationContext = $this->createMock(RegistrationContext::class); - $registrationContext->method('getSearchProviders')->willReturn($registrations); - - $this->bootstrapCoordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - } - - public function testGetProvidersWithNoRegisteredProviders(): void { - $this->setupEmptyRegistrationContext(); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertIsArray($providers); - $this->assertEmpty($providers); - } - - public function testSearchWithUnknownProvider(): void { - $this->setupEmptyRegistrationContext(); - - $user = $this->createMock(IUser::class); - $query = $this->createMock(ISearchQuery::class); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Provider unknown_provider is unknown'); - - $this->searchComposer->search($user, 'unknown_provider', $query); - } - - public function testGetProvidersWithMultipleProviders(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 10], - 'provider2' => ['service' => 'provider2_service', 'appId' => 'app2', 'order' => 5], - 'provider3' => ['service' => 'provider3_service', 'appId' => 'app3', 'order' => 15, 'isInApp' => true], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertProvidersStructureAndSorting($providers, [ - ['id' => 'provider2', 'name' => 'Provider provider2', 'appId' => 'app2', 'order' => 5, 'inAppSearch' => false], - ['id' => 'provider1', 'name' => 'Provider provider1', 'appId' => 'app1', 'order' => 10, 'inAppSearch' => false], - ['id' => 'provider3', 'name' => 'Provider provider3', 'appId' => 'app3', 'order' => 15, 'inAppSearch' => true], - ]); - } - - public function testGetProvidersWithEmptyAllowedProvidersConfiguration(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 10], - 'provider2' => ['service' => 'provider2_service', 'appId' => 'app2', 'order' => 5], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertCount(2, $providers); - $this->assertProvidersAreSortedByOrder($providers); - $this->assertEquals('provider2', $providers[0]['id']); - $this->assertEquals('provider1', $providers[1]['id']); - } - - public function testGetProvidersWithAllowedProvidersRestriction(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 10], - 'provider2' => ['service' => 'provider2_service', 'appId' => 'app2', 'order' => 5], - 'provider3' => ['service' => 'provider3_service', 'appId' => 'app3', 'order' => 15], - 'provider4' => ['service' => 'provider4_service', 'appId' => 'app4', 'order' => 8], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(['provider1', 'provider3']); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertProvidersStructureAndSorting($providers, [ - ['id' => 'provider1', 'name' => 'Provider provider1', 'appId' => 'app1', 'order' => 10, 'inAppSearch' => false], - ['id' => 'provider3', 'name' => 'Provider provider3', 'appId' => 'app3', 'order' => 15, 'inAppSearch' => false], - ]); - - // Verify excluded providers are not present - $providerIds = array_column($providers, 'id'); - $this->assertNotContains('provider2', $providerIds); - $this->assertNotContains('provider4', $providerIds); - } - - public function testGetProvidersFiltersByAllowedProvidersCompletely(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 10], - 'provider2' => ['service' => 'provider2_service', 'appId' => 'app2', 'order' => 5], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(['provider_not_exists']); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertIsArray($providers); - $this->assertEmpty($providers); - } - - public function testGetProvidersWithMixedOrderValues(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 100], - 'provider2' => ['service' => 'provider2_service', 'appId' => 'app2', 'order' => 1], - 'provider3' => ['service' => 'provider3_service', 'appId' => 'app3', 'order' => 50], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertCount(3, $providers); - $this->assertProvidersAreSortedByOrder($providers); - - // Verify correct ordering: provider2 (1), provider3 (50), provider1 (100) - $this->assertEquals('provider2', $providers[0]['id']); - $this->assertEquals('provider3', $providers[1]['id']); - $this->assertEquals('provider1', $providers[2]['id']); - } - - public function testProviderIconGeneration(): void { - $providerConfigs = [ - 'provider1' => ['service' => 'provider1_service', 'appId' => 'app1', 'order' => 10], - ]; - - $mockData = $this->createMockProvidersAndRegistrations($providerConfigs); - $this->setupRegistrationContextWithProviders($mockData['registrations']); - $this->setupAppConfigForAllowedProviders(); - - $providers = $this->searchComposer->getProviders('/test/route', []); - - $this->assertCount(1, $providers); - $this->assertArrayHasKey('icon', $providers[0]); - $this->assertStringContainsString('/apps/provider1/img/provider1.svg', $providers[0]['icon']); - } - - /** - * Assert providers array structure and expected sorting - */ - private function assertProvidersStructureAndSorting(array $actualProviders, array $expectedProviders): void { - $this->assertIsArray($actualProviders); - $this->assertCount(count($expectedProviders), $actualProviders); - - foreach ($actualProviders as $index => $provider) { - $this->assertProviderHasRequiredFields($provider); - - $expected = $expectedProviders[$index]; - $this->assertEquals($expected['id'], $provider['id']); - $this->assertEquals($expected['name'], $provider['name']); - $this->assertEquals($expected['appId'], $provider['appId']); - $this->assertEquals($expected['order'], $provider['order']); - $this->assertEquals($expected['inAppSearch'], $provider['inAppSearch']); - } - - $this->assertProvidersAreSortedByOrder($actualProviders); - } - - private function assertProviderHasRequiredFields(array $provider): void { - $requiredFields = ['id', 'appId', 'name', 'icon', 'order', 'triggers', 'filters', 'inAppSearch']; - foreach ($requiredFields as $field) { - $this->assertArrayHasKey($field, $provider, "Provider must have '$field' field"); - } - } - - private function assertProvidersAreSortedByOrder(array $providers): void { - $orders = array_column($providers, 'order'); - $sortedOrders = $orders; - sort($sortedOrders); - $this->assertEquals($sortedOrders, $orders, 'Providers should be sorted by order'); - } -} --- a/tests/lib/Security/Bruteforce/Backend/MemoryCacheBackendTest.php +++ /dev/null @@ -1,138 +0,0 @@ -cacheFactory = $this->createMock(ICacheFactory::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->cache = $this->createMock(ICache::class); - - $this->cacheFactory - ->expects($this->once()) - ->method('createDistributed') - ->with(MemoryCacheBackend::class) - ->willReturn($this->cache); - - $this->backend = new MemoryCacheBackend( - $this->cacheFactory, - $this->timeFactory - ); - } - - public function testGetAttemptsWithNoAttemptsBefore(): void { - $this->cache - ->expects($this->once()) - ->method('get') - ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') - ->willReturn(null); - - $this->assertSame(0, $this->backend->getAttempts('10.10.10.10/32', 0)); - } - - public static function dataGetAttempts(): array { - return [ - [0, null, null, 4], - [100, null, null, 2], - [0, 'action1', null, 2], - [100, 'action1', null, 1], - [0, 'action1', ['metadata2'], 1], - [100, 'action1', ['metadata2'], 1], - [100, 'action1', ['metadata1'], 0], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetAttempts')] - public function testGetAttempts(int $maxAge, ?string $action, ?array $metadata, int $expected): void { - $this->cache - ->expects($this->once()) - ->method('get') - ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') - ->willReturn(json_encode([ - '1' . '#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1'])), - '300' . '#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata2'])), - '1' . '#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata1'])), - '300' . '#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata2'])), - ])); - - $this->assertSame($expected, $this->backend->getAttempts('10.10.10.10/32', $maxAge, $action, $metadata)); - } - - public function testRegisterAttemptWithNoAttemptsBefore(): void { - $this->cache - ->expects($this->once()) - ->method('get') - ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') - ->willReturn(null); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - '8b9da631d1f7b022bb2c3c489e16092f82b42fd4', - json_encode(['223#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1']))]) - ); - - $this->backend->registerAttempt('10.10.10.10', '10.10.10.10/32', 223, 'action1', ['metadata1']); - } - - public function testRegisterAttempt(): void { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(12 * 3600 + 86); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') - ->willReturn(json_encode([ - '1#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1'])), - '2#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata1'])), - '87#' . hash('sha1', 'action3') . '#' . hash('sha1', json_encode(['metadata1'])), - '123#' . hash('sha1', 'action4') . '#' . hash('sha1', json_encode(['metadata1'])), - '123#' . hash('sha1', 'action5') . '#' . hash('sha1', json_encode(['metadata1'])), - '124#' . hash('sha1', 'action6') . '#' . hash('sha1', json_encode(['metadata1'])), - ])); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - '8b9da631d1f7b022bb2c3c489e16092f82b42fd4', - json_encode([ - '87#' . hash('sha1', 'action3') . '#' . hash('sha1', json_encode(['metadata1'])), - '123#' . hash('sha1', 'action4') . '#' . hash('sha1', json_encode(['metadata1'])), - '123#' . hash('sha1', 'action5') . '#' . hash('sha1', json_encode(['metadata1'])), - '124#' . hash('sha1', 'action6') . '#' . hash('sha1', json_encode(['metadata1'])), - '186#' . hash('sha1', 'action7') . '#' . hash('sha1', json_encode(['metadata2'])), - ]) - ); - - $this->backend->registerAttempt('10.10.10.10', '10.10.10.10/32', 186, 'action7', ['metadata2']); - } -} --- a/tests/lib/Security/Bruteforce/CapabilitiesTest.php +++ /dev/null @@ -1,85 +0,0 @@ -request = $this->createMock(IRequest::class); - - $this->throttler = $this->createMock(IThrottler::class); - - $this->capabilities = new Capabilities( - $this->request, - $this->throttler - ); - } - - public function testGetCapabilities(): void { - $this->throttler->expects($this->atLeastOnce()) - ->method('getDelay') - ->with('10.10.10.10') - ->willReturn(42); - - $this->throttler->expects($this->atLeastOnce()) - ->method('isBypassListed') - ->with('10.10.10.10') - ->willReturn(true); - - $this->request->method('getRemoteAddress') - ->willReturn('10.10.10.10'); - - $expected = [ - 'bruteforce' => [ - 'delay' => 42, - 'allow-listed' => true, - ] - ]; - $result = $this->capabilities->getCapabilities(); - - $this->assertEquals($expected, $result); - } - - public function testGetCapabilitiesOnCli(): void { - $this->throttler->expects($this->atLeastOnce()) - ->method('getDelay') - ->with('') - ->willReturn(0); - - $this->request->method('getRemoteAddress') - ->willReturn(''); - - $expected = [ - 'bruteforce' => [ - 'delay' => 0, - 'allow-listed' => false, - ] - ]; - $result = $this->capabilities->getCapabilities(); - - $this->assertEquals($expected, $result); - } -} --- a/tests/lib/Security/CSP/AddContentSecurityPolicyEventTest.php +++ /dev/null @@ -1,29 +0,0 @@ -createMock(ContentSecurityPolicyManager::class); - $policy = $this->createMock(ContentSecurityPolicy::class); - $event = new AddContentSecurityPolicyEvent($cspManager); - - $cspManager->expects($this->once()) - ->method('addDefaultPolicy') - ->with($policy); - - $event->addPolicy($policy); - } -} --- a/tests/lib/Security/CSP/ContentSecurityPolicyManagerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -dispatcher = Server::get(IEventDispatcher::class); - $this->contentSecurityPolicyManager = new ContentSecurityPolicyManager($this->dispatcher); - } - - public function testAddDefaultPolicy(): void { - $this->contentSecurityPolicyManager->addDefaultPolicy(new ContentSecurityPolicy()); - $this->addToAssertionCount(1); - } - - public function testGetDefaultPolicyWithPolicies(): void { - $policy = new ContentSecurityPolicy(); - $policy->addAllowedFontDomain('mydomain.com'); - $policy->addAllowedImageDomain('anotherdomain.de'); - $this->contentSecurityPolicyManager->addDefaultPolicy($policy); - $policy = new ContentSecurityPolicy(); - $policy->addAllowedFontDomain('example.com'); - $policy->addAllowedImageDomain('example.org'); - $this->contentSecurityPolicyManager->addDefaultPolicy($policy); - $policy = new EmptyContentSecurityPolicy(); - $policy->addAllowedFontDomain('anotherFontDomain'); - $policy->addAllowedFormActionDomain('thirdDomain'); - $this->contentSecurityPolicyManager->addDefaultPolicy($policy); - - $expected = new \OC\Security\CSP\ContentSecurityPolicy(); - $expected->addAllowedFontDomain('mydomain.com'); - $expected->addAllowedFontDomain('example.com'); - $expected->addAllowedFontDomain('anotherFontDomain'); - $expected->addAllowedFormActionDomain('thirdDomain'); - $expected->addAllowedImageDomain('anotherdomain.de'); - $expected->addAllowedImageDomain('example.org'); - $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self' thirdDomain"; - - $this->assertEquals($expected, $this->contentSecurityPolicyManager->getDefaultPolicy()); - $this->assertSame($expectedStringPolicy, $this->contentSecurityPolicyManager->getDefaultPolicy()->buildPolicy()); - } - - public function testGetDefaultPolicyWithPoliciesViaEvent(): void { - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { - $policy = new ContentSecurityPolicy(); - $policy->addAllowedFontDomain('mydomain.com'); - $policy->addAllowedImageDomain('anotherdomain.de'); - $policy->useStrictDynamic(true); - - $e->addPolicy($policy); - }); - - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { - $policy = new ContentSecurityPolicy(); - $policy->addAllowedFontDomain('example.com'); - $policy->addAllowedImageDomain('example.org'); - $e->addPolicy($policy); - }); - - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { - $policy = new EmptyContentSecurityPolicy(); - $policy->addAllowedFontDomain('anotherFontDomain'); - $policy->addAllowedFormActionDomain('thirdDomain'); - $e->addPolicy($policy); - }); - - $expected = new \OC\Security\CSP\ContentSecurityPolicy(); - $expected->addAllowedFontDomain('mydomain.com'); - $expected->addAllowedFontDomain('example.com'); - $expected->addAllowedFontDomain('anotherFontDomain'); - $expected->addAllowedImageDomain('anotherdomain.de'); - $expected->addAllowedImageDomain('example.org'); - $expected->addAllowedFormActionDomain('thirdDomain'); - $expected->useStrictDynamic(true); - $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self' thirdDomain"; - - $this->assertEquals($expected, $this->contentSecurityPolicyManager->getDefaultPolicy()); - $this->assertSame($expectedStringPolicy, $this->contentSecurityPolicyManager->getDefaultPolicy()->buildPolicy()); - } -} --- a/tests/lib/Security/CSP/ContentSecurityPolicyNonceManagerTest.php +++ /dev/null @@ -1,74 +0,0 @@ -CSRFTokenManager = $this->createMock(CsrfTokenManager::class); - $this->request = $this->createMock(Request::class); - $this->nonceManager = new ContentSecurityPolicyNonceManager( - $this->CSRFTokenManager, - $this->request - ); - } - - public function testGetNonce(): void { - $secret = base64_encode('secret'); - $tokenValue = base64_encode('secret' ^ 'value_') . ':' . $secret; - $token = $this->createMock(CsrfToken::class); - $token - ->expects($this->once()) - ->method('getEncryptedValue') - ->willReturn($tokenValue); - - $this->CSRFTokenManager - ->expects($this->once()) - ->method('getToken') - ->willReturn($token); - - $this->assertSame($secret, $this->nonceManager->getNonce()); - // call it twice but `getEncryptedValue` is expected to be called only once - $this->assertSame($secret, $this->nonceManager->getNonce()); - } - - public function testGetNonceServerVar(): void { - $token = 'SERVERNONCE'; - $this->request - ->method('__isset') - ->with('server') - ->willReturn(true); - - $this->request - ->method('__get') - ->with('server') - ->willReturn(['CSP_NONCE' => $token]); - - $this->assertSame($token, $this->nonceManager->getNonce()); - // call it twice but `CSP_NONCE` variable is expected to be loaded only once - $this->assertSame($token, $this->nonceManager->getNonce()); - } -} --- a/tests/lib/Security/CSRF/CsrfTokenGeneratorTest.php +++ /dev/null @@ -1,47 +0,0 @@ -random = $this->getMockBuilder('\OCP\Security\ISecureRandom') - ->disableOriginalConstructor()->getMock(); - $this->csrfTokenGenerator = new CsrfTokenGenerator($this->random); - } - - public function testGenerateTokenWithCustomNumber(): void { - $this->random - ->expects($this->once()) - ->method('generate') - ->with(3) - ->willReturn('abc'); - $this->assertSame('abc', $this->csrfTokenGenerator->generateToken(3)); - } - - public function testGenerateTokenWithDefault(): void { - $this->random - ->expects($this->once()) - ->method('generate') - ->with(32) - ->willReturn('12345678901234567890123456789012'); - $this->assertSame('12345678901234567890123456789012', $this->csrfTokenGenerator->generateToken(32)); - } -} --- a/tests/lib/Security/CSRF/CsrfTokenManagerTest.php +++ /dev/null @@ -1,151 +0,0 @@ -tokenGenerator = $this->getMockBuilder('\OC\Security\CSRF\CsrfTokenGenerator') - ->disableOriginalConstructor()->getMock(); - $this->storageInterface = $this->getMockBuilder('\OC\Security\CSRF\TokenStorage\SessionStorage') - ->disableOriginalConstructor()->getMock(); - - $this->csrfTokenManager = new CsrfTokenManager( - $this->tokenGenerator, - $this->storageInterface - ); - } - - public function testGetTokenWithExistingToken(): void { - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(true); - $this->storageInterface - ->expects($this->once()) - ->method('getToken') - ->willReturn('MyExistingToken'); - - $expected = new CsrfToken('MyExistingToken'); - $this->assertEquals($expected, $this->csrfTokenManager->getToken()); - } - - public function testGetTokenWithExistingTokenKeepsOnSecondRequest(): void { - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(true); - $this->storageInterface - ->expects($this->once()) - ->method('getToken') - ->willReturn('MyExistingToken'); - - $expected = new CsrfToken('MyExistingToken'); - $token = $this->csrfTokenManager->getToken(); - $this->assertSame($token, $this->csrfTokenManager->getToken()); - $this->assertSame($token, $this->csrfTokenManager->getToken()); - } - - public function testGetTokenWithoutExistingToken(): void { - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(false); - $this->tokenGenerator - ->expects($this->once()) - ->method('generateToken') - ->willReturn('MyNewToken'); - $this->storageInterface - ->expects($this->once()) - ->method('setToken') - ->with('MyNewToken'); - - $expected = new CsrfToken('MyNewToken'); - $this->assertEquals($expected, $this->csrfTokenManager->getToken()); - } - - public function testRefreshToken(): void { - $this->tokenGenerator - ->expects($this->once()) - ->method('generateToken') - ->willReturn('MyNewToken'); - $this->storageInterface - ->expects($this->once()) - ->method('setToken') - ->with('MyNewToken'); - - $expected = new CsrfToken('MyNewToken'); - $this->assertEquals($expected, $this->csrfTokenManager->refreshToken()); - } - - public function testRemoveToken(): void { - $this->storageInterface - ->expects($this->once()) - ->method('removeToken'); - - $this->csrfTokenManager->removeToken(); - } - - public function testIsTokenValidWithoutToken(): void { - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(false); - $token = new CsrfToken('Token'); - - $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); - } - - public function testIsTokenValidWithWrongToken(): void { - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(true); - $token = new CsrfToken('Token'); - $this->storageInterface - ->expects($this->once()) - ->method('getToken') - ->willReturn('MyToken'); - - $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); - } - - public function testIsTokenValidWithValidToken(): void { - $a = 'abc'; - $b = 'def'; - $xorB64 = 'BQcF'; - $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); - $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(true); - $token = new CsrfToken($tokenVal); - $this->storageInterface - ->expects($this->once()) - ->method('getToken') - ->willReturn($b); - - $this->assertSame(true, $this->csrfTokenManager->isTokenValid($token)); - } -} --- a/tests/lib/Security/CSRF/CsrfTokenTest.php +++ /dev/null @@ -1,37 +0,0 @@ -assertSame(33, strlen($csrfToken->getEncryptedValue())); - $this->assertSame(':', $csrfToken->getEncryptedValue()[16]); - } - - public function testGetEncryptedValueStaysSameOnSecondRequest(): void { - $csrfToken = new CsrfToken('MyCsrfToken'); - $tokenValue = $csrfToken->getEncryptedValue(); - $this->assertSame($tokenValue, $csrfToken->getEncryptedValue()); - $this->assertSame($tokenValue, $csrfToken->getEncryptedValue()); - } - - public function testGetDecryptedValue(): void { - $a = 'abc'; - $b = 'def'; - $xorB64 = 'BQcF'; - $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); - $csrfToken = new CsrfToken($tokenVal); - $this->assertSame($b, $csrfToken->getDecryptedValue()); - } -} --- a/tests/lib/Security/CSRF/TokenStorage/SessionStorageTest.php +++ /dev/null @@ -1,114 +0,0 @@ -session = $this->getMockBuilder(ISession::class) - ->disableOriginalConstructor()->getMock(); - $this->sessionStorage = new SessionStorage($this->session); - } - - /** - * @return array - */ - public static function getTokenDataProvider(): array { - return [ - [ - '', - ], - [ - null, - ], - ]; - } - - /** - * @param string $token - * - */ - #[\PHPUnit\Framework\Attributes\DataProvider('getTokenDataProvider')] - public function testGetTokenWithEmptyToken($token): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Session does not contain a requesttoken'); - - $this->session - ->expects($this->once()) - ->method('get') - ->with('requesttoken') - ->willReturn($token); - $this->sessionStorage->getToken(); - } - - public function testGetTokenWithValidToken(): void { - $this->session - ->expects($this->once()) - ->method('get') - ->with('requesttoken') - ->willReturn('MyFancyCsrfToken'); - $this->assertSame('MyFancyCsrfToken', $this->sessionStorage->getToken()); - } - - public function testSetToken(): void { - $this->session - ->expects($this->once()) - ->method('set') - ->with('requesttoken', 'TokenToSet'); - $this->sessionStorage->setToken('TokenToSet'); - } - - public function testRemoveToken(): void { - $this->session - ->expects($this->once()) - ->method('remove') - ->with('requesttoken'); - $this->sessionStorage->removeToken(); - } - - public function testHasTokenWithExistingToken(): void { - $this->session - ->expects($this->once()) - ->method('exists') - ->with('requesttoken') - ->willReturn(true); - $this->assertSame(true, $this->sessionStorage->hasToken()); - } - - public function testHasTokenWithoutExistingToken(): void { - $this->session - ->expects($this->once()) - ->method('exists') - ->with('requesttoken') - ->willReturn(false); - $this->assertSame(false, $this->sessionStorage->hasToken()); - } - - public function testSetSession(): void { - $session = $this->createMock(ISession::class); - $session - ->expects($this->once()) - ->method('get') - ->with('requesttoken') - ->willReturn('MyToken'); - $this->sessionStorage->setSession($session); - $this->assertSame('MyToken', $this->sessionStorage->getToken()); - } -} --- a/tests/lib/Security/CertificateManagerTest.php +++ /dev/null @@ -1,207 +0,0 @@ -username = $this->getUniqueID('', 20); - $this->createUser($this->username, ''); - - $storage = new Temporary(); - $this->registerMount($this->username, $storage, '/' . $this->username . '/'); - - \OC_Util::tearDownFS(); - \OC_User::setUserId($this->username); - Filesystem::tearDown(); - \OC_Util::setupFS($this->username); - - $config = $this->createMock(IConfig::class); - $config->expects($this->any())->method('getSystemValueBool') - ->with('installed', false)->willReturn(true); - $config - ->expects($this->any()) - ->method('getSystemValueString') - ->with('default_certificates_bundle_path', \OC::$SERVERROOT . '/resources/config/ca-bundle.crt') - ->willReturn(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'); - - $this->random = $this->createMock(ISecureRandom::class); - $this->random->method('generate') - ->willReturn('random'); - - $this->certificateManager = new CertificateManager( - new View(), - $config, - $this->createMock(LoggerInterface::class), - $this->random - ); - } - - #[\Override] - protected function tearDown(): void { - $user = Server::get(IUserManager::class)->get($this->username); - if ($user !== null) { - $user->delete(); - } - parent::tearDown(); - } - - protected function assertEqualsArrays($expected, $actual) { - sort($expected); - sort($actual); - - $this->assertEquals($expected, $actual); - } - - public function testListCertificates(): void { - // Test empty certificate bundle - $this->assertSame([], $this->certificateManager->listCertificates()); - - // Add some certificates - $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); - $certificateStore = []; - $certificateStore[] = new Certificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); - $this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates()); - - // Add another certificates - $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate'); - $certificateStore[] = new Certificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate'); - $this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates()); - } - - public function testAddInvalidCertificate(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Certificate could not get parsed.'); - - $this->certificateManager->addCertificate('InvalidCertificate', 'invalidCertificate'); - } - - public static function dangerousFileProvider(): array { - return [ - ['.htaccess'], - ['../../foo.txt'], - ['..\..\foo.txt'], - ]; - } - - /** - * @param string $filename - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dangerousFileProvider')] - public function testAddDangerousFile($filename): void { - $this->expectException(InvalidPathException::class); - $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), $filename); - } - - public function testRemoveDangerousFile(): void { - $this->assertFalse($this->certificateManager->removeCertificate('../../foo.txt')); - } - - public function testRemoveExistingFile(): void { - $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); - $this->assertTrue($this->certificateManager->removeCertificate('GoodCertificate')); - } - - public function testGetCertificateBundle(): void { - $this->assertSame('/files_external/rootcerts.crt', $this->certificateManager->getCertificateBundle()); - } - - /** - * - * @param int $CaBundleMtime - * @param int $targetBundleMtime - * @param int $targetBundleExists - * @param bool $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestNeedRebundling')] - public function testNeedRebundling($CaBundleMtime, - $targetBundleMtime, - $targetBundleExists, - $expected, - ): void { - $view = $this->getMockBuilder(View::class) - ->disableOriginalConstructor()->getMock(); - $config = $this->createMock(IConfig::class); - - /** @var CertificateManager | \PHPUnit\Framework\MockObject\MockObject $certificateManager */ - $certificateManager = $this->getMockBuilder('OC\Security\CertificateManager') - ->setConstructorArgs([$view, $config, $this->createMock(LoggerInterface::class), $this->random]) - ->onlyMethods(['getFilemtimeOfCaBundle', 'getCertificateBundle']) - ->getMock(); - - $certificateManager->expects($this->any())->method('getFilemtimeOfCaBundle') - ->willReturn($CaBundleMtime); - - $certificateManager->expects($this->once())->method('getCertificateBundle') - ->willReturn('targetBundlePath'); - - $view->expects($this->any())->method('file_exists') - ->with('targetBundlePath') - ->willReturn($targetBundleExists); - - $view->expects($this->any())->method('filemtime') - ->willReturnCallback(function ($path) use ($targetBundleMtime) { - if ($path === 'targetBundlePath') { - return $targetBundleMtime; - } - throw new \Exception('unexpected path'); - }); - - $this->assertSame($expected, - $this->invokePrivate($certificateManager, 'needsRebundling') - ); - } - - public static function dataTestNeedRebundling(): array { - return [ - //values: CaBundleMtime, targetBundleMtime, targetBundleExists, expected - - [10, 30, true, false], - [10, 15, true, false], - [10, 8, true, true], - [10, 30, true, false], - [10, 8, true, true], - - // if no target bundle exists we always build a new one - [10, 30, false, true], - [10, 15, false, true], - [10, 8, false, true], - [10, 30, false, true], - [10, 8, false, true], - ]; - } -} --- a/tests/lib/Security/CertificateTest.php +++ /dev/null @@ -1,104 +0,0 @@ -goodCertificate = new Certificate($goodCertificate, 'GoodCertificate'); - $badCertificate = file_get_contents(__DIR__ . '/../../data/certificates/badCertificate.crt'); - $this->invalidCertificate = new Certificate($badCertificate, 'BadCertificate'); - $expiredCertificate = file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'); - $this->expiredCertificate = new Certificate($expiredCertificate, 'ExpiredCertificate'); - } - - public function testBogusData(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Certificate could not get parsed.'); - - $certificate = new Certificate('foo', 'bar'); - $certificate->getIssueDate(); - } - - public function testOpenSslTrustedCertificateFormat(): void { - $trustedCertificate = file_get_contents(__DIR__ . '/../../data/certificates/openSslTrustedCertificate.crt'); - $certificate = new Certificate($trustedCertificate, 'TrustedCertificate'); - $this->assertSame('thawte, Inc.', $certificate->getOrganization()); - } - - public function testCertificateStartingWithFileReference(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Certificate could not get parsed.'); - - new Certificate('file://' . __DIR__ . '/../../data/certificates/goodCertificate.crt', 'bar'); - } - - public function testGetName(): void { - $this->assertSame('GoodCertificate', $this->goodCertificate->getName()); - $this->assertSame('BadCertificate', $this->invalidCertificate->getName()); - } - - public function testGetCommonName(): void { - $this->assertSame('security.nextcloud.com', $this->goodCertificate->getCommonName()); - $this->assertSame(null, $this->invalidCertificate->getCommonName()); - } - - public function testGetOrganization(): void { - $this->assertSame('Nextcloud Security', $this->goodCertificate->getOrganization()); - $this->assertSame('Internet Widgits Pty Ltd', $this->invalidCertificate->getOrganization()); - } - - public function testGetIssueDate(): void { - $expected = new \DateTime('2025-08-25 09:53:14 GMT'); - $this->assertEquals($expected->getTimestamp(), $this->goodCertificate->getIssueDate()->getTimestamp()); - $expected = new \DateTime('2025-08-25 10:00:15 GMT'); - $this->assertEquals($expected->getTimestamp(), $this->invalidCertificate->getIssueDate()->getTimestamp()); - } - - public function testGetExpireDate(): void { - $expected = new \DateTime('2035-08-23 09:53:14 GMT'); - $this->assertEquals($expected->getTimestamp(), $this->goodCertificate->getExpireDate()->getTimestamp()); - $expected = new \DateTime('2035-08-23 10:00:15 GMT'); - $this->assertEquals($expected->getTimestamp(), $this->invalidCertificate->getExpireDate()->getTimestamp()); - $expected = new \DateTime('2014-08-28 09:12:43 GMT'); - $this->assertEquals($expected->getTimestamp(), $this->expiredCertificate->getExpireDate()->getTimestamp()); - } - - public function testIsExpired(): void { - $this->assertSame(false, $this->goodCertificate->isExpired()); - $this->assertSame(false, $this->invalidCertificate->isExpired()); - $this->assertSame(true, $this->expiredCertificate->isExpired()); - } - - public function testGetIssuerName(): void { - $this->assertSame('security.nextcloud.com', $this->goodCertificate->getIssuerName()); - $this->assertSame(null, $this->invalidCertificate->getIssuerName()); - $this->assertSame(null, $this->expiredCertificate->getIssuerName()); - } - - public function testGetIssuerOrganization(): void { - $this->assertSame('Nextcloud Security', $this->goodCertificate->getIssuerOrganization()); - $this->assertSame('Internet Widgits Pty Ltd', $this->invalidCertificate->getIssuerOrganization()); - $this->assertSame('Internet Widgits Pty Ltd', $this->expiredCertificate->getIssuerOrganization()); - } -} --- a/tests/lib/Security/CredentialsManagerTest.php +++ /dev/null @@ -1,59 +0,0 @@ -store($userId, $identifier, $secrets); - $received = $credentialsManager->retrieve($userId, $identifier); - - $this->assertSame($secrets, $received); - - $removedRows = $credentialsManager->delete($userId, $identifier); - $this->assertSame(1, $removedRows); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('credentialsProvider')] - public function testUpdate($userId, $identifier): void { - $credentialsManager = Server::get(ICredentialsManager::class); - - $secrets = 'Open Sesame'; - $secretsRev = strrev($secrets); - - $credentialsManager->store($userId, $identifier, $secrets); - $credentialsManager->store($userId, $identifier, $secretsRev); - $received = $credentialsManager->retrieve($userId, $identifier); - - $this->assertSame($secretsRev, $received); - } - - public static function credentialsProvider(): array { - return [ - [ - 'alice', - 'privateCredentials' - ], - [ - '', - 'systemCredentials', - ], - ]; - } -} --- a/tests/lib/Security/CryptoTest.php +++ /dev/null @@ -1,132 +0,0 @@ -crypto = new Crypto(Server::get(IConfig::class)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('defaultEncryptionProvider')] - public function testDefaultEncrypt($stringToEncrypt): void { - $ciphertext = $this->crypto->encrypt($stringToEncrypt); - $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($ciphertext)); - } - - public function testWrongPassword(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('HMAC does not match.'); - - $stringToEncrypt = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'; - $ciphertext = $this->crypto->encrypt($stringToEncrypt); - $this->crypto->decrypt($ciphertext, 'A wrong password!'); - } - - public function testLaterDecryption(): void { - $stringToEncrypt = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'; - $encryptedString = '44a35023cca2e7a6125e06c29fc4b2ad9d8a33d0873a8b45b0de4ef9284f260c6c46bf25dc62120644c59b8bafe4281ddc47a70c35ae6c29ef7a63d79eefacc297e60b13042ac582733598d0a6b4de37311556bb5c480fd2633de4e6ebafa868c2d1e2d80a5d24f9660360dba4d6e0c8|lhrFgK0zd9U160Wo|a75e57ab701f9124e1113543fd1dc596f21e20d456a0d1e813d5a8aaec9adcb11213788e96598b67fe9486a9f0b99642c18296d0175db44b1ae426e4e91080ee'; - $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd')); - } - - public function testWrongIV(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('HMAC does not match.'); - - $encryptedString = '560f5436ba864b9f12f7f7ca6d41c327554a6f2c0a160a03316b202af07c65163274993f3a46e7547c07ba89304f00594a2f3bd99f83859097c58049c39d0d4ade10e0de914ff0604961e7c849d0271ed6c0b23f984ba16e7d033e3305fb0910e7b6a2a65c988d17dbee71d8f953684d|d2kdFUspVjC0o0sr|1a5feacf87eaa6869a6abdfba9a296e7bbad45b6ad89f7dce67cdc98e2da5dc4379cc672cc655e52bbf19599bf59482fbea13a73937697fa656bf10f3fc4f1aa'; - $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd'); - } - - public function testWrongParameters(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Authenticated ciphertext could not be decoded.'); - - $encryptedString = '1|2'; - $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd'); - } - - public function testLegacy(): void { - $cipherText = 'e16599188e3d212f5c7f17fdc2abca46|M1WfLAxbcAmITeD6|509457885d6ca5e6c3bfd3741852687a7f2bffce197f8d5ae97b65818b15a1b7f616b68326ff312371540f4ca8ac55f8e2de4aa13aab3474bd3431e51214e3ee'; - $password = 'mypass'; - - $this->assertSame('legacy test', $this->crypto->decrypt($cipherText, $password)); - } - - public function testVersion2CiphertextDecryptsToCorrectPlaintext(): void { - $this->assertSame( - 'This is a plaintext value that will be encrypted with version 2. Which addresses the reduced permutations on the IV.', - $this->crypto->decrypt( - 'be006387f753e8728717e43cfc5526c37adf7b2c9b4a113ceec03b7b0bccfebee74e0acfa0015c5712b4376dacbd7bce26a8fbca916fdccee46203d8289f6b2e4c19318044d375edfc67c72e6c3ae329d4c276b8d866ac1b281844e81f7681fe83d90bc4b6fffa4f3cbc157d64257a493b67fd2af3c8976cb76df520f5739305|02e78ea7c73a32f3b407c54227a9d2ce|3e7a09628f818b7b1cd7724467f5b1b33135de6d2ec62d8c0361be4f2c5203385f10babdcae017d7b30abe5be2117803e3195fb6d9ef20949fe35dad5e9241ea|2', - 'insecure-static-password' - ) - ); - } - - /** - * Test data taken from https://github.com/owncloud/core/blob/9deb8196b20354c8de0cd720ad4d18d52ccc96d8/tests/lib/Security/CryptoTest.php#L56-L60 - */ - public function testOcVersion2CiphertextDecryptsToCorrectPlaintext(): void { - $this->assertSame( - 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.', - $this->crypto->decrypt( - 'v2|d57dbe4d1317cdf19d4ddc2df807f6b5d63ab1e119c46590ce54bae56a9cd3969168c4ec1600ac9758dd7e7afb9c4c962dd23072c1463add1d9c77c467723b37bb768ef00e3c50898e59247cbb59ce56b74ce5990648ffe9e40d0e95076c27a785bdcf32c219ea4ad5c316b1f12f48c1|6bd21db258a5e406a2c288a444de195f|a19111a4cf1a11ee95fc1734699c20964eaa05bb007e1cecc4cc6872f827a4b7deedc977c13b138d728d68116aa3d82f9673e20c7e447a9788aa3be994b67cd6', - 'ThisIsAVeryS3cur3P4ssw0rd' - ) - ); - } - - public function testDecryptWithWrongSecretThrowsHmacExceptionNotValueError(): void { - // Encrypt with 'secret-A' - $configA = $this->createMock(IConfig::class); - $configA->method('getSystemValue')->with('secret')->willReturn('secret-A'); - $configA->method('getSystemValueString')->with('secret')->willReturn('secret-A'); - $cryptoA = new Crypto($configA); - $ciphertext = $cryptoA->encrypt('plaintext'); - - // Decrypt with 'secret-B': first attempt fails (HMAC mismatch), fallback to empty - // string must not propagate a ValueError for v3 ciphertexts — it must rethrow the - // original HMAC exception instead. - $configB = $this->createMock(IConfig::class); - $configB->method('getSystemValue')->with('secret')->willReturn('secret-B'); - $configB->method('getSystemValueString')->with('secret')->willReturn('secret-B'); - $cryptoB = new Crypto($configB); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('HMAC does not match.'); - $cryptoB->decrypt($ciphertext); - } - - public function testVersion3CiphertextDecryptsToCorrectPlaintext(): void { - $this->assertSame( - 'Another plaintext value that will be encrypted with version 3. It addresses the related key issue. Old ciphertexts should be decrypted properly, but only use the better version for encryption.', - $this->crypto->decrypt( - 'c99823461db746aa74f819c8640e9e3c367fa3bb9c21dff905b5dd14072c1d1b0da8b7e6b7307bf1561b6ba7aaa932a16c23b1fd5217dc019d55233ef0813c65fccaeabd6ea3a971ce1bbbdfda790ae00fb4442693cbb50072e02875b9f50591df74d00e96fd5b9bd13cb02a5f57b062ec98a4c64fc518ed325d097454883adbfc1687c2af995a392407c5e040a54afee4b2997ab158fe48ef67ccf721a6a7031fcb44d51170892ce7971021a7f3a00d19002eb9b007efe7aecf397ec0dc22064fb5d4a15ad83949f0804feca3c69cdd|8476f53c8d49a7e119798a70086d8911|ae3f7e23d469fbc791714ceb07d854624b1bbd39ac6a4edc05d552e10659adfdcada3a059fae737ffd7d842dd3fcc84bcc364cd298e814dd4967de4ad4a658eb|3', - 'insecure-static-password' - ) - ); - } -} --- a/tests/lib/Security/Events/GenerateSecurePasswordEventTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertNull($event->getPassword()); - $this->assertEquals(PasswordContext::ACCOUNT, $event->getContext()); - } - - public function testSettingPassword(): void { - $event = new GenerateSecurePasswordEvent(); - $event->setPassword('example'); - $this->assertEquals('example', $event->getPassword()); - } - - public function testSettingContext(): void { - $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING); - $this->assertEquals(PasswordContext::SHARING, $event->getContext()); - } -} --- a/tests/lib/Security/Events/ValidatePasswordPolicyEventTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertEquals($password, $event->getPassword()); - $this->assertEquals(PasswordContext::ACCOUNT, $event->getContext()); - } - - public function testSettingContext(): void { - $event = new ValidatePasswordPolicyEvent('example', PasswordContext::SHARING); - $this->assertEquals(PasswordContext::SHARING, $event->getContext()); - } -} --- a/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php +++ /dev/null @@ -1,28 +0,0 @@ -createMock(FeaturePolicyManager::class); - $policy = $this->createMock(FeaturePolicy::class); - $event = new AddFeaturePolicyEvent($manager); - - $manager->expects($this->once()) - ->method('addDefaultPolicy') - ->with($policy); - - $event->addPolicy($policy); - } -} --- a/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php +++ /dev/null @@ -1,73 +0,0 @@ -dispatcher = Server::get(IEventDispatcher::class); - $this->manager = new FeaturePolicyManager($this->dispatcher); - } - - public function testAddDefaultPolicy(): void { - $this->manager->addDefaultPolicy(new FeaturePolicy()); - $this->addToAssertionCount(1); - } - - public function testGetDefaultPolicyWithPoliciesViaEvent(): void { - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { - $policy = new FeaturePolicy(); - $policy->addAllowedMicrophoneDomain('mydomain.com'); - $policy->addAllowedPaymentDomain('mypaymentdomain.com'); - - $e->addPolicy($policy); - }); - - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { - $policy = new FeaturePolicy(); - $policy->addAllowedPaymentDomain('mydomainother.com'); - $policy->addAllowedGeoLocationDomain('mylocation.here'); - - $e->addPolicy($policy); - }); - - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { - $policy = new FeaturePolicy(); - $policy->addAllowedAutoplayDomain('youtube.com'); - - $e->addPolicy($policy); - }); - - $expected = new \OC\Security\FeaturePolicy\FeaturePolicy(); - $expected->addAllowedMicrophoneDomain('mydomain.com'); - $expected->addAllowedPaymentDomain('mypaymentdomain.com'); - $expected->addAllowedPaymentDomain('mydomainother.com'); - $expected->addAllowedGeoLocationDomain('mylocation.here'); - $expected->addAllowedAutoplayDomain('youtube.com'); - - $expectedStringPolicy = "autoplay 'self' youtube.com;camera 'none';fullscreen 'self';geolocation mylocation.here;microphone mydomain.com;payment mypaymentdomain.com mydomainother.com"; - - $this->assertEquals($expected, $this->manager->getDefaultPolicy()); - $this->assertSame($expectedStringPolicy, $this->manager->getDefaultPolicy()->buildPolicy()); - } -} --- a/tests/lib/Security/HasherTest.php +++ /dev/null @@ -1,277 +0,0 @@ - 1, 'hash' => '2|3|4']], - ['1|我看|这本书。 我看這本書', ['version' => 1, 'hash' => '我看|这本书。 我看這本書']], - ['2|newhash', ['version' => 2, 'hash' => 'newhash']], - ]; - } - - public static function hashProviders70_71(): array { - return [ - // Valid SHA1 strings - ['password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true], - ['owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true], - - // Invalid SHA1 strings - ['InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false], - ['AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false], - - // Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" - ['password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true], - ['password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true], - ['password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true], - ['owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true], - ['owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true], - ['owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true], - - // Invalid legacy passwords - ['password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], - - // Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" - ['password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true], - ['password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true], - ['password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true], - ['owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true], - ['owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true], - ['owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true], - - // Invalid passwords - ['password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], - ['password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], - ['password', '2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], - ]; - } - - public static function hashProviders72(): array { - return [ - // Valid ARGON2 hashes - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$T3JGcEkxVFNOVktNSjZUcg$4/hyLtSejxNgAuzSFFV/HLM3qRQKBwEtKw61qPN4zWA', true], - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$Zk52V24yNjMzTkhyYjJKOQ$vmqHkCaOD6SiiiFKD1GeKLg/D1ynWpyZbx4XA2yed34', true], - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$R1pRcUZKamVlNndBc3l5ag$ToRhR8SiZc7fGMpOYfSc5haS5t9+Y00rljPJV7+qLkM', true], - ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$NC9xM0FFaDlzM01QM3kudg$fSfndwtO2mKMZlKdsT8XAtPY51cSS6pLSGS3xMqeJhg', true], - ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$UjkvUjEuL042WWl1cmdHOA$FZivLkBdZnloQsW6qq/jqWK95JSYUHW9rwQC4Ff9GN0', true], - ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$ZnpNdUlzMEpUTW40OVpiMQ$c+yHT9dtSYsjtVGsa7UKOsxxgQAMiUc781d9WsFACqs', true], - - //Invalid ARGON2 hashes - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$UjFDUDg3cjBvM3FkbXVOWQ$7Y5xqFxSERnYn+2+7WChUpWZWMa5BEIhSHWnDgJ71Jk', false], - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZUxSUi5aQklXdkcyMG1uVA$sYjoSvXg/CS/aS6Xnas/o9a/OPVcGKldzzmuiCD1Fxo', false], - ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZHQ5V0xMOFNmUC52by44Sg$DzQFk3bJTX0J4PVGwW6rMvtnBJRalBkbtpDIXR+d4A0', false], - ]; - } - - public static function hashProviders73(): array { - return [ - // Valid ARGON2ID hashes - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$TEtIMnhUczliQzI0Y01WeA$BpMUDrApy25iagIogUAnlc0rNTPJmGs8lOEeVHujJ9Q', true], - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$RzdUdDNvbHhZalVQa2VIcQ$Wo8CGasVCBcSe69ldPdoVKTWEDQkET2cgQJSUiKcIzs', true], - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$djlDMTVkL3VnMlNZNWZPeg$PCMpdAjB+OtwGpM75IGWmYHh1h2I7l5P8YabYtKubWg', true], - ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$VGhGL05rcUI3d3k3WVhibQ$CSy0ShUnamZQhu8oeZfUTTd/S3z966zuQ/uz1Y80Rss', true], - ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$ZVlZTVlCaTZhRlZHOGFpYQ$xd1TtMz1Mi0SuZrP+VWB3v/hwoC7HfSVsUYmzOo2DUU', true], - ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$OG1wZUtzZ0tnLjF2MUZVMA$CBluq8W8ISmZ9QumeWsVhaVREP0Zcq8rwk2NrA9d4YE', true], - - //Invalid ARGON2ID hashes - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$V3ovTHlvc0Eyb24xenVRNQ$iY/A0Yf24c2DToedj2rj9+KeoJBGsJYQOlJMoa0SFXk', false], - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$NlYuMlQ0ODIudTRkZDhYUw$/Z71ckOIuydujedUGK73iXC9vbLzlH/iXkG9+gGgn+c', false], - ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$b09kNFZTZWFjS05aTkl6ZA$llE4TnIYYrC0H7wkTL1JsIwAAgoMJERlqtFcHHQcXTs', false], - ]; - } - - /** @var Hasher */ - protected $hasher; - - /** @var IConfig */ - protected $config; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->config = $this->createMock(IConfig::class); - - $this->config->method('getSystemValueInt') - ->willReturnCallback(function ($name, $default) { - return $default; - }); - - $this->hasher = new Hasher($this->config); - } - - public function testHash(): void { - $hash = $this->hasher->hash('String To Hash'); - $this->assertNotNull($hash); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('versionHashProvider')] - public function testSplitHash($hash, $expected): void { - $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]); - $this->assertSame($expected, $relativePath); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders70_71')] - public function testVerify($password, $hash, $expected): void { - $this->config - ->expects($this->any()) - ->method('getSystemValue') - ->willReturnCallback(function ($key, $default) { - if ($key === 'passwordsalt') { - return '6Wow67q1wZQZpUUeI6G2LsWUu4XKx'; - } - return $default; - }); - - $result = $this->hasher->verify($password, $hash); - $this->assertSame($expected, $result); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders72')] - public function testVerifyArgon2i($password, $hash, $expected): void { - if (!\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); - } - - $result = $this->hasher->verify($password, $hash); - $this->assertSame($expected, $result); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders73')] - public function testVerifyArgon2id(string $password, string $hash, bool $expected): void { - if (!\defined('PASSWORD_ARGON2ID')) { - $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes'); - } - - $result = $this->hasher->verify($password, $hash); - $this->assertSame($expected, $result); - } - - public function testUpgradeHashBlowFishToArgon2(): void { - if (!\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); - } - - $message = 'mysecret'; - - $blowfish = 1 . '|' . password_hash($message, PASSWORD_BCRYPT, []); - $argon2 = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []); - - $newAlg = PASSWORD_ARGON2I; - if (\defined('PASSWORD_ARGON2ID')) { - $newAlg = PASSWORD_ARGON2ID; - $argon2 = 2 . '|' . password_hash($message, PASSWORD_ARGON2ID, []); - } - - $this->assertTrue($this->hasher->verify($message, $blowfish, $newHash)); - $this->assertTrue($this->hasher->verify($message, $argon2)); - - $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]); - - $this->assertFalse(password_needs_rehash($relativePath['hash'], $newAlg, [])); - } - - public function testUsePasswordDefaultArgon2iVerify(): void { - if (!\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); - } - - $this->config->method('getSystemValueBool') - ->with('hashing_default_password') - ->willReturn(true); - - $message = 'mysecret'; - - $argon2i = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []); - - $newHash = null; - $this->assertTrue($this->hasher->verify($message, $argon2i, $newHash)); - $this->assertNotNull($newHash); - - $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]); - $this->assertEquals(1, $relativePath['version']); - $this->assertEquals(PASSWORD_BCRYPT, password_get_info($relativePath['hash'])['algo']); - $this->assertFalse(password_needs_rehash($relativePath['hash'], PASSWORD_BCRYPT)); - $this->assertTrue(password_verify($message, $relativePath['hash'])); - } - - public function testDoNotUsePasswordDefaultArgon2idVerify(): void { - if (!\defined('PASSWORD_ARGON2ID')) { - $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes'); - } - - $this->config->method('getSystemValueBool') - ->with('hashing_default_password') - ->willReturn(false); - - $message = 'mysecret'; - - $argon2id = 3 . '|' . password_hash($message, PASSWORD_ARGON2ID, []); - - $newHash = null; - $this->assertTrue($this->hasher->verify($message, $argon2id, $newHash)); - $this->assertNull($newHash); - } - - public function testHashUsePasswordDefault(): void { - if (!\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); - } - - $this->config->method('getSystemValueBool') - ->with('hashing_default_password') - ->willReturn(true); - - $message = 'mysecret'; - - $hash = $this->hasher->hash($message); - $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]); - - $this->assertSame(1, $relativePath['version']); - - $info = password_get_info($relativePath['hash']); - $this->assertEquals(PASSWORD_BCRYPT, $info['algo']); - } - - public function testValidHash(): void { - $hash = '3|$argon2id$v=19$m=65536,t=4,p=1$czFCSjk3LklVdXppZ2VCWA$li0NgdXe2/jwSRxgteGQPWlzJU0E0xdtfHbCbrpych0'; - - $isValid = $this->hasher->validate($hash); - - $this->assertTrue($isValid); - } - - public function testValidGeneratedHash(): void { - $message = 'secret'; - $hash = $this->hasher->hash($message); - - $isValid = $this->hasher->validate($hash); - - $this->assertTrue($isValid); - } - - public function testInvalidHash(): void { - $invalidHash = 'someInvalidHash'; - - $isValid = $this->hasher->validate($invalidHash); - - $this->assertFalse($isValid); - } -} --- a/tests/lib/Security/IdentityProof/KeyTest.php +++ /dev/null @@ -1,33 +0,0 @@ -key = new Key('public', 'private'); - } - - public function testGetPrivate(): void { - $this->assertSame('private', $this->key->getPrivate()); - } - - public function testGetPublic(): void { - $this->assertSame('public', $this->key->getPublic()); - } -} --- a/tests/lib/Security/IdentityProof/ManagerTest.php +++ /dev/null @@ -1,274 +0,0 @@ -factory = $this->createMock(Factory::class); - $this->appData = $this->createMock(AppData::class); - $this->config = $this->createMock(IConfig::class); - $this->factory->expects($this->any()) - ->method('get') - ->with('identityproof') - ->willReturn($this->appData); - $this->logger = $this->createMock(LoggerInterface::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->cache = $this->createMock(ICache::class); - - $this->cacheFactory->expects($this->any()) - ->method('createDistributed') - ->willReturn($this->cache); - - $this->crypto = $this->createMock(ICrypto::class); - $this->manager = $this->getManager(['generateKeyPair']); - } - - /** - * create manager object - * - * @param array $setMethods - * @return Manager|\PHPUnit\Framework\MockObject\MockObject - */ - protected function getManager($setMethods = []) { - if (empty($setMethods)) { - return new Manager( - $this->factory, - $this->crypto, - $this->config, - $this->logger, - $this->cacheFactory, - ); - } else { - return $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->factory, - $this->crypto, - $this->config, - $this->logger, - $this->cacheFactory, - ]) - ->onlyMethods($setMethods) - ->getMock(); - } - } - - public function testGetKeyWithExistingKey(): void { - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $folder = $this->createMock(ISimpleFolder::class); - $privateFile = $this->createMock(ISimpleFile::class); - $privateFile - ->expects($this->once()) - ->method('getContent') - ->willReturn('EncryptedPrivateKey'); - $publicFile = $this->createMock(ISimpleFile::class); - $publicFile - ->expects($this->once()) - ->method('getContent') - ->willReturn('MyPublicKey'); - $this->crypto - ->expects($this->once()) - ->method('decrypt') - ->with('EncryptedPrivateKey') - ->willReturn('MyPrivateKey'); - $folder - ->expects($this->exactly(2)) - ->method('getFile') - ->willReturnMap([ - ['private', $privateFile], - ['public', $publicFile], - ]); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('user-MyUid') - ->willReturn($folder); - $this->cache - ->expects($this->exactly(2)) - ->method('get') - ->willReturn(null); - - $expected = new Key('MyPublicKey', 'MyPrivateKey'); - $this->assertEquals($expected, $this->manager->getKey($user)); - } - - public function testGetKeyWithExistingKeyCached(): void { - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->crypto - ->expects($this->once()) - ->method('decrypt') - ->with('EncryptedPrivateKey') - ->willReturn('MyPrivateKey'); - $this->cache - ->expects($this->exactly(2)) - ->method('get') - ->willReturnMap([ - ['user-MyUid-public', 'MyPublicKey'], - ['user-MyUid-private', 'EncryptedPrivateKey'], - ]); - - $expected = new Key('MyPublicKey', 'MyPrivateKey'); - $this->assertEquals($expected, $this->manager->getKey($user)); - } - - public function testSetPublicKey(): void { - $user = $this->createMock(IUser::class); - $user - ->expects($this->exactly(1)) - ->method('getUID') - ->willReturn('MyUid'); - $publicFile = $this->createMock(ISimpleFile::class); - $folder = $this->createMock(ISimpleFolder::class); - $folder - ->expects($this->once()) - ->method('newFile') - ->willReturnMap([ - ['public', 'MyNewPublicKey', $publicFile], - ]); - $this->appData - ->expects($this->once()) - ->method('getFolder') - ->with('user-MyUid') - ->willReturn($folder); - $this->cache - ->expects($this->once()) - ->method('set') - ->with('user-MyUid-public', 'MyNewPublicKey'); - - $this->manager->setPublicKey($user, 'MyNewPublicKey'); - } - - public function testGetKeyWithNotExistingKey(): void { - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->manager - ->expects($this->once()) - ->method('generateKeyPair') - ->willReturn(['MyNewPublicKey', 'MyNewPrivateKey']); - $this->appData - ->expects($this->once()) - ->method('newFolder') - ->with('user-MyUid'); - $folder = $this->createMock(ISimpleFolder::class); - $this->crypto - ->expects($this->once()) - ->method('encrypt') - ->with('MyNewPrivateKey') - ->willReturn('MyNewEncryptedPrivateKey'); - $privateFile = $this->createMock(ISimpleFile::class); - $privateFile - ->expects($this->once()) - ->method('putContent') - ->with('MyNewEncryptedPrivateKey'); - $publicFile = $this->createMock(ISimpleFile::class); - $publicFile - ->expects($this->once()) - ->method('putContent') - ->with('MyNewPublicKey'); - $folder - ->expects($this->exactly(2)) - ->method('newFile') - ->willReturnMap([ - ['private', null, $privateFile], - ['public', null, $publicFile], - ]); - $this->appData - ->expects($this->exactly(2)) - ->method('getFolder') - ->with('user-MyUid') - ->willReturnOnConsecutiveCalls( - $this->throwException(new \Exception()), - $folder - ); - - $expected = new Key('MyNewPublicKey', 'MyNewPrivateKey'); - $this->assertEquals($expected, $this->manager->getKey($user)); - } - - public function testGenerateKeyPair(): void { - $manager = $this->getManager(); - $data = 'MyTestData'; - - [$resultPublicKey, $resultPrivateKey] = self::invokePrivate($manager, 'generateKeyPair'); - openssl_sign($data, $signature, $resultPrivateKey); - $details = openssl_pkey_get_details(openssl_pkey_get_public($resultPublicKey)); - - $this->assertSame(1, openssl_verify($data, $signature, $resultPublicKey)); - $this->assertSame(2048, $details['bits']); - } - - public function testGetSystemKey(): void { - $manager = $this->getManager(['retrieveKey']); - - /** @var Key|\PHPUnit\Framework\MockObject\MockObject $key */ - $key = $this->createMock(Key::class); - - $this->config->expects($this->once())->method('getSystemValue') - ->with('instanceid', null)->willReturn('instanceId'); - - $manager->expects($this->once())->method('retrieveKey')->with('system-instanceId') - ->willReturn($key); - - $this->assertSame($key, $manager->getSystemKey()); - } - - public function testGetSystemKeyFailure(): void { - $this->expectException(\RuntimeException::class); - - $manager = $this->getManager(['retrieveKey']); - - /** @var Key|\PHPUnit\Framework\MockObject\MockObject $key */ - $key = $this->createMock(Key::class); - - $this->config->expects($this->once())->method('getSystemValue') - ->with('instanceid', null)->willReturn(null); - - $manager->getSystemKey(); - } -} --- a/tests/lib/Security/IdentityProof/SignerTest.php +++ /dev/null @@ -1,189 +0,0 @@ -key = new Key($this->public, $this->private); - - $this->keyManager = $this->createMock(Manager::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->userManager = $this->createMock(IUserManager::class); - - $this->signer = new Signer( - $this->keyManager, - $this->timeFactory, - $this->userManager - ); - } - - public function testSign(): void { - $user = $this->createMock(IUser::class); - $user->method('getCloudId') - ->willReturn('foo@example.com'); - - $this->timeFactory->method('getTime') - ->willReturn(42); - - $this->keyManager->method('getKey') - ->with($this->equalTo($user)) - ->willReturn($this->key); - - $data = [ - 'foo' => 'bar', - 'abc' => 'def', - 'xyz' => 123, - ]; - - $expects = [ - 'message' => [ - 'data' => $data, - 'type' => 'myType', - 'signer' => 'foo@example.com', - 'timestamp' => 42, - ], - 'signature' => 'E1fNdoWMX1QmSyKv+S3FtOgLe9niYGQFWOKGaMLxc2h7s3V++EIqJvw/NCLBfrUowzWkTzhkjfbHaf88Hz34WAn4sAwXYAO8cnboQs6SClKRzQ/nvbtLgd2wm9RQ8UTOM7wR6C7HpIn4qqJ4BTQ1bAwYAiJ2GoK+W8wC0o0Gpub2906j3JJ4cbc9lufd5ohWKCev8Ubem/EEKaRIZA7qHh+Q1MKXTaJQJlCjTMe5PyGy0fsmtVxsPls3/Fkd9sVeHEHSYHzOiF6ttlxWou4TdRbq3WSEVpt1DOOvkKI9w2+zBJ7IPH8CnVpXcdIzWDctUygZKzNMUQnweDOOziEdUw==' - ]; - - $result = $this->signer->sign('myType', $data, $user); - - $this->assertEquals($expects, $result); - } - - public function testVerifyValid(): void { - $data = [ - 'message' => [ - 'data' => [ - 'foo' => 'bar', - 'abc' => 'def', - 'xyz' => 123, - ], - 'type' => 'myType', - 'signer' => 'foo@example.com', - 'timestamp' => 42, - ], - 'signature' => 'E1fNdoWMX1QmSyKv+S3FtOgLe9niYGQFWOKGaMLxc2h7s3V++EIqJvw/NCLBfrUowzWkTzhkjfbHaf88Hz34WAn4sAwXYAO8cnboQs6SClKRzQ/nvbtLgd2wm9RQ8UTOM7wR6C7HpIn4qqJ4BTQ1bAwYAiJ2GoK+W8wC0o0Gpub2906j3JJ4cbc9lufd5ohWKCev8Ubem/EEKaRIZA7qHh+Q1MKXTaJQJlCjTMe5PyGy0fsmtVxsPls3/Fkd9sVeHEHSYHzOiF6ttlxWou4TdRbq3WSEVpt1DOOvkKI9w2+zBJ7IPH8CnVpXcdIzWDctUygZKzNMUQnweDOOziEdUw==' - ]; - - $user = $this->createMock(IUser::class); - - $this->keyManager->method('getKey') - ->with($this->equalTo($user)) - ->willReturn($this->key); - - $this->userManager->method('get') - ->with('foo') - ->willReturn($user); - - $this->assertTrue($this->signer->verify($data)); - } - - public function testVerifyInvalid(): void { - $data = [ - 'message' => [ - 'data' => [ - 'foo' => 'bar', - 'abc' => 'def', - 'xyz' => 123, - ], - 'type' => 'myType', - 'signer' => 'foo@example.com', - 'timestamp' => 42, - ], - 'signature' => 'Invalid sig' - ]; - - $user = $this->createMock(IUser::class); - - $this->keyManager->method('getKey') - ->with($this->equalTo($user)) - ->willReturn($this->key); - - $this->userManager->method('get') - ->with('foo') - ->willReturn($user); - - $this->assertFalse($this->signer->verify($data)); - } - - public function testVerifyInvalidData(): void { - $data = [ - ]; - - $this->assertFalse($this->signer->verify($data)); - } -} --- a/tests/lib/Security/Ip/BruteforceAllowListTest.php +++ /dev/null @@ -1,162 +0,0 @@ -appConfig = $this->createMock(IAppConfig::class); - $this->factory = new Factory(); - - $this->allowList = new BruteforceAllowList( - $this->appConfig, - $this->factory, - ); - } - - public static function dataIsBypassListed(): array { - return [ - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.0/24', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '192.168.0.0/16', - ], - false, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.11/31', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.9/31', - ], - false, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.15/29', - ], - true, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:beef:cafe:1234::/64' - ], - false, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:beef::/64' - ], - false, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:cafe::/8' - ], - true, - ], - [ - 'dead:beef:cafe::1111', - [ - 'whitelist_0' => 'dead:beef:cafe::1100/123', - ], - true, - ], - [ - 'invalid', - [], - false, - ], - ]; - } - - /** - * @param string[] $allowList - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataIsBypassListed')] - public function testIsBypassListed( - string $ip, - array $allowList, - bool $isAllowListed, - ): void { - $this->appConfig->method('searchKeys') - ->with($this->equalTo('bruteForce'), $this->equalTo('whitelist_')) - ->willReturn(array_keys($allowList)); - - $this->appConfig->method('getValueString') - ->willReturnCallback(function ($app, $key, $default) use ($allowList) { - if ($app !== 'bruteForce') { - return $default; - } - if (isset($allowList[$key])) { - return $allowList[$key]; - } - return $default; - }); - - $this->assertSame( - $isAllowListed, - $this->allowList->isBypassListed($ip) - ); - } -} --- a/tests/lib/Security/Ip/RemoteAddressTest.php +++ /dev/null @@ -1,80 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->request = $this->createMock(IRequest::class); - } - - /** - * @param mixed $allowedRanges - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] - public function testAllowedIps(string $remoteIp, $allowedRanges, bool $expected): void { - $this->request - ->method('getRemoteAddress') - ->willReturn($remoteIp); - $this->config - ->method('getSystemValue') - ->with('allowed_admin_ranges', false) - ->willReturn($allowedRanges); - - $remoteAddress = new RemoteAddress($this->config, $this->request); - - $this->assertEquals($expected, $remoteAddress->allowsAdminActions()); - } - - /** - * @return array - */ - public static function dataProvider(): array { - return [ - // No IP (ie. CLI) - ['', ['192.168.1.2/24'], true], - ['', ['fe80/8'], true], - // No configuration - ['1.2.3.4', false, true], - ['1234:4567:8910::', false, true], - // v6 Zone ID - ['fe80::1fc4:15d8:78db:2319%enp4s0', false, true], - // Empty configuration - ['1.2.3.4', [], true], - ['1234:4567:8910::', [], true], - // Invalid configuration - ['1.2.3.4', 'hello', true], - ['1234:4567:8910::', 'world', true], - // Mixed configuration - ['192.168.1.5', ['1.2.3.*', '1234::/8'], false], - ['::1', ['127.0.0.1', '1234::/8'], false], - ['192.168.1.5', ['192.168.1.0/24', '1234::/8'], true], - // Allowed IP - ['1.2.3.4', ['1.2.3.*'], true], - ['fc00:1:2:3::1', ['fc00::/7'], true], - ['1.2.3.4', ['192.168.1.2/24', '1.2.3.0/24'], true], - ['1234:4567:8910::1', ['fe80::/8','1234:4567::/16'], true], - // Blocked IP - ['192.168.1.5', ['1.2.3.*'], false], - ['9234:4567:8910::', ['1234:4567::1'], false], - ['192.168.2.1', ['192.168.1.2/24', '1.2.3.0/24'], false], - ['9234:4567:8910::', ['fe80::/8','1234:4567::/16'], false], - ]; - } -} --- a/tests/lib/Security/Normalizer/IpAddressTest.php +++ /dev/null @@ -1,70 +0,0 @@ -assertSame($expected, (new IpAddress($input))->getSubnet()); - } - - public function testToString(): void { - $this->assertSame('127.0.0.1', (string)(new IpAddress('127.0.0.1'))); - } -} --- a/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php +++ /dev/null @@ -1,144 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->cache = $this->createMock(ICache::class); - - $this->cacheFactory - ->expects($this->once()) - ->method('createDistributed') - ->with('OC\Security\RateLimiting\Backend\MemoryCacheBackend') - ->willReturn($this->cache); - - $this->config->method('getSystemValueBool') - ->with('ratelimit.protection.enabled') - ->willReturn(true); - - $this->memoryCache = new MemoryCacheBackend( - $this->config, - $this->cacheFactory, - $this->timeFactory - ); - } - - public function testGetAttemptsWithNoAttemptsBefore(): void { - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(null); - - $this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User')); - } - - public function testGetAttempts(): void { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(210); - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(json_encode([ - '1', - '2', - '87', - '223', - '223', - '224', - ])); - - $this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User')); - } - - public function testRegisterAttemptWithNoAttemptsBefore(): void { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(123); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(null); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', - json_encode(['223']) - ); - - $this->memoryCache->registerAttempt('Method', 'User', 100); - } - - public function testRegisterAttempt(): void { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(86); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(json_encode([ - '1', - '2', - '87', - '123', - '123', - '124', - ])); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', - json_encode([ - '87', - '123', - '123', - '124', - '186', - ]) - ); - - $this->memoryCache->registerAttempt('Method', 'User', 100); - } -} --- a/tests/lib/Security/RateLimiting/LimiterTest.php +++ /dev/null @@ -1,134 +0,0 @@ -backend = $this->createMock(IBackend::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->limiter = new Limiter( - $this->backend, - $this->logger, - ); - } - - public function testRegisterAnonRequestExceeded(): void { - $this->expectException(RateLimitExceededException::class); - $this->expectExceptionMessage('Rate limit exceeded'); - - $this->backend - ->expects($this->once()) - ->method('getAttempts') - ->with( - 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' - ) - ->willReturn(101); - $this->logger->expects($this->once()) - ->method('info'); - - $this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); - } - - public function testRegisterAnonRequestSuccess(): void { - $this->backend - ->expects($this->once()) - ->method('getAttempts') - ->with( - 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' - ) - ->willReturn(99); - $this->backend - ->expects($this->once()) - ->method('registerAttempt') - ->with( - 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 100 - ); - $this->logger->expects($this->never()) - ->method('info'); - - $this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); - } - - public function testRegisterUserRequestExceeded(): void { - $this->expectException(RateLimitExceededException::class); - $this->expectExceptionMessage('Rate limit exceeded'); - - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - $this->backend - ->expects($this->once()) - ->method('getAttempts') - ->with( - 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' - ) - ->willReturn(101); - $this->logger->expects($this->once()) - ->method('info'); - - $this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); - } - - public function testRegisterUserRequestSuccess(): void { - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->createMock(IUser::class); - $user - ->expects($this->once()) - ->method('getUID') - ->willReturn('MyUid'); - - $this->backend - ->expects($this->once()) - ->method('getAttempts') - ->with( - 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' - ) - ->willReturn(99); - $this->backend - ->expects($this->once()) - ->method('registerAttempt') - ->with( - 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 100 - ); - $this->logger->expects($this->never()) - ->method('info'); - - $this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); - } -} --- a/tests/lib/Security/RemoteHostValidatorIntegrationTest.php +++ /dev/null @@ -1,123 +0,0 @@ -config = $this->createMock(IConfig::class); - - $this->validator = new RemoteHostValidator( - $this->config, - Server::get(HostnameClassifier::class), - Server::get(IpAddressClassifier::class), - new NullLogger(), - ); - } - - public static function localHostsData(): array { - return [ - ['[::1]'], - ['[::]'], - ['192.168.0.1'], - ['127.0.0.1'], - ['127.0.0.13'], // all 127.0.0.0/8 network is loopback address - ['172.16.42.1'], - ['[fdf8:f53b:82e4::53]'], - ['[fe80::200:5aee:feaa:20a2]'], - ['[0:0:0:0:0:ffff:10.0.0.1]'], - ['[0:0:0:0:0:ffff:127.0.0.0]'], - ['10.0.0.1'], - ['!@#$'], // test invalid url - ['100.100.100.200'], - ['192.0.0.1'], - ['0177.0.0.9'], - ['⑯⑨。②⑤④。⑯⑨。②⑤④'], - ['127。②⑤④。⑯⑨.②⑤④'], - ['127.0.00000000000000000000000000000000001'], - ['127.1'], - ['127.000.001'], - ['0177.0.0.01'], - ['0x7f.0x0.0x0.0x1'], - ['0x7f000001'], - ['2130706433'], - ['00000000000000000000000000000000000000000000000000177.1'], - ['0x7f.1'], - ['127.0x1'], - ['[0000:0000:0000:0000:0000:0000:0000:0001]'], - ['[0:0:0:0:0:0:0:1]'], - ['[0:0:0:0::0:0:1]'], - ['%31%32%37%2E%30%2E%30%2E%31'], - ['%31%32%37%2E%30%2E%30.%31'], - ['[%3A%3A%31]'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localHostsData')] - public function testLocalHostsWhenNotAllowed(string $host): void { - $this->config - ->method('getSystemValueBool') - ->with('allow_local_remote_servers', false) - ->willReturn(false); - - $isValid = $this->validator->isValid($host); - - self::assertFalse($isValid); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localHostsData')] - public function testLocalHostsWhenAllowed(string $host): void { - $this->config - ->method('getSystemValueBool') - ->with('allow_local_remote_servers', false) - ->willReturn(true); - - $isValid = $this->validator->isValid($host); - - self::assertTrue($isValid); - } - - public static function externalAddressesData():array { - return [ - ['8.8.8.8'], - ['8.8.4.4'], - ['8.8.8.8'], - ['8.8.4.4'], - ['[2001:4860:4860::8888]'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('externalAddressesData')] - public function testExternalHost(string $host): void { - $this->config - ->method('getSystemValueBool') - ->with('allow_local_remote_servers', false) - ->willReturn(false); - - $isValid = $this->validator->isValid($host); - - self::assertTrue($isValid); - } -} --- a/tests/lib/Security/RemoteHostValidatorTest.php +++ /dev/null @@ -1,101 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->hostnameClassifier = $this->createMock(HostnameClassifier::class); - $this->ipAddressClassifier = $this->createMock(IpAddressClassifier::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->validator = new RemoteHostValidator( - $this->config, - $this->hostnameClassifier, - $this->ipAddressClassifier, - $this->logger, - ); - } - - public static function dataValid(): array { - return [ - ['nextcloud.com', true], - ['com.one-.nextcloud-one.com', false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataValid')] - public function testValid(string $host, bool $expected): void { - $this->hostnameClassifier - ->method('isLocalHostname') - ->with($host) - ->willReturn(false); - $this->ipAddressClassifier - ->method('isLocalAddress') - ->with($host) - ->willReturn(false); - - $valid = $this->validator->isValid($host); - - self::assertSame($expected, $valid); - } - - public function testLocalHostname(): void { - $host = 'localhost'; - $this->hostnameClassifier - ->method('isLocalHostname') - ->with($host) - ->willReturn(true); - $this->ipAddressClassifier - ->method('isLocalAddress') - ->with($host) - ->willReturn(false); - - $valid = $this->validator->isValid($host); - - self::assertFalse($valid); - } - - public function testLocalAddress(): void { - $host = '10.0.0.10'; - $this->hostnameClassifier - ->method('isLocalHostname') - ->with($host) - ->willReturn(false); - $this->ipAddressClassifier - ->method('isLocalAddress') - ->with($host) - ->willReturn(true); - - $valid = $this->validator->isValid($host); - - self::assertFalse($valid); - } -} --- a/tests/lib/Security/SecureRandomTest.php +++ /dev/null @@ -1,85 +0,0 @@ -rng = new SecureRandom(); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] - public function testGetLowStrengthGeneratorLength($length, $expectedLength): void { - $generator = $this->rng; - - $this->assertEquals($expectedLength, strlen($generator->generate($length))); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] - public function testMediumLowStrengthGeneratorLength($length, $expectedLength): void { - $generator = $this->rng; - - $this->assertEquals($expectedLength, strlen($generator->generate($length))); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] - public function testUninitializedGenerate($length, $expectedLength): void { - $this->assertEquals($expectedLength, strlen($this->rng->generate($length))); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('charCombinations')] - public function testScheme($charName, $chars): void { - $generator = $this->rng; - $scheme = constant('OCP\Security\ISecureRandom::' . $charName); - $randomString = $generator->generate(100, $scheme); - $matchesRegex = preg_match('/^' . $chars . '+$/', $randomString); - $this->assertSame(1, $matchesRegex); - } - - public static function invalidLengths(): array { - return [ - [0], - [-1], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('invalidLengths')] - public function testInvalidLengths($length): void { - $this->expectException(\LengthException::class); - $generator = $this->rng; - $generator->generate($length); - } -} --- a/tests/lib/Security/Signature/Model/Rfc9421RoundTripTest.php +++ /dev/null @@ -1,554 +0,0 @@ -ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = '{"hello":"world"}'; - $method = 'POST'; - $uri = 'https://receiver.example.org/ocm/shares'; - - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', $method, $uri); - $out->sign(); - - $req = $this->mockRequestFromOutgoing($out, $method, '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest($body, $req); - $in->setKey($jwk); - - $this->assertSame($out->getSignatureBaseString(), $in->getSignatureBaseString()); - // the Date header is deliberately not covered by the signature - $this->assertSame( - ['@method', '@target-uri', 'content-digest', 'content-length'], - $in->getCoveredComponents(), - ); - $in->verify(); // throws on failure - $this->addToAssertionCount(1); - } - - public function testEd25519VerifyAcceptedWhenSodiumLoaded(): void { - $this->skipUnlessSodium(); - [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManagerWithSigningAlgorithm($signatory, 'ed25519'); - - $body = '{"hello":"world"}'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - // Ed25519 sign() throws via Algorithm::sign; produce the signature directly - // over a manually reconstructed signature base. - $headers = $out->getHeaders(); - $paramsLine = '("@method" "@target-uri" "content-digest" "content-length");created=' . time() . ';keyid="' . $signatory->getKeyId() . '";tag="ocm"'; - $base = implode("\n", [ - '"@method": POST', - '"@target-uri": https://receiver.example.org/ocm/shares', - '"content-digest": ' . $headers['Content-Digest'], - '"content-length": ' . $headers['Content-Length'], - '"@signature-params": ' . $paramsLine, - ]); - $rawSig = sodium_crypto_sign_detached($base, $signatory->getPrivateKey()); - $headers['Signature-Input'] = 'ocm=' . $paramsLine; - $headers['Signature'] = 'ocm=:' . base64_encode($rawSig) . ':'; - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest($body, $req); - $in->setKey($jwk); - $in->verify(); - $this->addToAssertionCount(1); - } - - public function testTamperedBodyRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'original'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - $req = $this->mockRequestFromOutgoing($out, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest('tampered', $req); - } - - public function testTamperedSignatureRejected(): void { - [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - $headers = $out->getHeaders(); - // Replace the inner base64 of the signature with a different valid base64. - $headers['Signature'] = preg_replace('/=:[^:]+:/', '=:' . base64_encode(random_bytes(64)) . ':', (string)$headers['Signature']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest($body, $req); - $in->setKey($jwk); - - $this->expectException(InvalidSignatureException::class); - $in->verify(); - } - - public function testOutgoingCarriesOcmTag(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - $headers = $out->getHeaders(); - // the label is cosmetic; the integrity-protected tag parameter is - // what marks the signature as the OCM one - $this->assertStringStartsWith('ocm=(', (string)$headers['Signature-Input']); - $this->assertStringContainsString(';tag="ocm"', (string)$headers['Signature-Input']); - $this->assertStringStartsWith('ocm=:', (string)$headers['Signature']); - } - - public function testArbitraryLabelWithOcmTagVerifies(): void { - [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Rename the dictionary label; the verifier MUST select by the - // tag="ocm" parameter and disregard labels. - $headers = $out->getHeaders(); - $headers['Signature-Input'] = preg_replace('/^ocm=/', 'sig1=', (string)$headers['Signature-Input']); - $headers['Signature'] = preg_replace('/^ocm=/', 'sig1=', (string)$headers['Signature']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest('msg', $req); - $in->setKey($jwk); - $in->verify(); - $this->addToAssertionCount(1); - } - - public function testRequestWithoutOcmTagTreatedAsUnsigned(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Strip the tag parameter; without tag="ocm" the request carries no - // OCM signature and is handled as unsigned. - $headers = $out->getHeaders(); - $headers['Signature-Input'] = str_replace(';tag="ocm"', '', (string)$headers['Signature-Input']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(SignatureNotFoundException::class); - new Rfc9421IncomingSignedRequest('msg', $req); - } - - public function testTwoSignaturesCarryingOcmTagRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // A second, differently-labeled signature also carrying tag="ocm": - // the entire message MUST be rejected. - $headers = $out->getHeaders(); - $headers['Signature-Input'] = (string)$headers['Signature-Input'] . ', ' . preg_replace('/^ocm=/', 'sig2=', (string)$headers['Signature-Input']); - $headers['Signature'] = (string)$headers['Signature'] . ', ' . preg_replace('/^ocm=/', 'sig2=', (string)$headers['Signature']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest('msg', $req); - } - - public function testDuplicateOcmLabelRejected(): void { - // RFC 8941 §4.2 last-wins on duplicate dictionary keys, which would - // silently hide one of two identically-labeled OCM signatures; that - // ambiguity on the selected entry causes outright rejection. - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - $headers = $out->getHeaders(); - $headers['Signature-Input'] = (string)$headers['Signature-Input'] . ', ' . (string)$headers['Signature-Input']; - $headers['Signature'] = (string)$headers['Signature'] . ', ' . (string)$headers['Signature']; - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest('msg', $req); - } - - public function testForeignSiblingLabelIgnored(): void { - [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Splice in a sibling proxy_sig1 entry; the verifier must ignore it - // and still verify the ocm-labeled signature successfully. - $headers = $out->getHeaders(); - $proxyParams = '("@method");created=1;keyid="proxy"'; - $proxySig = base64_encode(random_bytes(64)); - $headers['Signature-Input'] = (string)$headers['Signature-Input'] . ', proxy_sig1=' . $proxyParams; - $headers['Signature'] = (string)$headers['Signature'] . ', proxy_sig1=:' . $proxySig . ':'; - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest('msg', $req); - $in->setKey($jwk); - $in->verify(); - $this->addToAssertionCount(1); - } - - public function testTooOldSignatureRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Backdate `created` in Signature-Input by 10 minutes. - $headers = $out->getHeaders(); - $pastCreated = time() - 600; - $headers['Signature-Input'] = preg_replace('/created=\d+/', 'created=' . $pastCreated, (string)$headers['Signature-Input']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest($body, $req, ['ttl' => 300]); - } - - public function testFutureCreatedRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Push `created` 10 minutes into the future, well past the - // 60-second skew tolerance. - $headers = $out->getHeaders(); - $futureCreated = time() + 600; - $headers['Signature-Input'] = preg_replace('/created=\d+/', 'created=' . $futureCreated, (string)$headers['Signature-Input']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest($body, $req); - } - - public function testMissingCreatedRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Strip the `;created=...` parameter so the signature loses its - // freshness anchor. - $headers = $out->getHeaders(); - $headers['Signature-Input'] = preg_replace('/;created=\d+/', '', (string)$headers['Signature-Input']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest($body, $req); - } - - public function testMissingKeyidRejected(): void { - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManager($signatory); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - // Strip the `;keyid="..."` parameter; verifiers MUST reject - // signatures without it. - $headers = $out->getHeaders(); - $headers['Signature-Input'] = preg_replace('/;keyid="[^"]*"/', '', (string)$headers['Signature-Input']); - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest($body, $req); - } - - public function testExtraCoveredDateStillVerifies(): void { - // covering more than the mandatory components (here: `date`) is - // allowed; only the four baseline components are required - [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManagerWithComponents( - $signatory, - ['@method', '@target-uri', 'content-digest', 'content-length', 'date'], - ); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - - $req = $this->mockRequestFromOutgoing($out, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest($body, $req); - $in->setKey($jwk); - $in->verify(); - $this->addToAssertionCount(1); - } - - public function testSignatureNotCoveringRequiredComponentsRejected(): void { - // A peer that signs only `@method` and `@target-uri`: the body and - // freshness window aren't bound. Even with a valid signature we - // must refuse it. - [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256'); - $signatoryManager = $this->makeSignatoryManagerWithComponents( - $signatory, - ['@method', '@target-uri'], - ); - - $body = 'msg'; - $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares'); - $out->sign(); - $req = $this->mockRequest($out->getHeaders(), 'POST', '/ocm/shares', 'receiver.example.org'); - - $this->expectException(IncomingRequestException::class); - new Rfc9421IncomingSignedRequest($body, $req); - } - - public function testKeyIdIsOpaqueAndOriginIsExternal(): void { - // keyid is opaque; the origin is supplied by the caller via setOrigin(). - $kid = 'sender.example.org#key1'; - [$privatePem, $jwk] = $this->ecdsaP256Jwk($kid); - - $body = 'msg'; - $digest = ContentDigest::compute($body, ContentDigest::ALGO_SHA256); - $paramsLine = '("@method" "@target-uri" "content-digest" "content-length");created=' . time() . ';keyid="' . $kid . '";tag="ocm"'; - $base = implode("\n", [ - '"@method": POST', - '"@target-uri": https://receiver.example.org/ocm/shares', - '"content-digest": ' . $digest, - '"content-length": ' . strlen($body), - '"@signature-params": ' . $paramsLine, - ]); - $rawSig = Algorithm::sign($base, $privatePem, 'ecdsa-p256-sha256'); - $headers = [ - 'Content-Digest' => $digest, - 'Content-Length' => (string)strlen($body), - 'Signature-Input' => 'sig1=' . $paramsLine, - 'Signature' => 'sig1=:' . base64_encode($rawSig) . ':', - ]; - - $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - $in = new Rfc9421IncomingSignedRequest($body, $req); - // The keyid is not parsed; the origin comes from the caller. - $in->setOrigin('sender.example.org'); - $this->assertSame('sender.example.org', $in->getOrigin()); - $in->setKey($jwk); - $in->verify(); - $this->addToAssertionCount(1); - } - - private function skipUnlessSodium(): void { - if (!extension_loaded('sodium')) { - $this->markTestSkipped('ext-sodium is not loaded'); - } - } - - private function makeSignatoryManagerWithComponents(Signatory $signatory, array $components): ISignatoryManager { - return new class($signatory, $components) implements ISignatoryManager { - public function __construct( - private Signatory $sig, - private array $components, - ) { - } - - public function getProviderId(): string { - return 'test'; - } - - public function getOptions(): array { - return [ - 'algorithm' => SignatureAlgorithm::RSA_SHA256, - 'digestAlgorithm' => DigestAlgorithm::SHA256, - 'rfc9421.coveredComponents' => $this->components, - ]; - } - - public function getLocalSignatory(): Signatory { - return $this->sig; - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return null; - } - }; - } - - private function makeSignatoryManagerWithSigningAlgorithm(Signatory $signatory, string $signingAlgorithm): ISignatoryManager { - return new class($signatory, $signingAlgorithm) implements ISignatoryManager { - public function __construct( - private Signatory $sig, - private string $signingAlgorithm, - ) { - } - - public function getProviderId(): string { - return 'test'; - } - - public function getOptions(): array { - return [ - 'algorithm' => SignatureAlgorithm::RSA_SHA256, - 'digestAlgorithm' => DigestAlgorithm::SHA256, - 'rfc9421.signingAlgorithm' => $this->signingAlgorithm, - ]; - } - - public function getLocalSignatory(): Signatory { - return $this->sig; - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return null; - } - }; - } - - /** - * @return array{0: Signatory, 1: \Firebase\JWT\Key} - */ - private function ecdsaP256Material(string $kid): array { - $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']); - $privatePem = ''; - openssl_pkey_export($pkey, $privatePem); - $details = openssl_pkey_get_details($pkey); - $publicPem = $details['key']; - - $signatory = new Signatory(true); - $signatory->setKeyId($kid); - $signatory->setPublicKey($publicPem); - $signatory->setPrivateKey($privatePem); - - $key = self::jwkFromEcDetails($details, $kid); - return [$signatory, $key]; - } - - /** - * Key material for a peer whose kid is not a URL; Nextcloud's Signatory - * model cannot represent those. - * - * @return array{0: string, 1: \Firebase\JWT\Key} [private key PEM, verification key] - */ - private function ecdsaP256Jwk(string $kid): array { - $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']); - $privatePem = ''; - openssl_pkey_export($pkey, $privatePem); - $details = openssl_pkey_get_details($pkey); - - return [$privatePem, self::jwkFromEcDetails($details, $kid)]; - } - - private static function jwkFromEcDetails(array $details, string $kid): \Firebase\JWT\Key { - $x = str_pad($details['ec']['x'], 32, "\x00", STR_PAD_LEFT); - $y = str_pad($details['ec']['y'], 32, "\x00", STR_PAD_LEFT); - return JWK::parseKey([ - 'kty' => 'EC', - 'crv' => 'P-256', - 'kid' => $kid, - 'alg' => 'ES256', - 'x' => self::b64url($x), - 'y' => self::b64url($y), - ], 'ES256'); - } - - /** - * @return array{0: Signatory, 1: \Firebase\JWT\Key} - */ - private function ed25519Material(string $kid): array { - $keypair = sodium_crypto_sign_keypair(); - $publicKey = sodium_crypto_sign_publickey($keypair); - $secretKey = sodium_crypto_sign_secretkey($keypair); - $signatory = new Signatory(true); - $signatory->setKeyId($kid); - $signatory->setPublicKey($publicKey); - $signatory->setPrivateKey($secretKey); - $key = JWK::parseKey([ - 'kty' => 'OKP', - 'crv' => 'Ed25519', - 'kid' => $kid, - 'alg' => 'EdDSA', - 'x' => self::b64url($publicKey), - ], 'EdDSA'); - return [$signatory, $key]; - } - - private static function b64url(string $bin): string { - return rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); - } - - private function makeSignatoryManager(Signatory $signatory): ISignatoryManager { - return new class($signatory) implements ISignatoryManager { - public function __construct( - private Signatory $sig, - ) { - } - - public function getProviderId(): string { - return 'test'; - } - - public function getOptions(): array { - return [ - 'algorithm' => SignatureAlgorithm::RSA_SHA256, - 'digestAlgorithm' => DigestAlgorithm::SHA256, - ]; - } - - public function getLocalSignatory(): Signatory { - return $this->sig; - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return null; - } - }; - } - - private function mockRequestFromOutgoing(Rfc9421OutgoingSignedRequest $out, string $method, string $path, string $host): IRequest { - return $this->mockRequest($out->getHeaders(), $method, $path, $host); - } - - private function mockRequest(array $headers, string $method, string $path, string $host): IRequest { - $lowered = []; - foreach ($headers as $name => $value) { - $lowered[strtolower($name)] = (string)$value; - } - $mock = $this->createMock(IRequest::class); - $mock->method('getHeader')->willReturnCallback(static fn (string $h) => $lowered[strtolower($h)] ?? ''); - $mock->method('getMethod')->willReturn($method); - $mock->method('getRequestUri')->willReturn($path); - $mock->method('getServerProtocol')->willReturn('https'); - $mock->method('getServerHost')->willReturn($host); - return $mock; - } -} --- a/tests/lib/Security/Signature/Rfc9421/AlgorithmTest.php +++ /dev/null @@ -1,218 +0,0 @@ -assertSame('ed25519', Algorithm::normalize('ed25519')); - $this->assertSame('rsa-v1_5-sha256', Algorithm::normalize('rsa-v1_5-sha256')); - $this->assertSame('rsa-v1_5-sha384', Algorithm::normalize('rsa-v1_5-sha384')); - $this->assertSame('rsa-v1_5-sha512', Algorithm::normalize('rsa-v1_5-sha512')); - $this->assertSame('ecdsa-p256-sha256', Algorithm::normalize('ecdsa-p256-sha256')); - $this->assertSame('ecdsa-p384-sha384', Algorithm::normalize('ecdsa-p384-sha384')); - } - - public function testNormalizeJoseAliases(): void { - $this->assertSame('ed25519', Algorithm::normalize('EdDSA')); - // fully-specified RFC 9864 name, recommended by the OCM spec - $this->assertSame('ed25519', Algorithm::normalize('Ed25519')); - $this->assertSame('ecdsa-p256-sha256', Algorithm::normalize('ES256')); - $this->assertSame('ecdsa-p384-sha384', Algorithm::normalize('ES384')); - $this->assertSame('rsa-v1_5-sha256', Algorithm::normalize('RS256')); - } - - public function testNormalizeRejectsUnknown(): void { - $this->expectException(SignatureException::class); - Algorithm::normalize('totally-not-real'); - } - - public function testNormalizeRejectsRsaPss(): void { - $this->expectException(SignatureException::class); - Algorithm::normalize('rsa-pss-sha512'); - } - - public function testNormalizeRejectsJosePsAlias(): void { - $this->expectException(SignatureException::class); - Algorithm::normalize('PS512'); - } - - public function testDeriveJoseAlgFromJwk(): void { - $this->assertSame('EdDSA', Algorithm::deriveJoseAlgFromJwk(['kty' => 'OKP', 'crv' => 'Ed25519'])); - $this->assertSame('ES256', Algorithm::deriveJoseAlgFromJwk(['kty' => 'EC', 'crv' => 'P-256'])); - $this->assertSame('ES384', Algorithm::deriveJoseAlgFromJwk(['kty' => 'EC', 'crv' => 'P-384'])); - // RSA: hash function isn't determined by key shape. - $this->assertNull(Algorithm::deriveJoseAlgFromJwk(['kty' => 'RSA'])); - $this->assertNull(Algorithm::deriveJoseAlgFromJwk([])); - } - - public function testEd25519SigningIsRejected(): void { - $this->expectException(SignatureException::class); - $this->expectExceptionMessageMatches('/Ed25519 signing is not supported/'); - Algorithm::sign('payload', str_repeat("\x00", 64), 'ed25519'); - } - - public function testEd25519VerifyRoundTripWithSodium(): void { - $this->skipUnlessSodium(); - [$secret, $key] = $this->ed25519KeyPair(); - $base = 'arbitrary signature base'; - $sig = sodium_crypto_sign_detached($base, $secret); - $this->assertSame(64, strlen($sig)); - $this->assertTrue(Algorithm::verify($base, $sig, $key, 'ed25519')); - // JOSE alias accepted. - $this->assertTrue(Algorithm::verify($base, $sig, $key, 'EdDSA')); - // alg-omitted path resolves through Key alg. - $this->assertTrue(Algorithm::verify($base, $sig, $key, null)); - // tamper detection - $this->assertFalse(Algorithm::verify($base . 'x', $sig, $key, 'ed25519')); - } - - public function testRsaPkcs1RoundTrip(): void { - [$priv, $key] = $this->rsaKeyPair(); - $sig = Algorithm::sign('payload', $priv, 'rsa-v1_5-sha256'); - $this->assertSame(256, strlen($sig)); - $this->assertTrue(Algorithm::verify('payload', $sig, $key, 'rsa-v1_5-sha256')); - $this->assertTrue(Algorithm::verify('payload', $sig, $key, 'RS256')); - } - - public function testEcdsaP256RoundTrip(): void { - [$priv, $key] = $this->ecKeyPair('prime256v1', 'P-256', 'ES256'); - $sig = Algorithm::sign('payload', $priv, 'ecdsa-p256-sha256'); - $this->assertSame(64, strlen($sig)); - $this->assertTrue(Algorithm::verify('payload', $sig, $key, 'ecdsa-p256-sha256')); - $this->assertTrue(Algorithm::verify('payload', $sig, $key, 'ES256')); - } - - public function testEcdsaP384RoundTrip(): void { - [$priv, $key] = $this->ecKeyPair('secp384r1', 'P-384', 'ES384'); - $sig = Algorithm::sign('payload', $priv, 'ecdsa-p384-sha384'); - $this->assertSame(96, strlen($sig)); - $this->assertTrue(Algorithm::verify('payload', $sig, $key, 'ecdsa-p384-sha384')); - } - - public function testKeyTypeMismatchFailsClosed(): void { - [, $rsaKey] = $this->rsaKeyPair(); - $this->expectException(SignatureException::class); - Algorithm::verify('payload', random_bytes(64), $rsaKey, 'ed25519'); - } - - public function testAlgHintConflictsWithJwkAlgRejected(): void { - $this->skipUnlessSodium(); - // Ed25519 JWK, request claims ES256: RFC 9421 §3.2 step 6 disagreement. - [, $key] = $this->ed25519KeyPair(); - $this->expectException(SignatureException::class); - Algorithm::verify('payload', random_bytes(64), $key, 'ES256'); - } - - public function testParseKeyRejectsContradictoryAlg(): void { - $this->markTestSkipped( - 'firebase/php-jwt JWK::parseKey does not validate kty/crv/alg coherence; ' - . 'OCMSignatoryManager::findKid() rejects such keys before parsing ' - . '(see OCMSignatoryManagerJwksTest::testGetRemoteKeyRejectsJwkAlgMismatchingKeyType) ' - . 'and a remaining mismatch is caught at verify() time.' - ); - } - - public function testEcdsaRawToDerProducesValidSignature(): void { - [$priv, $key] = $this->ecKeyPair('prime256v1', 'P-256', 'ES256'); - $rawSig = Algorithm::sign('msg', $priv, 'ecdsa-p256-sha256'); - $der = Algorithm::ecdsaRawToDer($rawSig, 32); - $this->assertNotNull($der); - $this->assertTrue(Algorithm::verify('msg', $rawSig, $key, 'ecdsa-p256-sha256')); - } - - public function testEcdsaRawToDerWrongLength(): void { - $this->assertNull(Algorithm::ecdsaRawToDer('short', 32)); - } - - private function skipUnlessSodium(): void { - if (!extension_loaded('sodium')) { - $this->markTestSkipped('ext-sodium is not loaded'); - } - } - - /** - * @return array{0: string, 1: Key} - */ - private function ed25519KeyPair(): array { - $keypair = sodium_crypto_sign_keypair(); - $publicKey = sodium_crypto_sign_publickey($keypair); - $secretKey = sodium_crypto_sign_secretkey($keypair); - $key = JWK::parseKey([ - 'kty' => 'OKP', - 'crv' => 'Ed25519', - 'kid' => 'k', - 'alg' => 'EdDSA', - 'x' => self::b64url($publicKey), - ], 'EdDSA'); - return [$secretKey, $key]; - } - - /** - * @return array{0: string, 1: Key} - */ - private function rsaKeyPair(): array { - $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => 2048]); - $priv = ''; - openssl_pkey_export($pkey, $priv); - $details = openssl_pkey_get_details($pkey); - $key = JWK::parseKey([ - 'kty' => 'RSA', - 'kid' => 'k', - 'alg' => 'RS256', - 'n' => self::b64url($details['rsa']['n']), - 'e' => self::b64url($details['rsa']['e']), - ], 'RS256'); - return [$priv, $key]; - } - - /** - * @return array{0: string, 1: Key} - */ - private function ecKeyPair(string $opensslCurve, string $jwkCurve, string $joseAlg): array { - $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => $opensslCurve]); - $priv = ''; - openssl_pkey_export($pkey, $priv); - $details = openssl_pkey_get_details($pkey); - $coordinateSize = $opensslCurve === 'prime256v1' ? 32 : 48; - [$x, $y] = self::zeroPadCoordinates($details['ec']['x'], $details['ec']['y'], $coordinateSize); - $key = JWK::parseKey([ - 'kty' => 'EC', - 'crv' => $jwkCurve, - 'kid' => 'k', - 'alg' => $joseAlg, - 'x' => self::b64url($x), - 'y' => self::b64url($y), - ], $joseAlg); - return [$priv, $key]; - } - - /** - * openssl strips leading zero bytes from EC coordinates; JWK requires them - * zero-padded to the full field size (RFC 7518 §6.2.1.2). - * - * @return array{0: string, 1: string} - */ - private static function zeroPadCoordinates(string $x, string $y, int $coordinateSize): array { - return [ - str_pad($x, $coordinateSize, "\x00", STR_PAD_LEFT), - str_pad($y, $coordinateSize, "\x00", STR_PAD_LEFT), - ]; - } - - private static function b64url(string $bin): string { - return rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); - } -} --- a/tests/lib/Security/Signature/Rfc9421/ContentDigestTest.php +++ /dev/null @@ -1,76 +0,0 @@ -assertStringStartsWith('sha-256=:', $header); - $this->assertStringEndsWith(':', $header); - $this->assertTrue(ContentDigest::verify($header, $body)); - } - - public function testDifferentBodyFails(): void { - $header = ContentDigest::compute('hello', ContentDigest::ALGO_SHA256); - $this->assertFalse(ContentDigest::verify($header, 'goodbye')); - } - - public function testSha512(): void { - $header = ContentDigest::compute('payload', ContentDigest::ALGO_SHA512); - $this->assertStringStartsWith('sha-512=:', $header); - $this->assertTrue(ContentDigest::verify($header, 'payload')); - } - - public function testParseMultipleAlgorithmsAcceptsAnyMatch(): void { - $body = 'data'; - $sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256); - $sha512 = ContentDigest::compute($body, ContentDigest::ALGO_SHA512); - $header = $sha256 . ', ' . $sha512; - $this->assertTrue(ContentDigest::verify($header, $body)); - } - - public function testFailsIfAnyRecognisedAlgorithmMismatches(): void { - // All recognised digests must agree. A correct sha-256 alongside a - // wrong sha-512 is treated as an attack on the weaker algorithm, - // not as a successful match on the stronger one. - $body = 'data'; - $sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256); - $wrongSha512 = 'sha-512=:' . base64_encode(hash('sha512', 'tampered', true)) . ':'; - $this->assertFalse(ContentDigest::verify($sha256 . ', ' . $wrongSha512, $body)); - // And the inverse ordering. - $this->assertFalse(ContentDigest::verify($wrongSha512 . ', ' . $sha256, $body)); - } - - public function testUnknownAlgorithmIsIgnored(): void { - $body = 'data'; - $sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256); - $header = 'md5=:abcd:, ' . $sha256; - $this->assertTrue(ContentDigest::verify($header, $body)); - } - - public function testEmptyHeaderFails(): void { - $this->assertFalse(ContentDigest::verify('', 'body')); - } - - public function testGarbageHeaderFails(): void { - $this->assertFalse(ContentDigest::verify('not a digest', 'body')); - } - - public function testParseExtractsRawBytes(): void { - $header = ContentDigest::compute('abc', ContentDigest::ALGO_SHA256); - $parsed = ContentDigest::parse($header); - $this->assertArrayHasKey('sha-256', $parsed); - $this->assertSame(hash('sha256', 'abc', true), $parsed['sha-256']); - } -} --- a/tests/lib/Security/Signature/Rfc9421/SignatureBaseTest.php +++ /dev/null @@ -1,85 +0,0 @@ - 'sha-256=:abcd:', - 'date' => 'Mon, 04 May 2026 12:00:00 GMT', - ], - components: ['@method', '@target-uri', 'content-digest', 'date'], - signatureParamsLine: '("@method" "@target-uri" "content-digest" "date");created=1;keyid="k"', - ); - - $expected = '"@method": POST' . "\n" - . '"@target-uri": https://example.org/foo?bar=baz' . "\n" - . '"content-digest": sha-256=:abcd:' . "\n" - . '"date": Mon, 04 May 2026 12:00:00 GMT' . "\n" - . '"@signature-params": ("@method" "@target-uri" "content-digest" "date");created=1;keyid="k"'; - $this->assertSame($expected, $base); - } - - public function testAuthorityStripsDefaultPort(): void { - $base = SignatureBase::build('GET', 'https://EXAMPLE.org:443/x', [], ['@authority'], '()'); - $this->assertStringContainsString('"@authority": example.org' . "\n", $base); - } - - public function testAuthorityKeepsCustomPort(): void { - $base = SignatureBase::build('GET', 'https://example.org:8443/x', [], ['@authority'], '()'); - $this->assertStringContainsString('"@authority": example.org:8443' . "\n", $base); - } - - public function testQueryComponent(): void { - $base = SignatureBase::build('GET', 'https://example.org/x?a=1', [], ['@query'], '()'); - $this->assertStringContainsString('"@query": ?a=1' . "\n", $base); - } - - public function testMissingFieldThrows(): void { - $this->expectException(SignatureException::class); - SignatureBase::build('GET', 'https://example.org/', [], ['x-missing'], '()'); - } - - public function testFieldValueIsTrimmed(): void { - $base = SignatureBase::build( - 'GET', - 'https://example.org/', - ['date' => ' Mon, 04 May 2026 12:00:00 GMT '], - ['date'], - '()' - ); - $this->assertStringContainsString('"date": Mon, 04 May 2026 12:00:00 GMT' . "\n", $base); - } - - public function testSerializeSignatureParams(): void { - $line = SignatureBase::serializeSignatureParams( - ['@method', '@target-uri'], - ['created' => 100, 'keyid' => 'kid', 'expires' => 200], - ); - $this->assertSame('("@method" "@target-uri");created=100;keyid="kid";expires=200', $line); - } - - public function testSerializeBareItemEscapesQuotes(): void { - $this->assertSame('"\\"hi\\""', SignatureBase::serializeBareItem('"hi"')); - $this->assertSame('"\\\\"', SignatureBase::serializeBareItem('\\')); - } - - public function testSerializeBareItemBoolean(): void { - $this->assertSame('?1', SignatureBase::serializeBareItem(true)); - $this->assertSame('?0', SignatureBase::serializeBareItem(false)); - } -} --- a/tests/lib/Security/Signature/SignatureManagerDispatchTest.php +++ /dev/null @@ -1,288 +0,0 @@ -request = $this->createMock(IRequest::class); - $this->mapper = $this->createMock(SignatoryMapper::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->signatureManager = new SignatureManager( - $this->request, - $this->mapper, - $this->appConfig, - $this->logger, - ); - } - - public function testOutgoingDispatchesToCavageByDefault(): void { - $signatoryManager = $this->rsaSignatoryManager(); - - $signed = $this->signatureManager->getOutgoingSignedRequest( - $signatoryManager, - '{}', - 'POST', - 'https://receiver.example.org/ocm/shares', - ); - - $this->assertNotInstanceOf(Rfc9421OutgoingSignedRequest::class, $signed); - } - - public function testOutgoingDispatchesToRfc9421WhenOptionSet(): void { - [$signatoryManager,] = $this->ecdsaP256SignatoryManager(rfc9421Format: true); - - $signed = $this->signatureManager->getOutgoingSignedRequest( - $signatoryManager, - '{}', - 'POST', - 'https://receiver.example.org/ocm/shares', - ); - - $this->assertInstanceOf(Rfc9421OutgoingSignedRequest::class, $signed); - $headers = $signed->getHeaders(); - $this->assertArrayHasKey('Signature-Input', $headers); - $this->assertStringStartsWith('ocm=(', (string)$headers['Signature-Input']); - } - - public function testInboundDispatchesToRfc9421WhenSignatureInputPresent(): void { - [$signatoryManager, $jwk] = $this->ecdsaP256SignatoryManager(rfc9421Format: true); - - // Build a real signed request and replay its headers as the inbound - // request to exercise the full inbound path including verification. - $body = '{"hello":"world"}'; - $out = new Rfc9421OutgoingSignedRequest( - $body, - $signatoryManager, - 'receiver.example.org', - 'POST', - 'https://receiver.example.org/ocm/shares', - ); - $out->sign(); - $headers = $out->getHeaders(); - - $this->primeRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org'); - - $resolver = $this->makeKeyResolver($signatoryManager, $jwk, 'https://sender.example.org/ocm#ecdsa-p256-sha256'); - - // RFC 9421 verification needs the sender origin from the caller. - $signed = $this->signatureManager->getIncomingSignedRequest($resolver, $body, 'sender.example.org'); - $this->assertInstanceOf(Rfc9421IncomingSignedRequest::class, $signed); - } - - public function testInboundRejectsRfc9421WhenSignatoryManagerCannotResolve(): void { - [$signatoryManager,] = $this->ecdsaP256SignatoryManager(rfc9421Format: true); - - $body = '{"hello":"world"}'; - $out = new Rfc9421OutgoingSignedRequest( - $body, - $signatoryManager, - 'receiver.example.org', - 'POST', - 'https://receiver.example.org/ocm/shares', - ); - $out->sign(); - $this->primeRequest($out->getHeaders(), 'POST', '/ocm/shares', 'receiver.example.org'); - - // $signatoryManager does NOT implement IJwkResolvingSignatoryManager. - $this->expectException(IncomingRequestException::class); - $this->signatureManager->getIncomingSignedRequest($signatoryManager, $body); - } - - public function testInboundRejectsRfc9421WhenNoKeyResolvedForKeyid(): void { - [$signatoryManager, $jwk] = $this->ecdsaP256SignatoryManager(rfc9421Format: true); - - $body = '{"hello":"world"}'; - $out = new Rfc9421OutgoingSignedRequest( - $body, - $signatoryManager, - 'receiver.example.org', - 'POST', - 'https://receiver.example.org/ocm/shares', - ); - $out->sign(); - $this->primeRequest($out->getHeaders(), 'POST', '/ocm/shares', 'receiver.example.org'); - - // resolver knows a different kid only: a present signature whose key - // cannot be resolved is a verification failure, not an unsigned - // request - $resolver = $this->makeKeyResolver($signatoryManager, $jwk, 'https://other.example.org/ocm#nomatch'); - - $this->expectException(IncomingRequestException::class); - $this->signatureManager->getIncomingSignedRequest($resolver, $body, 'sender.example.org'); - } - - private function rsaSignatoryManager(): ISignatoryManager { - $key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_bits' => 2048]); - $priv = ''; - openssl_pkey_export($key, $priv); - $pub = openssl_pkey_get_details($key)['key']; - - $signatory = new Signatory(true); - $signatory->setKeyId('https://sender.example.org/ocm#signature'); - $signatory->setPublicKey($pub); - $signatory->setPrivateKey($priv); - - return new class($signatory) implements ISignatoryManager { - public function __construct( - private Signatory $signatory, - ) { - } - - public function getProviderId(): string { - return 'test'; - } - - public function getOptions(): array { - return [ - 'algorithm' => SignatureAlgorithm::RSA_SHA256, - 'digestAlgorithm' => DigestAlgorithm::SHA256, - ]; - } - - public function getLocalSignatory(): Signatory { - return $this->signatory; - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return null; - } - }; - } - - /** - * @return array{ISignatoryManager, Key} [manager, parsed verification key] - */ - private function ecdsaP256SignatoryManager(bool $rfc9421Format): array { - $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']); - $privatePem = ''; - openssl_pkey_export($pkey, $privatePem); - $details = openssl_pkey_get_details($pkey); - $publicPem = $details['key']; - $kid = 'https://sender.example.org/ocm#ecdsa-p256-sha256'; - - $signatory = new Signatory(true); - $signatory->setKeyId($kid); - $signatory->setPublicKey($publicPem); - $signatory->setPrivateKey($privatePem); - - $x = str_pad($details['ec']['x'], 32, "\x00", STR_PAD_LEFT); - $y = str_pad($details['ec']['y'], 32, "\x00", STR_PAD_LEFT); - $key = JWK::parseKey([ - 'kty' => 'EC', - 'crv' => 'P-256', - 'kid' => $kid, - 'alg' => 'ES256', - 'x' => rtrim(strtr(base64_encode($x), '+/', '-_'), '='), - 'y' => rtrim(strtr(base64_encode($y), '+/', '-_'), '='), - ], 'ES256'); - - $manager = new class($signatory, $rfc9421Format) implements ISignatoryManager { - public function __construct( - private Signatory $signatory, - private bool $rfc9421, - ) { - } - - public function getProviderId(): string { - return 'test'; - } - - public function getOptions(): array { - return [ - 'algorithm' => SignatureAlgorithm::RSA_SHA256, - 'digestAlgorithm' => DigestAlgorithm::SHA256, - 'rfc9421.format' => $this->rfc9421, - ]; - } - - public function getLocalSignatory(): Signatory { - return $this->signatory; - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return null; - } - }; - return [$manager, $key]; - } - - private function makeKeyResolver(ISignatoryManager $delegate, Key $key, string $kid): IJwkResolvingSignatoryManager { - return new class($delegate, $key, $kid) implements IJwkResolvingSignatoryManager { - public function __construct( - private ISignatoryManager $delegate, - private Key $key, - private string $kid, - ) { - } - - public function getProviderId(): string { - return $this->delegate->getProviderId(); - } - - public function getOptions(): array { - return $this->delegate->getOptions(); - } - - public function getLocalSignatory(): Signatory { - return $this->delegate->getLocalSignatory(); - } - - public function getRemoteSignatory(string $remote): ?Signatory { - return $this->delegate->getRemoteSignatory($remote); - } - - public function getRemoteKey(string $origin, string $keyId): ?Key { - return $keyId === $this->kid ? $this->key : null; - } - }; - } - - private function primeRequest(array $headers, string $method, string $path, string $host): void { - $lowered = []; - foreach ($headers as $name => $value) { - $lowered[strtolower($name)] = (string)$value; - } - $this->request->method('getHeader') - ->willReturnCallback(static fn (string $name) => $lowered[strtolower($name)] ?? ''); - $this->request->method('getMethod')->willReturn($method); - $this->request->method('getRequestUri')->willReturn($path); - $this->request->method('getServerProtocol')->willReturn('https'); - $this->request->method('getServerHost')->willReturn($host); - } -} --- a/tests/lib/Security/TrustedDomainHelperTest.php +++ /dev/null @@ -1,147 +0,0 @@ -config = $this->getMockBuilder(IConfig::class)->getMock(); - } - - /** - * @param string $trustedDomains - * @param string $testDomain - * @param bool $result - */ - #[\PHPUnit\Framework\Attributes\DataProvider('trustedDomainDataProvider')] - public function testIsTrustedUrl($trustedDomains, $testDomain, $result): void { - $this->config->method('getSystemValue') - ->willReturnMap([ - ['overwritehost', '', ''], - ['trusted_domains', [], $trustedDomains], - ]); - - $trustedDomainHelper = new TrustedDomainHelper($this->config); - $this->assertEquals($result, $trustedDomainHelper->isTrustedUrl('https://' . $testDomain . '/index.php/something')); - } - - /** - * @param string $trustedDomains - * @param string $testDomain - * @param bool $result - */ - #[\PHPUnit\Framework\Attributes\DataProvider('trustedDomainDataProvider')] - public function testIsTrustedDomain($trustedDomains, $testDomain, $result): void { - $this->config->method('getSystemValue') - ->willReturnMap([ - ['overwritehost', '', ''], - ['trusted_domains', [], $trustedDomains], - ]); - - $trustedDomainHelper = new TrustedDomainHelper($this->config); - $this->assertEquals($result, $trustedDomainHelper->isTrustedDomain($testDomain)); - } - - /** - * @return array - */ - public static function trustedDomainDataProvider(): array { - $trustedHostTestList = [ - 'host.one.test', - 'host.two.test', - '[1fff:0:a88:85a3::ac1f]', - 'host.three.test:443', - '*.leading.host', - 'trailing.host*', - 'cen*ter', - '*.leadingwith.port:123', - 'trailingwith.port*:456', - 'UPPERCASE.DOMAIN', - 'lowercase.domain', - ]; - return [ - // empty defaults to false with 8.1 - [null, 'host.one.test:8080', false], - ['', 'host.one.test:8080', false], - [[], 'host.one.test:8080', false], - // trust list when defined - [$trustedHostTestList, 'host.two.test:8080', true], - [$trustedHostTestList, 'host.two.test:9999', true], - [$trustedHostTestList, 'host.three.test:8080', false], - [$trustedHostTestList, 'host.two.test:8080:aa:222', false], - [$trustedHostTestList, '[1fff:0:a88:85a3::ac1f]', true], - [$trustedHostTestList, '[1fff:0:a88:85a3::ac1f]:801', true], - [$trustedHostTestList, '[1fff:0:a88:85a3::ac1f]:801:34', false], - [$trustedHostTestList, 'host.three.test:443', true], - [$trustedHostTestList, 'host.three.test:80', false], - [$trustedHostTestList, 'host.three.test', false], - // trust localhost regardless of trust list - [$trustedHostTestList, 'localhost', true], - [$trustedHostTestList, 'localhost:8080', true], - [$trustedHostTestList, '127.0.0.1', true], - [$trustedHostTestList, '127.0.0.1:8080', true], - // do not trust invalid localhosts - [$trustedHostTestList, 'localhost:1:2', false], - [$trustedHostTestList, 'localhost: evil.host', false], - // do not trust casting - [[1], '1', false], - // leading * - [$trustedHostTestList, 'abc.leading.host', true], - [$trustedHostTestList, 'abc.def.leading.host', true], - [$trustedHostTestList, 'abc.def.leading.host.another', false], - [$trustedHostTestList, 'abc.def.leading.host:123', true], - [$trustedHostTestList, 'leading.host', false], - // trailing * - [$trustedHostTestList, 'trailing.host', true], - [$trustedHostTestList, 'trailing.host.abc', true], - [$trustedHostTestList, 'trailing.host.abc.def', true], - [$trustedHostTestList, 'trailing.host.abc:123', true], - [$trustedHostTestList, 'another.trailing.host', false], - // center * - [$trustedHostTestList, 'center', true], - [$trustedHostTestList, 'cenxxxter', true], - [$trustedHostTestList, 'cen.x.y.ter', true], - // with port - [$trustedHostTestList, 'abc.leadingwith.port:123', true], - [$trustedHostTestList, 'abc.leadingwith.port:1234', false], - [$trustedHostTestList, 'trailingwith.port.abc:456', true], - [$trustedHostTestList, 'trailingwith.port.abc:123', false], - // bad hostname - [$trustedHostTestList, '-bad', false], - [$trustedHostTestList, '-bad.leading.host', false], - [$trustedHostTestList, 'bad..der.leading.host', false], - // case sensitivity - [$trustedHostTestList, 'uppercase.domain', true], - [$trustedHostTestList, 'LOWERCASE.DOMAIN', true], - ]; - } - - public function testIsTrustedDomainOverwriteHost(): void { - $this->config->method('getSystemValue') - ->with('overwritehost') - ->willReturn('myproxyhost'); - - $trustedDomainHelper = new TrustedDomainHelper($this->config); - $this->assertTrue($trustedDomainHelper->isTrustedDomain('myproxyhost')); - $this->assertTrue($trustedDomainHelper->isTrustedDomain('myotherhost')); - } -} --- a/tests/lib/Security/VerificationToken/VerificationTokenTest.php +++ /dev/null @@ -1,329 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->crypto = $this->createMock(ICrypto::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->secureRandom = $this->createMock(ISecureRandom::class); - $this->jobList = $this->createMock(IJobList::class); - $this->lastInteractiveLogin = $this->createMock(LastInteractiveLogin::class); - - $this->token = new VerificationToken( - $this->config, - $this->crypto, - $this->timeFactory, - $this->secureRandom, - $this->jobList, - $this->lastInteractiveLogin, - ); - } - - protected function mockLastInteractiveLogin(int $timestamp): void { - $this->lastInteractiveLogin->expects($this->atLeastOnce()) - ->method('get') - ->willReturn($timestamp); - } - - public function testTokenUserUnknown(): void { - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::USER_UNKNOWN); - $this->token->check('encryptedToken', null, 'fingerprintToken', 'foobar'); - } - - public function testTokenUserUnknown2(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(false); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::USER_UNKNOWN); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenNotFound(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - // implicit: IConfig::getUserValue returns null by default - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_NOT_FOUND); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenDecryptionError(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willThrowException(new \Exception('decryption failed')); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_DECRYPTION_ERROR); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenInvalidFormat(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('decrypted^nonsense'); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_INVALID_FORMAT); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenExpired(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('604800:mY70K3n'); - - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturn(604800 * 3); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_EXPIRED); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenExpiredByLogin(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - $this->mockLastInteractiveLogin(604803); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('604800:mY70K3n'); - - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturn(604801); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_EXPIRED); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar', true); - } - - public function testTokenNotExpiredBySessionRevalidation(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - $user->expects($this->never()) - ->method('getLastLogin'); - // last actual authentication predates the token - $this->mockLastInteractiveLogin(604700); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('604800:barfoo'); - - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturn(604801); - - $this->token->check('barfoo', $user, 'fingerprintToken', 'foobar', true); - } - - public function testTokenMismatch(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('604802:mY70K3n'); - - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturn(604801); - - $this->expectException(InvalidTokenException::class); - $this->expectExceptionCode(InvalidTokenException::TOKEN_MISMATCH); - $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); - } - - public function testTokenSuccess(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('isEnabled') - ->willReturn(true); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('alice'); - - $this->config->expects($this->atLeastOnce()) - ->method('getUserValue') - ->with('alice', 'core', 'fingerprintToken', null) - ->willReturn('encryptedToken'); - $this->config->expects($this->any()) - ->method('getSystemValueString') - ->with('secret') - ->willReturn('357111317'); - - $this->crypto->method('decrypt') - ->with('encryptedToken', 'foobar' . '357111317') - ->willReturn('604802:barfoo'); - - $this->timeFactory->expects($this->any()) - ->method('getTime') - ->willReturn(604801); - - $this->token->check('barfoo', $user, 'fingerprintToken', 'foobar'); - } - - public function testCreate(): void { - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('alice'); - - $this->secureRandom->expects($this->atLeastOnce()) - ->method('generate') - ->willReturn('barfoo'); - $this->crypto->expects($this->atLeastOnce()) - ->method('encrypt') - ->willReturn('encryptedToken'); - $this->config->expects($this->atLeastOnce()) - ->method('setUserValue') - ->with('alice', 'core', 'fingerprintToken', 'encryptedToken'); - - $vToken = $this->token->create($user, 'fingerprintToken', 'foobar'); - $this->assertSame('barfoo', $vToken); - } -} --- a/tests/lib/ServerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -server = new Server('', $config); - } - - public static function dataTestQuery(): array { - return [ - ['\OCP\Activity\IManager', '\OC\Activity\Manager'], - ['\OCP\IConfig', '\OC\AllConfig'], - ['\OCP\IAppConfig', '\OC\AppConfig'], - [AppFetcher::class, AppFetcher::class], - ['\OCP\App\IAppManager', '\OC\App\AppManager'], - ['\OCP\Command\IBus', '\OC\Command\AsyncBus'], - ['\OCP\IAvatarManager', '\OC\Avatar\AvatarManager'], - ]; - } - - /** - * - * @param string $serviceName - * @param string $instanceOf - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataTestQuery')] - public function testQuery(string $serviceName, string $instanceOf): void { - $this->assertInstanceOf($instanceOf, $this->server->query($serviceName), 'Service "' . $serviceName . '"" did not return the right class'); - } - - public function testOverwriteDefaultCommentsManager(): void { - $config = $this->server->get(IConfig::class); - $defaultManagerFactory = $config->getSystemValue('comments.managerFactory', '\OC\Comments\ManagerFactory'); - - $config->setSystemValue('comments.managerFactory', '\Test\Comments\FakeFactory'); - - $manager = $this->server->get(ICommentsManager::class); - $this->assertInstanceOf('\OCP\Comments\ICommentsManager', $manager); - - $config->setSystemValue('comments.managerFactory', $defaultManagerFactory); - } -} --- a/tests/lib/Session/CryptoSessionDataTest.php +++ /dev/null @@ -1,45 +0,0 @@ -wrappedSession = new Memory(); - $this->crypto = $this->createMock(ICrypto::class); - $this->crypto->expects($this->any()) - ->method('encrypt') - ->willReturnCallback(function ($input) { - return '#' . $input . '#'; - }); - $this->crypto->expects($this->any()) - ->method('decrypt') - ->willReturnCallback(function ($input) { - if ($input === '') { - return ''; - } - return substr($input, 1, -1); - }); - - $this->instance = new CryptoSessionData($this->wrappedSession, $this->crypto, 'PASS'); - } -} --- a/tests/lib/Session/CryptoWrappingTest.php +++ /dev/null @@ -1,66 +0,0 @@ -wrappedSession = $this->getMockBuilder(ISession::class) - ->disableOriginalConstructor() - ->getMock(); - $this->crypto = $this->getMockBuilder('OCP\Security\ICrypto') - ->disableOriginalConstructor() - ->getMock(); - $this->crypto->expects($this->any()) - ->method('encrypt') - ->willReturnCallback(function ($input) { - return $input; - }); - $this->crypto->expects($this->any()) - ->method('decrypt') - ->willReturnCallback(function ($input) { - if ($input === '') { - return ''; - } - return substr($input, 1, -1); - }); - - $this->instance = new CryptoSessionData($this->wrappedSession, $this->crypto, 'PASS'); - } - - public function testUnwrappingGet(): void { - $unencryptedValue = 'foobar'; - $encryptedValue = $this->crypto->encrypt($unencryptedValue); - - $this->wrappedSession->expects($this->once()) - ->method('get') - ->with('encrypted_session_data') - ->willReturnCallback(function () use ($encryptedValue) { - return $encryptedValue; - }); - - $this->assertSame($unencryptedValue, $this->wrappedSession->get('encrypted_session_data')); - } -} --- a/tests/lib/Session/MemoryTest.php +++ /dev/null @@ -1,26 +0,0 @@ -instance = new Memory(); - } - - public function testThrowsExceptionOnGetId(): void { - $this->expectException(SessionNotAvailableException::class); - - $this->instance->getId(); - } -} --- a/tests/lib/Session/Session.php +++ /dev/null @@ -1,67 +0,0 @@ -instance->clear(); - parent::tearDown(); - } - - public function testNotExistsEmpty(): void { - $this->assertFalse($this->instance->exists('foo')); - } - - public function testExistsAfterSet(): void { - $this->instance->set('foo', 1); - $this->assertTrue($this->instance->exists('foo')); - } - - public function testNotExistsAfterRemove(): void { - $this->instance->set('foo', 1); - $this->instance->remove('foo'); - $this->assertFalse($this->instance->exists('foo')); - } - - public function testGetNonExisting(): void { - $this->assertNull($this->instance->get('foo')); - } - - public function testGetAfterSet(): void { - $this->instance->set('foo', 'bar'); - $this->assertEquals('bar', $this->instance->get(('foo'))); - } - - public function testRemoveNonExisting(): void { - $this->assertFalse($this->instance->exists('foo')); - $this->instance->remove('foo'); - $this->assertFalse($this->instance->exists('foo')); - } - - public function testNotExistsAfterClear(): void { - $this->instance->set('foo', 1); - $this->instance->clear(); - $this->assertFalse($this->instance->exists('foo')); - } - - public function testArrayInterface(): void { - $this->assertFalse(isset($this->instance['foo'])); - $this->instance['foo'] = 'bar'; - $this->assertTrue(isset($this->instance['foo'])); - $this->assertEquals('bar', $this->instance['foo']); - unset($this->instance['foo']); - $this->assertFalse(isset($this->instance['foo'])); - } -} --- a/tests/lib/Settings/DeclarativeManagerTest.php +++ /dev/null @@ -1,639 +0,0 @@ - 'test_form_1', - 'priority' => 10, - 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, // admin, personal - 'section_id' => 'additional', - 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, // external, internal (handled by core to store in appconfig and preferences) - 'title' => 'Test declarative settings', // NcSettingsSection name - 'description' => 'These fields are rendered dynamically from declarative schema', // NcSettingsSection description - 'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed - 'fields' => [ - [ - 'id' => 'test_field_7', // configkey - 'title' => 'Multi-selection', // name or label - 'description' => 'Select some option setting', // hint - 'type' => DeclarativeSettingsTypes::MULTI_SELECT, - 'options' => ['foo', 'bar', 'baz'], // simple options for select, radio, multi-select - 'placeholder' => 'Select some multiple options', // input placeholder - 'default' => ['foo', 'bar'], - ], - [ - 'id' => 'some_real_setting', - 'title' => 'Select single option', - 'description' => 'Single option radio buttons', - 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) - 'placeholder' => 'Select single option, test interval', - 'default' => '40m', - 'options' => [ - [ - 'name' => 'Each 40 minutes', // NcCheckboxRadioSwitch display name - 'value' => '40m' // NcCheckboxRadioSwitch value - ], - [ - 'name' => 'Each 60 minutes', - 'value' => '60m' - ], - [ - 'name' => 'Each 120 minutes', - 'value' => '120m' - ], - [ - 'name' => 'Each day', - 'value' => 60 * 24 . 'm' - ], - ], - ], - [ - 'id' => 'test_field_1', // configkey - 'title' => 'Default text field', // label - 'description' => 'Set some simple text setting', // hint - 'type' => DeclarativeSettingsTypes::TEXT, - 'placeholder' => 'Enter text setting', // placeholder - 'default' => 'foo', - ], - [ - 'id' => 'test_field_1_1', - 'title' => 'Email field', - 'description' => 'Set email config', - 'type' => DeclarativeSettingsTypes::EMAIL, - 'placeholder' => 'Enter email', - 'default' => '', - ], - [ - 'id' => 'test_field_1_2', - 'title' => 'Tel field', - 'description' => 'Set tel config', - 'type' => DeclarativeSettingsTypes::TEL, - 'placeholder' => 'Enter your tel', - 'default' => '', - ], - [ - 'id' => 'test_field_1_3', - 'title' => 'Url (website) field', - 'description' => 'Set url config', - 'type' => 'url', - 'placeholder' => 'Enter url', - 'default' => '', - ], - [ - 'id' => 'test_field_1_4', - 'title' => 'Number field', - 'description' => 'Set number config', - 'type' => DeclarativeSettingsTypes::NUMBER, - 'placeholder' => 'Enter number value', - 'default' => 0, - ], - [ - 'id' => 'test_field_2', - 'title' => 'Password', - 'description' => 'Set some secure value setting', - 'type' => 'password', - 'placeholder' => 'Set secure value', - 'default' => '', - ], - [ - 'id' => 'test_field_3', - 'title' => 'Selection', - 'description' => 'Select some option setting', - 'type' => DeclarativeSettingsTypes::SELECT, - 'options' => ['foo', 'bar', 'baz'], - 'placeholder' => 'Select some option setting', - 'default' => 'foo', - ], - [ - 'id' => 'test_field_4', - 'title' => 'Toggle something', - 'description' => 'Select checkbox option setting', - 'type' => DeclarativeSettingsTypes::CHECKBOX, - 'label' => 'Verify something if enabled', - 'default' => false, - ], - [ - 'id' => 'test_field_5', - 'title' => 'Multiple checkbox toggles, describing one setting, checked options are saved as an JSON object {foo: true, bar: false}', - 'description' => 'Select checkbox option setting', - 'type' => DeclarativeSettingsTypes::MULTI_CHECKBOX, - 'default' => ['foo' => true, 'bar' => true], - 'options' => [ - [ - 'name' => 'Foo', - 'value' => 'foo', // multiple-checkbox configkey - ], - [ - 'name' => 'Bar', - 'value' => 'bar', - ], - [ - 'name' => 'Baz', - 'value' => 'baz', - ], - [ - 'name' => 'Qux', - 'value' => 'qux', - ], - ], - ], - [ - 'id' => 'test_field_6', - 'title' => 'Radio toggles, describing one setting like single select', - 'description' => 'Select radio option setting', - 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) - 'label' => 'Select single toggle', - 'default' => 'foo', - 'options' => [ - [ - 'name' => 'First radio', // NcCheckboxRadioSwitch display name - 'value' => 'foo' // NcCheckboxRadioSwitch value - ], - [ - 'name' => 'Second radio', - 'value' => 'bar' - ], - [ - 'name' => 'Second radio', - 'value' => 'baz' - ], - ], - ], - [ - 'id' => 'test_sensitive_field', - 'title' => 'Sensitive text field', - 'description' => 'Set some secure value setting that is stored encrypted', - 'type' => DeclarativeSettingsTypes::TEXT, - 'label' => 'Sensitive field', - 'placeholder' => 'Set secure value', - 'default' => '', - 'sensitive' => true, // only for TEXT, PASSWORD types - ], - [ - 'id' => 'test_sensitive_field_2', - 'title' => 'Sensitive password field', - 'description' => 'Set some password setting that is stored encrypted', - 'type' => DeclarativeSettingsTypes::PASSWORD, - 'label' => 'Sensitive field', - 'placeholder' => 'Set secure value', - 'default' => '', - 'sensitive' => true, // only for TEXT, PASSWORD types - ], - [ - 'id' => 'test_non_sensitive_field', - 'title' => 'Password field', - 'description' => 'Set some password setting', - 'type' => DeclarativeSettingsTypes::PASSWORD, - 'label' => 'Password field', - 'placeholder' => 'Set secure value', - 'default' => '', - 'sensitive' => false, - ], - ], - ]; - - public static bool $testSetInternalValueAfterChange = false; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->coordinator = $this->createMock(Coordinator::class); - $this->config = $this->createMock(IConfig::class); - $this->appConfig = $this->createMock(IAppConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->crypto = $this->createMock(ICrypto::class); - - $this->declarativeManager = new DeclarativeManager( - $this->eventDispatcher, - $this->groupManager, - $this->coordinator, - $this->config, - $this->appConfig, - $this->logger, - $this->crypto, - ); - - $this->user = $this->createMock(IUser::class); - $this->user->expects($this->any()) - ->method('getUID') - ->willReturn('test_user'); - - $this->adminUser = $this->createMock(IUser::class); - $this->adminUser->expects($this->any()) - ->method('getUID') - ->willReturn('admin_test_user'); - - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->willReturnCallback(function ($userId) { - return $userId === 'admin_test_user'; - }); - } - - public function testRegisterSchema(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - } - - /** - * Simple test to verify that exception is thrown when trying to register schema with duplicate id - */ - public function testRegisterDuplicateSchema(): void { - $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); - $this->expectException(\Exception::class); - $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); - } - - /** - * It's not allowed to register schema with duplicate fields ids for the same app - */ - public function testRegisterSchemaWithDuplicateFields(): void { - // Register first valid schema - $this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); - // Register second schema with duplicate fields, but different schema id - $this->expectException(\Exception::class); - $schema = self::validSchemaAllFields; - $schema['id'] = 'test_form_2'; - $this->declarativeManager->registerSchema('testing', $schema); - } - - public function testRegisterMultipleSchemasAndDuplicate(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - // 1. Check that form is registered for the app - $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - $app = 'testing2'; - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - // 2. Check that form is registered for the second app - $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - $app = 'testing'; - $this->expectException(\Exception::class); // expecting duplicate form id and duplicate fields ids exception - $this->declarativeManager->registerSchema($app, $schema); - $schemaDuplicateFields = self::validSchemaAllFields; - $schemaDuplicateFields['id'] = 'test_form_2'; // change form id to test duplicate fields - $this->declarativeManager->registerSchema($app, $schemaDuplicateFields); - // 3. Check that not valid form with duplicate fields is not registered - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schemaDuplicateFields['section_type'], $schemaDuplicateFields['section_id']); - $this->assertFalse(isset($formIds[$app]) && in_array($schemaDuplicateFields['id'], $formIds[$app])); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataValidateSchema')] - public function testValidateSchema(bool $expected, bool $expectException, string $app, array $schema): void { - if ($expectException) { - $this->expectException(\Exception::class); - } - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertEquals($expected, isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - } - - public static function dataValidateSchema(): array { - return [ - 'valid schema with all supported fields' => [ - true, - false, - 'testing', - self::validSchemaAllFields, - ], - 'invalid schema with missing id' => [ - false, - true, - 'testing', - [ - 'priority' => 10, - 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, - 'section_id' => 'additional', - 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, - 'title' => 'Test declarative settings', - 'description' => 'These fields are rendered dynamically from declarative schema', - 'doc_url' => '', - 'fields' => [ - [ - 'id' => 'test_field_7', - 'title' => 'Multi-selection', - 'description' => 'Select some option setting', - 'type' => DeclarativeSettingsTypes::MULTI_SELECT, - 'options' => ['foo', 'bar', 'baz'], - 'placeholder' => 'Select some multiple options', - 'default' => ['foo', 'bar'], - ], - ], - ], - ], - 'invalid schema with invalid field' => [ - false, - true, - 'testing', - [ - 'id' => 'test_form_1', - 'priority' => 10, - 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, - 'section_id' => 'additional', - 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, - 'title' => 'Test declarative settings', - 'description' => 'These fields are rendered dynamically from declarative schema', - 'doc_url' => '', - 'fields' => [ - [ - 'id' => 'test_invalid_field', - 'title' => 'Invalid field', - 'description' => 'Some invalid setting description', - 'type' => 'some_invalid_type', - 'placeholder' => 'Some invalid field placeholder', - 'default' => null, - ], - ], - ], - ], - ]; - } - - public function testGetFormIDs(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - $app = 'testing2'; - $this->declarativeManager->registerSchema($app, $schema); - $formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); - } - - /** - * Check that form with default values is returned with internal storage_type - */ - public function testGetFormsWithDefaultValues(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback(fn ($app, $configkey, $default) => $default); - - $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertNotEmpty($forms); - $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); - // Check some_real_setting field default value - $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; - $schemaSomeRealSettingField = array_values(array_filter($schema['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; - $this->assertEquals($schemaSomeRealSettingField['default'], $someRealSettingField['default']); - } - - /** - * Check values in json format to ensure that they are properly encoded - */ - public function testGetFormsWithDefaultValuesJson(): void { - $app = 'testing'; - $schema = [ - 'id' => 'test_form_1', - 'priority' => 10, - 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL, - 'section_id' => 'additional', - 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, - 'title' => 'Test declarative settings', - 'description' => 'These fields are rendered dynamically from declarative schema', - 'doc_url' => '', - 'fields' => [ - [ - 'id' => 'test_field_json', - 'title' => 'Multi-selection', - 'description' => 'Select some option setting', - 'type' => DeclarativeSettingsTypes::MULTI_SELECT, - 'options' => ['foo', 'bar', 'baz'], - 'placeholder' => 'Select some multiple options', - 'default' => ['foo', 'bar'], - ], - ], - ]; - $this->declarativeManager->registerSchema($app, $schema); - - // config->getUserValue() should be called with json encoded default value - $this->config->expects($this->once()) - ->method('getUserValue') - ->with($this->adminUser->getUID(), $app, 'test_field_json', json_encode($schema['fields'][0]['default'])) - ->willReturn(json_encode($schema['fields'][0]['default'])); - - $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertNotEmpty($forms); - $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); - $testFieldJson = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'test_field_json'))[0]; - $this->assertEquals(json_encode($schema['fields'][0]['default']), $testFieldJson['value']); - } - - /** - * Check that saving value for field with internal storage_type is handled by core - */ - public function testSetInternalValue(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - self::$testSetInternalValueAfterChange = false; - - $this->config->expects($this->any()) - ->method('getAppValue') - ->willReturnCallback(function ($app, $configkey, $default) { - if ($configkey === 'some_real_setting' && self::$testSetInternalValueAfterChange) { - return '120m'; - } - return $default; - }); - - $this->appConfig->expects($this->once()) - ->method('setValueString') - ->with($app, 'some_real_setting', '120m'); - - $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); - $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; - $this->assertEquals('40m', $someRealSettingField['value']); // first check that default value (40m) is returned - - // Set new value for some_real_setting field - $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); - self::$testSetInternalValueAfterChange = true; - - $forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); - $this->assertNotEmpty($forms); - $this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); - // Check some_real_setting field default value - $someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; - $this->assertEquals('120m', $someRealSettingField['value']); - } - - public function testSetExternalValue(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - // Change storage_type to external and section_type to personal - $schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL; - $schema['section_type'] = DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL; - $this->declarativeManager->registerSchema($app, $schema); - - $setDeclarativeSettingsValueEvent = new DeclarativeSettingsSetValueEvent( - $this->adminUser, - $app, - $schema['id'], - 'some_real_setting', - '120m' - ); - - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with($setDeclarativeSettingsValueEvent); - $this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); - } - - public function testAdminFormUserUnauthorized(): void { - $app = 'testing'; - $schema = self::validSchemaAllFields; - $this->declarativeManager->registerSchema($app, $schema); - - // A non-admin user must not receive admin declarative forms, but this must not - // throw: it would abort rendering of a section a user can legitimately access - // through admin delegation (which only covers non-declarative settings). - $forms = $this->declarativeManager->getFormsWithValues($this->user, $schema['section_type'], $schema['section_id']); - $this->assertEmpty($forms); - - // Writing to an admin declarative form is still forbidden for a non-admin user. - $this->expectException(\Exception::class); - $this->declarativeManager->setValue($this->user, $app, $schema['id'], 'some_real_setting', '120m'); - } - - /** - * Ensure that the `setValue` method is called if the form implements the handler interface. - */ - public function testSetValueWithHandler(): void { - $schema = self::validSchemaAllFields; - $schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL; - - $form = $this->createMock(IDeclarativeSettingsFormWithHandlers::class); - $form->expects(self::atLeastOnce()) - ->method('getSchema') - ->willReturn($schema); - // The setter should be called once! - $form->expects(self::once()) - ->method('setValue') - ->with('test_field_2', 'some password', $this->adminUser); - - \OC::$server->registerService('OCA\\Testing\\Settings\\DeclarativeForm', fn () => $form, false); - - $context = $this->createMock(RegistrationContext::class); - $context->expects(self::atLeastOnce()) - ->method('getDeclarativeSettings') - ->willReturn([new ServiceRegistration('testing', 'OCA\\Testing\\Settings\\DeclarativeForm')]); - - $this->coordinator->expects(self::atLeastOnce()) - ->method('getRegistrationContext') - ->willReturn($context); - - $this->declarativeManager->loadSchemas(); - - $this->eventDispatcher->expects(self::never()) - ->method('dispatchTyped'); - - $this->declarativeManager->setValue($this->adminUser, 'testing', 'test_form_1', 'test_field_2', 'some password'); - } - - public function testGetValueWithHandler(): void { - $schema = self::validSchemaAllFields; - $schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL; - - $form = $this->createMock(IDeclarativeSettingsFormWithHandlers::class); - $form->expects(self::atLeastOnce()) - ->method('getSchema') - ->willReturn($schema); - // The setter should be called once! - $form->expects(self::once()) - ->method('getValue') - ->with('test_field_2', $this->adminUser) - ->willReturn('very secret password'); - - \OC::$server->registerService('OCA\\Testing\\Settings\\DeclarativeForm', fn () => $form, false); - - $context = $this->createMock(RegistrationContext::class); - $context->expects(self::atLeastOnce()) - ->method('getDeclarativeSettings') - ->willReturn([new ServiceRegistration('testing', 'OCA\\Testing\\Settings\\DeclarativeForm')]); - - $this->coordinator->expects(self::atLeastOnce()) - ->method('getRegistrationContext') - ->willReturn($context); - - $this->declarativeManager->loadSchemas(); - - $this->eventDispatcher->expects(self::never()) - ->method('dispatchTyped'); - - $password = $this->invokePrivate($this->declarativeManager, 'getValue', [$this->adminUser, 'testing', 'test_form_1', 'test_field_2']); - self::assertEquals('very secret password', $password); - } - -} --- a/tests/lib/Settings/ManagerTest.php +++ /dev/null @@ -1,284 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->l10n = $this->createMock(IL10N::class); - $this->l10nFactory = $this->createMock(IFactory::class); - $this->url = $this->createMock(IURLGenerator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->mapper = $this->createMock(AuthorizedGroupMapper::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->subAdmin = $this->createMock(ISubAdmin::class); - $this->appManager = $this->createMock(IAppManager::class); - - $this->manager = new Manager( - $this->logger, - $this->l10nFactory, - $this->url, - $this->container, - $this->mapper, - $this->groupManager, - $this->subAdmin, - $this->appManager, - ); - } - - public function testGetAdminSections(): void { - $this->manager->registerSection('admin', Section::class); - - $section = Server::get(Section::class); - $this->container->method('get') - ->with(Section::class) - ->willReturn($section); - - $this->assertEquals([ - 55 => [$section], - ], $this->manager->getAdminSections()); - } - - public function testGetPersonalSections(): void { - $this->manager->registerSection('personal', Section::class); - - $section = Server::get(Section::class); - $this->container->method('get') - ->with(Section::class) - ->willReturn($section); - - $this->assertEquals([ - 55 => [$section], - ], $this->manager->getPersonalSections()); - } - - public function testGetAdminSectionsEmptySection(): void { - $this->assertEquals([], $this->manager->getAdminSections()); - } - - public function testGetPersonalSectionsEmptySection(): void { - $this->l10nFactory - ->expects($this->once()) - ->method('get') - ->with('lib') - ->willReturn($this->l10n); - $this->l10n - ->expects($this->any()) - ->method('t') - ->willReturnArgument(0); - - $this->assertEquals([], $this->manager->getPersonalSections()); - } - - public function testGetAdminSettings(): void { - $section = $this->createMock(ISettings::class); - $section->method('getPriority') - ->willReturn(13); - $section->method('getSection') - ->willReturn('sharing'); - $this->container->method('get') - ->with('myAdminClass') - ->willReturn($section); - - $this->manager->registerSetting('admin', 'myAdminClass'); - $settings = $this->manager->getAdminSettings('sharing'); - - $this->assertEquals([ - 13 => [$section] - ], $settings); - } - - public function testGetAdminSettingsAsSubAdmin(): void { - $section = $this->createMock(ISettings::class); - $section->method('getPriority') - ->willReturn(13); - $section->method('getSection') - ->willReturn('sharing'); - $this->container->method('get') - ->with('myAdminClass') - ->willReturn($section); - - $this->manager->registerSetting('admin', 'myAdminClass'); - $settings = $this->manager->getAdminSettings('sharing', true); - - $this->assertEquals([], $settings); - } - - public function testGetSubAdminSettingsAsSubAdmin(): void { - $section = $this->createMock(ISubAdminSettings::class); - $section->method('getPriority') - ->willReturn(13); - $section->method('getSection') - ->willReturn('sharing'); - $this->container->expects($this->once()) - ->method('get') - ->with('mySubAdminClass') - ->willReturn($section); - - $this->manager->registerSetting('admin', 'mySubAdminClass'); - $settings = $this->manager->getAdminSettings('sharing', true); - - $this->assertEquals([ - 13 => [$section] - ], $settings); - } - - public function testGetPersonalSettings(): void { - $section = $this->createMock(ISettings::class); - $section->method('getPriority') - ->willReturn(16); - $section->method('getSection') - ->willReturn('security'); - $section2 = $this->createMock(ISettings::class); - $section2->method('getPriority') - ->willReturn(100); - $section2->method('getSection') - ->willReturn('security'); - - $this->manager->registerSetting('personal', 'section1'); - $this->manager->registerSetting('personal', 'section2'); - - $this->container->expects($this->exactly(2)) - ->method('get') - ->willReturnMap([ - ['section1', $section], - ['section2', $section2], - ]); - - $settings = $this->manager->getPersonalSettings('security'); - - $this->assertEquals([ - 16 => [$section], - 100 => [$section2], - ], $settings); - } - - public function testGetPersonalSettingsHidesSettingsOfAppsNotEnabledForUser(): void { - $visible = $this->createMock(ISettings::class); - $visible->method('getPriority') - ->willReturn(16); - $visible->method('getSection') - ->willReturn('security'); - - $this->manager->registerSetting('personal', 'visibleClass', 'enabled_app'); - $this->manager->registerSetting('personal', 'hiddenClass', 'restricted_app'); - - $this->appManager->method('isEnabledForUser') - ->willReturnCallback(static fn (string $appId): bool => $appId === 'enabled_app'); - - // The settings of the app the user has no access to are never instantiated. - $this->container->expects($this->once()) - ->method('get') - ->with('visibleClass') - ->willReturn($visible); - - $this->assertEquals([ - 16 => [$visible], - ], $this->manager->getPersonalSettings('security')); - } - - public function testGetPersonalSectionsHidesSectionsOfAppsNotEnabledForUser(): void { - $this->l10nFactory->method('get') - ->with('lib') - ->willReturn($this->l10n); - $this->l10n->method('t') - ->willReturnArgument(0); - - $this->manager->registerSection('personal', Section::class, 'restricted_app'); - - $this->appManager->method('isEnabledForUser') - ->with('restricted_app') - ->willReturn(false); - - $this->container->expects($this->never()) - ->method('get'); - - $this->assertEquals([], $this->manager->getPersonalSections()); - } - - public function testGetAdminSettingsAreNotHiddenForAppsNotEnabledForUser(): void { - // Admins configure apps they are not a member of themselves. - $setting = $this->createMock(ISettings::class); - $setting->method('getPriority') - ->willReturn(13); - $setting->method('getSection') - ->willReturn('sharing'); - - $this->manager->registerSetting('admin', 'myAdminClass', 'restricted_app'); - - $this->appManager->expects($this->never()) - ->method('isEnabledForUser'); - $this->container->method('get') - ->with('myAdminClass') - ->willReturn($setting); - - $this->assertEquals([ - 13 => [$setting], - ], $this->manager->getAdminSettings('sharing')); - } - - public function testSameSectionAsPersonalAndAdmin(): void { - $this->l10nFactory - ->expects($this->once()) - ->method('get') - ->with('lib') - ->willReturn($this->l10n); - $this->l10n - ->expects($this->any()) - ->method('t') - ->willReturnArgument(0); - - $this->manager->registerSection('personal', Section::class); - $this->manager->registerSection('admin', Section::class); - - $section = Server::get(Section::class); - $this->container->method('get') - ->with(Section::class) - ->willReturn($section); - - $this->assertEquals([ - 55 => [$section], - ], $this->manager->getPersonalSections()); - - $this->assertEquals([ - 55 => [$section], - ], $this->manager->getAdminSections()); - } -} --- a/tests/lib/Settings/SectionTest.php +++ /dev/null @@ -1,23 +0,0 @@ -assertSame('ldap', (new Section('ldap', 'name', 1))->getID()); - } - public function testGetName(): void { - $this->assertSame('name', (new Section('ldap', 'name', 1))->getName()); - } - public function testGetPriority(): void { - $this->assertSame(1, (new Section('ldap', 'name', 1))->getPriority()); - } -} --- a/tests/lib/Setup/AbstractDatabaseTest.php +++ /dev/null @@ -1,289 +0,0 @@ -config = $this->createMock(SystemConfig::class); - $this->connectionFactory = $this->createMock(ConnectionFactory::class); - $this->connection = $this->createMock(Connection::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $l10n = $this->createMock(IL10N::class); - $l10n->method('t') - ->willReturnCallback(fn (string $text, array $parameters = []) => vsprintf($text, $parameters)); - - $this->database = new TestDatabase( - $l10n, - $this->config, - $this->logger, - $this->createMock(ISecureRandom::class), - ); - $this->database->connectionFactory = $this->connectionFactory; - } - - public function testInitializeWritesConnectionConfig(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org:5432', - 'dbtableprefix' => 'nc_', - ]); - - $this->database->initialize([ - 'dbuser' => 'admin', - 'dbpass' => 'admin-password', - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org:5432', - 'dbtableprefix' => 'nc_', - ]); - } - - public function testInitializeFallsBackToLocalhost(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'localhost', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize([ - 'dbuser' => 'admin', - 'dbpass' => 'admin-password', - 'dbname' => 'nextcloud', - 'dbhost' => '', - ]); - } - - /** - * The connection encryption options are only read from the system config, so they have - * to be persisted by initialize() - before any connection is opened by setupDatabase(). - */ - public function testInitializePersistsDriverOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'dbdriveroptions' => [self::MYSQL_ATTR_SSL_CA => '/ca.pem'], - ]); - - $this->database->initialize($this->options([ - 'dbdriveroptions' => [self::MYSQL_ATTR_SSL_CA => '/ca.pem'], - ])); - } - - /** - * Only the options of the database being set up may be persisted, every database - * configures an encrypted connection differently. - */ - public function testInitializeSkipsOptionsOfOtherDatabases(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize($this->options([ - 'pgsql_ssl' => ['mode' => 'verify-full'], - ])); - } - - public static function emptyEncryptionOptions(): array { - return [ - 'not provided' => [[]], - 'empty array' => [['dbdriveroptions' => []]], - 'null' => [['dbdriveroptions' => null]], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('emptyEncryptionOptions')] - public function testInitializeSkipsEmptyEncryptionOptions(array $additional): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize($this->options($additional)); - } - - /** - * A malformed option must never be persisted, as that would end up configuring an - * unencrypted connection while the admin expects an encrypted one. - */ - public function testInitializeRejectsMalformedEncryptionOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - $this->logger->expects($this->once()) - ->method('error'); - - $this->database->initialize($this->options(['dbdriveroptions' => '/ca.pem'])); - } - - public function testValidateRejectsMalformedEncryptionOptions(): void { - $errors = $this->database->validate($this->options(['dbdriveroptions' => '/ca.pem'])); - - $this->assertEquals([ - 'The database option "dbdriveroptions" for Test has to be a list of values', - ], $errors); - } - - public static function encryptionOptions(): array { - return [ - 'dbsslmode' => ['dbsslmode', 'verify-full'], - 'dbsslca' => ['dbsslca', '/ca.pem'], - 'dbsslcert' => ['dbsslcert', '/client.crt'], - 'dbsslkey' => ['dbsslkey', '/client.key'], - 'dbsslcrl' => ['dbsslcrl', '/crl.pem'], - 'dbsslnoverify' => ['dbsslnoverify', true], - ]; - } - - /** - * A database that cannot be configured to use an encrypted connection has to reject - * every such option instead of installing an unencrypted instance silently. - */ - #[\PHPUnit\Framework\Attributes\DataProvider('encryptionOptions')] - public function testValidateRejectsUnsupportedEncryptionOptions(string $option, string|bool $value): void { - $errors = $this->database->validate($this->options([$option => $value])); - - $this->assertContains("The database option \"$option\" is not supported by Test", $errors); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('encryptionOptions')] - public function testInitializeSkipsUnsupportedEncryptionOptions(string $option, string|bool $value): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize($this->options([$option => $value])); - } - - public function testValidateAcceptsEncryptionOptions(): void { - $errors = $this->database->validate($this->options([ - 'dbdriveroptions' => [self::MYSQL_ATTR_SSL_CA => '/ca.pem'], - // not an option of this database, so it is not validated either - 'pgsql_ssl' => 'verify-full', - ])); - - $this->assertEquals([], $errors); - } - - /** - * Host, database name and table prefix must not be passed as additional parameters: - * they are resolved from the system config by the connection factory, so that setup - * connects exactly like the installed instance will. - */ - public function testConnectOnlyPassesCredentials(): void { - $this->expectConnection('mysql', [ - 'user' => 'admin', - 'password' => 'admin-password', - ]); - - $this->database->initialize($this->options()); - - $this->assertSame($this->connection, $this->database->connectForTest()); - } - - public function testConnectPassesPort(): void { - $this->expectConnection('pgsql', [ - 'user' => 'admin', - 'password' => 'admin-password', - 'port' => 5432, - ]); - - $this->database->initialize($this->options(['dbport' => '5432'])); - $this->database->connectForTest(); - } - - public function testConnectPassesSocket(): void { - $this->expectConnection('mysql', [ - 'user' => 'admin', - 'password' => 'admin-password', - 'unix_socket' => '/var/run/mysqld/mysqld.sock', - ]); - - $this->database->initialize($this->options(['dbport' => '/var/run/mysqld/mysqld.sock'])); - $this->database->connectForTest(); - } - - public function testConnectAppliesConfigOverwrite(): void { - $this->expectConnection('pgsql', [ - 'user' => 'admin', - 'password' => 'admin-password', - 'dbname' => 'postgres', - ]); - - $this->database->initialize($this->options()); - $this->database->connectForTest(['dbname' => 'postgres']); - } - - private function options(array $additional = []): array { - return array_merge([ - 'dbuser' => 'admin', - 'dbpass' => 'admin-password', - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - ], $additional); - } - - private function expectConnection(string $dbType, array $expectedParams): void { - $this->config->method('getValue') - ->willReturnCallback(fn ($key, $default = '') => $key === 'dbtype' ? $dbType : $default); - - $this->connectionFactory->expects($this->once()) - ->method('getConnection') - ->with($dbType, $expectedParams) - ->willReturn($this->connection); - - $this->connection->expects($this->once()) - ->method('ensureConnectedToPrimary'); - } -} --- a/tests/lib/Setup/MySQLTest.php +++ /dev/null @@ -1,170 +0,0 @@ -config = $this->createMock(SystemConfig::class); - - $l10n = $this->createMock(IL10N::class); - $l10n->method('t') - ->willReturnCallback(fn (string $text, array $parameters = []) => vsprintf($text, $parameters)); - - $this->database = new MySQL( - $l10n, - $this->config, - $this->createMock(LoggerInterface::class), - $this->createMock(ISecureRandom::class), - ); - } - - /** - * The numeric value of a PDO MySQL attribute, e.g. `SSL_CA`. - * - * The values are not stable across PHP versions, so they must never be hardcoded. - * Since PHP 8.5 the `PDO::MYSQL_ATTR_*` constants are deprecated in favor of - * `Pdo\Mysql::ATTR_*`, and either only exists with the MySQL driver installed. - */ - private function attribute(string $name): int { - if (!extension_loaded('pdo_mysql')) { - $this->markTestSkipped('The pdo_mysql extension is required to resolve the PDO attribute values'); - } - if (PHP_VERSION_ID >= 80500 && class_exists(\Pdo\Mysql::class)) { - return (int)constant('Pdo\Mysql::ATTR_' . $name); - } - return (int)constant('PDO::MYSQL_ATTR_' . $name); - } - - /** - * MySQL/MariaDB is configured through PDO driver options, keyed by the numeric PDO - * attributes - which is why the web installer and CLI cannot pass them directly. - */ - public function testInitializeMapsEncryptionOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'dbdriveroptions' => [ - $this->attribute('SSL_CA') => '/ca.pem', - $this->attribute('SSL_CERT') => '/client.crt', - $this->attribute('SSL_KEY') => '/client.key', - $this->attribute('SSL_VERIFY_SERVER_CERT') => false, - ], - ]); - - $this->database->initialize($this->options([ - 'dbsslca' => '/ca.pem', - 'dbsslcert' => '/client.crt', - 'dbsslkey' => '/client.key', - 'dbsslnoverify' => true, - ])); - } - - /** - * Driver options provided as raw config value, e.g. through an autoconfig file, must - * survive - and their numeric keys must not be renumbered. - */ - public function testInitializeMergesWithRawDriverOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'dbdriveroptions' => [ - $this->attribute('INIT_COMMAND') => 'SET wait_timeout = 28800', - $this->attribute('SSL_CA') => '/ca.pem', - ], - ]); - - $this->database->initialize($this->options([ - 'dbdriveroptions' => [$this->attribute('INIT_COMMAND') => 'SET wait_timeout = 28800'], - 'dbsslca' => '/ca.pem', - ])); - } - - public function testInitializeSkipsEmptyEncryptionOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize($this->options([ - 'dbsslca' => '', - 'dbsslcert' => '', - 'dbsslkey' => '', - 'dbsslnoverify' => false, - ])); - } - - public static function unsupportedEncryptionOptions(): array { - return [ - // There is no PDO equivalent of the PostgreSQL sslmode - 'dbsslmode' => ['dbsslmode', 'require'], - // A revocation list cannot be passed through PDO - 'dbsslcrl' => ['dbsslcrl', '/crl.pem'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('unsupportedEncryptionOptions')] - public function testValidateRejectsUnsupportedEncryptionOptions(string $option, string $value): void { - $errors = $this->database->validate($this->options([$option => $value])); - - $this->assertEquals([ - "The database option \"$option\" is not supported by MySQL/MariaDB", - ], $errors); - } - - public function testValidateRejectsIncompleteClientCertificate(): void { - $errors = $this->database->validate($this->options(['dbsslcert' => '/client.crt'])); - - $this->assertEquals([ - 'The database options "dbsslcert" and "dbsslkey" have to be provided together', - ], $errors); - } - - public function testValidateAcceptsEncryptionOptions(): void { - $errors = $this->database->validate($this->options([ - 'dbsslca' => '/ca.pem', - 'dbsslcert' => '/client.crt', - 'dbsslkey' => '/client.key', - 'dbsslnoverify' => true, - ])); - - $this->assertEquals([], $errors); - } - - private function options(array $additional = []): array { - return array_merge([ - 'dbuser' => 'admin', - 'dbpass' => 'admin-password', - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - ], $additional); - } -} --- a/tests/lib/Setup/PostgreSQLTest.php +++ /dev/null @@ -1,202 +0,0 @@ - 'verify-full', - 'rootcert' => '/rootCA.crt', - 'cert' => '/client.crt', - 'key' => '/client.key', - ]; - - private SystemConfig&MockObject $config; - private LoggerInterface&MockObject $logger; - private PostgreSQL $database; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->config = $this->createMock(SystemConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $l10n = $this->createMock(IL10N::class); - $l10n->method('t') - ->willReturnCallback(fn (string $text, array $parameters = []) => vsprintf($text, $parameters)); - - $this->database = new PostgreSQL( - $l10n, - $this->config, - $this->logger, - $this->createMock(ISecureRandom::class), - ); - } - - /** - * PostgreSQL is configured through its own set of connection parameters instead of PDO - * driver options. They are only read from the system config, so they have to be - * persisted by initialize() - before any connection is opened by setupDatabase(). - */ - public function testInitializePersistsPgsqlSsl(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'pgsql_ssl' => self::PGSQL_SSL, - ]); - - $this->database->initialize($this->options(['pgsql_ssl' => self::PGSQL_SSL])); - } - - /** - * The database independent options provided by the web installer and the CLI have to - * end up in the `pgsql_ssl` connection parameters. - */ - public function testInitializeMapsEncryptionOptions(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'pgsql_ssl' => self::PGSQL_SSL + ['crl' => '/client.crl'], - ]); - - $this->database->initialize($this->options([ - 'dbsslmode' => 'verify-full', - 'dbsslca' => '/rootCA.crt', - 'dbsslcert' => '/client.crt', - 'dbsslkey' => '/client.key', - 'dbsslcrl' => '/client.crl', - ])); - } - - /** - * A `pgsql_ssl` value provided as raw config value, e.g. through an autoconfig file, - * must survive the mapped options. - */ - public function testInitializeMergesWithRawPgsqlSsl(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - 'pgsql_ssl' => ['rootcert' => '/rootCA.crt', 'mode' => 'verify-full'], - ]); - - $this->database->initialize($this->options([ - 'pgsql_ssl' => ['rootcert' => '/rootCA.crt'], - 'dbsslmode' => 'verify-full', - ])); - } - - public function testValidateRejectsUnsupportedEncryptionOptions(): void { - // There is no PDO attribute to skip the host verification for PostgreSQL, - // the sslmode covers it - $errors = $this->database->validate($this->options(['dbsslnoverify' => true])); - - $this->assertEquals([ - 'The database option "dbsslnoverify" is not supported by PostgreSQL', - ], $errors); - } - - public function testValidateRejectsIncompleteClientCertificate(): void { - $errors = $this->database->validate($this->options(['dbsslkey' => '/client.key'])); - - $this->assertEquals([ - 'The database options "dbsslcert" and "dbsslkey" have to be provided together', - ], $errors); - } - - public function testValidateAcceptsEncryptionOptions(): void { - $errors = $this->database->validate($this->options([ - 'dbsslmode' => 'verify-full', - 'dbsslca' => '/rootCA.crt', - 'dbsslcert' => '/client.crt', - 'dbsslkey' => '/client.key', - 'dbsslcrl' => '/client.crl', - ])); - - $this->assertEquals([], $errors); - } - - public static function emptyPgsqlSsl(): array { - return [ - 'not provided' => [[]], - 'empty array' => [['pgsql_ssl' => []]], - 'null' => [['pgsql_ssl' => null]], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('emptyPgsqlSsl')] - public function testInitializeSkipsEmptyPgsqlSsl(array $additional): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - - $this->database->initialize($this->options($additional)); - } - - /** - * A malformed option must never be persisted, as that would end up configuring an - * unencrypted connection while the admin expects an encrypted one. - */ - public function testInitializeRejectsMalformedPgsqlSsl(): void { - $this->config->expects($this->once()) - ->method('setValues') - ->with([ - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - 'dbtableprefix' => 'oc_', - ]); - $this->logger->expects($this->once()) - ->method('error'); - - $this->database->initialize($this->options(['pgsql_ssl' => 'verify-full'])); - } - - public function testValidateRejectsMalformedPgsqlSsl(): void { - $errors = $this->database->validate($this->options(['pgsql_ssl' => 'verify-full'])); - - $this->assertEquals([ - 'The database option "pgsql_ssl" for PostgreSQL has to be a list of values', - ], $errors); - } - - public function testValidateAcceptsPgsqlSsl(): void { - $errors = $this->database->validate($this->options(['pgsql_ssl' => self::PGSQL_SSL])); - - $this->assertEquals([], $errors); - } - - private function options(array $additional = []): array { - return array_merge([ - 'dbuser' => 'admin', - 'dbpass' => 'admin-password', - 'dbname' => 'nextcloud', - 'dbhost' => 'db.example.org', - ], $additional); - } -} --- a/tests/lib/Setup/TestDatabase.php +++ /dev/null @@ -1,36 +0,0 @@ -connectionFactory; - } - - #[\Override] - public function setupDatabase(): void { - } - - public function connectForTest(array $configOverwrite = []): Connection { - return $this->connect($configOverwrite); - } -} --- a/tests/lib/SetupCheck/CheckServerResponseTraitImplementation.php +++ /dev/null @@ -1,40 +0,0 @@ -l10n = $this->createMock(IL10N::class); - $this->l10n->method('t') - ->willReturnArgument(0); - $this->config = $this->createMock(IConfig::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->clientService = $this->createMock(IClientService::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->trait = new CheckServerResponseTraitImplementation( - $this->l10n, - $this->config, - $this->urlGenerator, - $this->clientService, - $this->logger, - ); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataNormalizeUrl')] - public function testNormalizeUrl(string $url, bool $isRootRequest, string $expected): void { - $this->assertEquals($expected, $this->trait->normalizeUrl($url, $isRootRequest)); - } - - public static function dataNormalizeUrl(): array { - return [ - // untouched web-root - 'valid and nothing to change' => ['http://example.com/root', false, 'http://example.com/root'], - 'valid with port and nothing to change' => ['http://example.com:8081/root', false, 'http://example.com:8081/root'], - 'trailing slash' => ['http://example.com/root/', false, 'http://example.com/root'], - 'deep web root' => ['http://example.com/deep/webroot', false, 'http://example.com/deep/webroot'], - // removal of the web-root - 'remove web root' => ['http://example.com/root/', true, 'http://example.com'], - 'remove web root but empty' => ['http://example.com', true, 'http://example.com'], - 'remove deep web root' => ['http://example.com/deep/webroot', true, 'http://example.com'], - 'remove web root with port' => ['http://example.com:8081/root', true, 'http://example.com:8081'], - 'remove web root with port but empty' => ['http://example.com:8081', true, 'http://example.com:8081'], - 'remove web root from IP' => ['https://127.0.0.1/root', true, 'https://127.0.0.1'], - 'remove web root from IP with port' => ['https://127.0.0.1:8080/root', true, 'https://127.0.0.1:8080'], - 'remove web root from IPv6' => ['https://[ff02::1]/root', true, 'https://[ff02::1]'], - 'remove web root from IPv6 with port' => ['https://[ff02::1]:8080/root', true, 'https://[ff02::1]:8080'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetTestUrls')] - public function testGetTestUrls( - string $url, - bool $isRootRequest, - string $cliUrl, - string $webRoot, - array $trustedDomains, - array $expected, - ): void { - $this->config->expects(self::atLeastOnce()) - ->method('getSystemValueString') - ->with('overwrite.cli.url', '') - ->willReturn($cliUrl); - - $this->config->expects(self::atLeastOnce()) - ->method('getSystemValue') - ->with('trusted_domains', []) - ->willReturn($trustedDomains); - - $this->urlGenerator->expects(self::atLeastOnce()) - ->method('getWebroot') - ->willReturn($webRoot); - - $this->urlGenerator->expects(self::atLeastOnce()) - ->method('getBaseUrl') - ->willReturn(self::BASE_URL . $webRoot); - - $result = $this->trait->getTestUrls($url, $isRootRequest); - $this->assertEquals($expected, $result); - } - - /** - * @return array, list}> - */ - public static function dataGetTestUrls(): array { - return [ - 'same cli and base URL' => [ - '/apps/files/js/example.js', false, 'https://nextcloud.local', '', ['nextcloud.local'], [ - // from cli url - 'https://nextcloud.local/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/apps/files/js/example.js', - ] - ], - 'different cli and base URL' => [ - '/apps/files/js/example.js', false, 'https://example.com', '', ['nextcloud.local'], [ - // from cli url - 'https://example.com/apps/files/js/example.js', - // from base url - 'https://nextcloud.local/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/apps/files/js/example.js', - ] - ], - 'different cli and base URL and trusted domains' => [ - '/apps/files/js/example.js', false, 'https://example.com', '', ['nextcloud.local', 'example.com', '127.0.0.1'], [ - // from cli url - 'https://example.com/apps/files/js/example.js', - // from base url - 'https://nextcloud.local/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/apps/files/js/example.js', - 'http://example.com/apps/files/js/example.js', - // trusted domains - 'https://127.0.0.1/apps/files/js/example.js', - 'http://127.0.0.1/apps/files/js/example.js', - ] - ], - 'wildcard trusted domains' => [ - '/apps/files/js/example.js', false, '', '', ['nextcloud.local', '*.example.com'], [ - // from base url - 'https://nextcloud.local/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/apps/files/js/example.js', - // trusted domains with wild card are skipped - ] - ], - 'missing leading slash' => [ - 'apps/files/js/example.js', false, 'https://nextcloud.local', '', ['nextcloud.local'], [ - // from cli url - 'https://nextcloud.local/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/apps/files/js/example.js', - ] - ], - 'keep web-root' => [ - '/apps/files/js/example.js', false, 'https://example.com', '/nextcloud', ['nextcloud.local', 'example.com', '192.168.100.1'], [ - // from cli url (note that the CLI url has NO web root) - 'https://example.com/apps/files/js/example.js', - // from base url - 'https://nextcloud.local/nextcloud/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/nextcloud/apps/files/js/example.js', - // trusted domains with web-root - 'https://example.com/nextcloud/apps/files/js/example.js', - 'http://example.com/nextcloud/apps/files/js/example.js', - 'https://192.168.100.1/nextcloud/apps/files/js/example.js', - 'http://192.168.100.1/nextcloud/apps/files/js/example.js', - ] - ], - // example if the URL is generated by the URL generator - 'keep web-root and web root in url' => [ - '/nextcloud/apps/files/js/example.js', false, 'https://example.com', '/nextcloud', ['nextcloud.local', 'example.com', '192.168.100.1'], [ - // from cli url (note that the CLI url has NO web root) - 'https://example.com/apps/files/js/example.js', - // from base url - 'https://nextcloud.local/nextcloud/apps/files/js/example.js', - // http variant from trusted domains - 'http://nextcloud.local/nextcloud/apps/files/js/example.js', - // trusted domains with web-root - 'https://example.com/nextcloud/apps/files/js/example.js', - 'http://example.com/nextcloud/apps/files/js/example.js', - 'https://192.168.100.1/nextcloud/apps/files/js/example.js', - 'http://192.168.100.1/nextcloud/apps/files/js/example.js', - ] - ], - 'remove web-root' => [ - '/.well-known/caldav', true, 'https://example.com', '/nextcloud', ['nextcloud.local', 'example.com', '192.168.100.1'], [ - // from cli url (note that the CLI url has NO web root) - 'https://example.com/.well-known/caldav', - // from base url - 'https://nextcloud.local/.well-known/caldav', - // http variant from trusted domains - 'http://nextcloud.local/.well-known/caldav', - 'http://example.com/.well-known/caldav', - // trusted domains with web-root - 'https://192.168.100.1/.well-known/caldav', - 'http://192.168.100.1/.well-known/caldav', - ] - ], - ]; - } - -} --- a/tests/lib/SetupCheck/SetupCheckManagerTest.php +++ /dev/null @@ -1,158 +0,0 @@ - */ - public static array $ran = []; - - #[\Override] - public function getCategory(): string { - return 'security'; - } - - #[\Override] - public function getName(): string { - return 'Security test check'; - } - - #[\Override] - public function run(): SetupResult { - self::$ran[] = static::class; - return SetupResult::success(); - } -} - -class SetupCheckManagerTestSystemCheck extends SetupCheckManagerTestSecurityCheck { - #[\Override] - public function getCategory(): string { - return 'system'; - } - - #[\Override] - public function getName(): string { - return 'System test check'; - } -} - -class SetupCheckManagerTest extends TestCase { - private Coordinator&MockObject $coordinator; - private LoggerInterface&MockObject $logger; - private SetupCheckManager $manager; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - SetupCheckManagerTestSecurityCheck::$ran = []; - - $registrationContext = $this->createMock(RegistrationContext::class); - $registrationContext->method('getSetupChecks') - ->willReturn([ - new ServiceRegistration('test', SetupCheckManagerTestSecurityCheck::class), - new ServiceRegistration('test', SetupCheckManagerTestSystemCheck::class), - ]); - - $this->coordinator = $this->createMock(Coordinator::class); - $this->coordinator->method('getRegistrationContext') - ->willReturn($registrationContext); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->manager = new SetupCheckManager($this->coordinator, $this->logger); - } - - /** - * Record the reported progress, together with the checks that already ran when it was reported. - * - * @param list}> $reported - */ - private function createOutput(array &$reported): IOutput&MockObject { - $output = $this->createMock(IOutput::class); - $output->method('debug') - ->willReturnCallback(function (string $message) use (&$reported): void { - $reported[] = [$message, SetupCheckManagerTestSecurityCheck::$ran]; - }); - return $output; - } - - public function testRunAllReportsEveryCheckBeforeAndAfterItIsRun(): void { - $reported = []; - - $results = $this->manager->runAll($this->createOutput($reported)); - - $security = preg_quote(SetupCheckManagerTestSecurityCheck::class, '/'); - $system = preg_quote(SetupCheckManagerTestSystemCheck::class, '/'); - - $this->assertCount(4, $reported); - // The check is reported before it gets a chance to run - $this->assertSame('Starting check Security test check (' . SetupCheckManagerTestSecurityCheck::class . ')', $reported[0][0]); - $this->assertSame([], $reported[0][1]); - $this->assertMatchesRegularExpression( - '/^Check Security test check \(' . $security . '\) done in \d+\.\d\d seconds, peak memory usage: /', - $reported[1][0] - ); - $this->assertSame([SetupCheckManagerTestSecurityCheck::class], $reported[1][1]); - $this->assertSame('Starting check System test check (' . SetupCheckManagerTestSystemCheck::class . ')', $reported[2][0]); - $this->assertSame([SetupCheckManagerTestSecurityCheck::class], $reported[2][1]); - $this->assertMatchesRegularExpression( - '/^Check System test check \(' . $system . '\) done in \d+\.\d\d seconds, peak memory usage: /', - $reported[3][0] - ); - - $this->assertEquals( - ['security' => [SetupCheckManagerTestSecurityCheck::class], 'system' => [SetupCheckManagerTestSystemCheck::class]], - array_map(array_keys(...), $results) - ); - } - - public function testRunByCategoryOnlyReportsMatchingChecks(): void { - $reported = []; - - $this->manager->runByCategory('system', $this->createOutput($reported)); - - $this->assertCount(2, $reported); - $this->assertSame('Starting check System test check (' . SetupCheckManagerTestSystemCheck::class . ')', $reported[0][0]); - $this->assertStringStartsWith('Check System test check (' . SetupCheckManagerTestSystemCheck::class . ') done in ', $reported[1][0]); - $this->assertEquals([SetupCheckManagerTestSystemCheck::class], SetupCheckManagerTestSecurityCheck::$ran); - } - - public function testRunByClassOnlyReportsMatchingChecks(): void { - $reported = []; - - $this->manager->runByClass(SetupCheckManagerTestSecurityCheck::class, $this->createOutput($reported)); - - $this->assertCount(2, $reported); - $this->assertStringContainsString(SetupCheckManagerTestSecurityCheck::class, $reported[0][0]); - $this->assertEquals([SetupCheckManagerTestSecurityCheck::class], SetupCheckManagerTestSecurityCheck::$ran); - } - - public function testRunByClassAcceptsALeadingBackslash(): void { - $results = $this->manager->runByClass('\\' . SetupCheckManagerTestSystemCheck::class); - - $this->assertEquals(['system' => [SetupCheckManagerTestSystemCheck::class]], array_map(array_keys(...), $results)); - } - - public function testRunWithoutOutput(): void { - $results = $this->manager->runAll(); - - $this->assertCount(2, $results); - } -} --- a/tests/lib/SetupTest.php +++ /dev/null @@ -1,176 +0,0 @@ -config = $this->createMock(SystemConfig::class); - $this->iniWrapper = $this->createMock(IniGetWrapper::class); - $this->l10n = $this->createMock(IL10N::class); - $this->l10nFactory = $this->createMock(IL10NFactory::class); - $this->l10nFactory->method('get') - ->willReturn($this->l10n); - $this->defaults = $this->createMock(Defaults::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->random = $this->createMock(ISecureRandom::class); - $this->installer = $this->createMock(Installer::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->setupClass = $this->getMockBuilder(Setup::class) - ->onlyMethods(['class_exists', 'is_callable', 'getAvailableDbDriversForPdo']) - ->setConstructorArgs([$this->config, $this->iniWrapper, $this->l10nFactory, $this->defaults, $this->logger, $this->random, $this->installer, $this->eventDispatcher]) - ->getMock(); - } - - public function testGetSupportedDatabasesWithOneWorking(): void { - $this->config - ->expects($this->once()) - ->method('getValue') - ->willReturn( - ['sqlite', 'mysql', 'oci'] - ); - $this->setupClass - ->expects($this->once()) - ->method('is_callable') - ->willReturn(false); - $this->setupClass - ->expects($this->any()) - ->method('getAvailableDbDriversForPdo') - ->willReturn(['sqlite']); - $result = $this->setupClass->getSupportedDatabases(); - $expectedResult = [ - 'sqlite' => 'SQLite' - ]; - - $this->assertSame($expectedResult, $result); - } - - public function testGetSupportedDatabasesWithNoWorking(): void { - $this->config - ->expects($this->once()) - ->method('getValue') - ->willReturn( - ['sqlite', 'mysql', 'oci', 'pgsql'] - ); - $this->setupClass - ->expects($this->any()) - ->method('is_callable') - ->willReturn(false); - $this->setupClass - ->expects($this->any()) - ->method('getAvailableDbDriversForPdo') - ->willReturn([]); - $result = $this->setupClass->getSupportedDatabases(); - - $this->assertSame([], $result); - } - - public function testGetSupportedDatabasesWithAllWorking(): void { - $this->config - ->expects($this->once()) - ->method('getValue') - ->willReturn( - ['sqlite', 'mysql', 'pgsql', 'oci'] - ); - $this->setupClass - ->expects($this->any()) - ->method('is_callable') - ->willReturn(true); - $this->setupClass - ->expects($this->any()) - ->method('getAvailableDbDriversForPdo') - ->willReturn(['sqlite', 'mysql', 'pgsql']); - $result = $this->setupClass->getSupportedDatabases(); - $expectedResult = [ - 'sqlite' => 'SQLite', - 'mysql' => 'MySQL/MariaDB', - 'pgsql' => 'PostgreSQL', - 'oci' => 'Oracle' - ]; - $this->assertSame($expectedResult, $result); - } - - public function testGetSupportedDatabaseException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Supported databases are not properly configured.'); - - $this->config - ->expects($this->once()) - ->method('getValue') - ->willReturn('NotAnArray'); - $this->setupClass->getSupportedDatabases(); - } - - /** - * @param $url - * @param $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('findWebRootProvider')] - public function testFindWebRootCli($url, $expected): void { - $cliState = \OC::$CLI; - - $this->config - ->expects($this->once()) - ->method('getValue') - ->willReturn($url); - \OC::$CLI = true; - - try { - $webRoot = self::invokePrivate($this->setupClass, 'findWebRoot', [$this->config]); - } catch (\InvalidArgumentException $e) { - $webRoot = false; - } - - \OC::$CLI = $cliState; - $this->assertSame($webRoot, $expected); - } - - public static function findWebRootProvider(): array { - return [ - 'https://www.example.com/nextcloud/' => ['https://www.example.com/nextcloud/', '/nextcloud'], - 'https://www.example.com/nextcloud' => ['https://www.example.com/nextcloud', '/nextcloud'], - 'https://www.example.com/' => ['https://www.example.com/', ''], - 'https://www.example.com' => ['https://www.example.com', ''], - 'https://nctest13pgsql.lan/test123/' => ['https://nctest13pgsql.lan/test123/', '/test123'], - 'https://nctest13pgsql.lan/test123' => ['https://nctest13pgsql.lan/test123', '/test123'], - 'https://nctest13pgsql.lan/' => ['https://nctest13pgsql.lan/', ''], - 'https://nctest13pgsql.lan' => ['https://nctest13pgsql.lan', ''], - 'https://192.168.10.10/nc/' => ['https://192.168.10.10/nc/', '/nc'], - 'https://192.168.10.10/nc' => ['https://192.168.10.10/nc', '/nc'], - 'https://192.168.10.10/' => ['https://192.168.10.10/', ''], - 'https://192.168.10.10' => ['https://192.168.10.10', ''], - 'invalid' => ['invalid', false], - 'empty' => ['', false], - ]; - } -} --- a/tests/lib/Share20/DefaultShareProviderTest.php +++ /dev/null @@ -1,3139 +0,0 @@ -dbConn = Server::get(IDBConnection::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->mailer = $this->createMock(IMailer::class); - $this->l10nFactory = $this->createMock(IFactory::class); - $this->l10n = $this->createMock(IL10N::class); - $this->defaults = $this->getMockBuilder(Defaults::class)->disableOriginalConstructor()->getMock(); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->shareManager = $this->createMock(IShareManager::class); - $this->config = $this->createMock(IConfig::class); - - $this->userManager->expects($this->any())->method('userExists')->willReturn(true); - $this->timeFactory->expects($this->any())->method('now')->willReturn(new \DateTimeImmutable('2023-05-04 00:00 Europe/Berlin')); - - //Empty share table - $this->dbConn->getQueryBuilder()->delete('share')->executeStatement(); - - $this->provider = new DefaultShareProvider( - $this->dbConn, - $this->userManager, - $this->groupManager, - $this->rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ); - } - - #[\Override] - protected function tearDown(): void { - $this->dbConn->getQueryBuilder()->delete('share')->executeStatement(); - $this->dbConn->getQueryBuilder()->delete('filecache')->runAcrossAllShards()->executeStatement(); - $this->dbConn->getQueryBuilder()->delete('storages')->executeStatement(); - parent::tearDown(); - } - - /** - * @param int $shareType - * @param string $sharedWith - * @param string $sharedBy - * @param string $shareOwner - * @param string $itemType - * @param int $fileSource - * @param string $fileTarget - * @param int $permissions - * @param $token - * @param $expiration - * @return int - */ - private function addShareToDB($shareType, $sharedWith, $sharedBy, $shareOwner, - $itemType, $fileSource, $fileTarget, $permissions, $token, $expiration, - $parent = null) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share'); - - if ($shareType) { - $qb->setValue('share_type', $qb->expr()->literal($shareType)); - } - if ($sharedWith) { - $qb->setValue('share_with', $qb->expr()->literal($sharedWith)); - } - if ($sharedBy) { - $qb->setValue('uid_initiator', $qb->expr()->literal($sharedBy)); - } - if ($shareOwner) { - $qb->setValue('uid_owner', $qb->expr()->literal($shareOwner)); - } - if ($itemType) { - $qb->setValue('item_type', $qb->expr()->literal($itemType)); - } - if ($fileSource) { - $qb->setValue('file_source', $qb->expr()->literal($fileSource)); - } - if ($fileTarget) { - $qb->setValue('file_target', $qb->expr()->literal($fileTarget)); - } - if ($permissions) { - $qb->setValue('permissions', $qb->expr()->literal($permissions)); - } - if ($token) { - $qb->setValue('token', $qb->expr()->literal($token)); - } - if ($expiration) { - $qb->setValue('expiration', $qb->createNamedParameter($expiration, IQueryBuilder::PARAM_DATETIME_MUTABLE)); - } - if ($parent) { - $qb->setValue('parent', $qb->expr()->literal($parent)); - } - - $this->assertEquals(1, $qb->executeStatement()); - return $qb->getLastInsertId(); - } - - public function testGetShareByIdNotExist(): void { - $this->expectException(ShareNotFound::class); - - $this->provider->getShareById(1); - } - - public function testGetShareByIdUserShare(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $qb->executeStatement(); - - $id = $qb->getLastInsertId(); - - $sharedBy = $this->createMock(IUser::class); - $sharedBy->method('getUID')->willReturn('sharedBy'); - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $ownerPath = $this->createMock(File::class); - $shareOwnerFolder = $this->createMock(Folder::class); - $shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['shareOwner', $shareOwnerFolder], - ]); - - $share = $this->provider->getShareById($id); - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals($ownerPath, $share->getNode()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals(null, $share->getExpirationDate()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetShareByIdLazy(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $qb->executeStatement(); - - $id = $qb->getLastInsertId(); - - $this->rootFolder->expects($this->never())->method('getUserFolder'); - - $share = $this->provider->getShareById($id); - - // We do not fetch the node so the rootfolder is never called. - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals(null, $share->getExpirationDate()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetShareByIdLazy2(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $qb->executeStatement(); - - $id = $qb->getLastInsertId(); - - $ownerPath = $this->createMock(File::class); - - $shareOwnerFolder = $this->createMock(Folder::class); - $shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath); - - $this->rootFolder - ->method('getUserFolder') - ->with('shareOwner') - ->willReturn($shareOwnerFolder); - - $share = $this->provider->getShareById($id); - - // We fetch the node so the root folder is eventually called - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals($ownerPath, $share->getNode()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals(null, $share->getExpirationDate()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetShareByIdGroupShare(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - // Get the id - $id = $qb->getLastInsertId(); - - $ownerPath = $this->createMock(Folder::class); - $shareOwnerFolder = $this->createMock(Folder::class); - $shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['shareOwner', $shareOwnerFolder], - ]); - - $share = $this->provider->getShareById($id); - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_GROUP, $share->getShareType()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals($ownerPath, $share->getNode()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals(null, $share->getExpirationDate()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetShareByIdUserGroupShare(): void { - $id = $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user0', 'user0', 'file', 42, 'myTarget', 31, null, null); - $this->addShareToDB(2, 'user1', 'user0', 'user0', 'file', 42, 'userTarget', 0, null, null, $id); - - $user0 = $this->createMock(IUser::class); - $user0->method('getUID')->willReturn('user0'); - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - - $group0 = $this->createMock(IGroup::class); - $group0->method('inGroup')->with($user1)->willReturn(true); - $group0->method('getDisplayName')->willReturn('g0-displayname'); - - $node = $this->createMock(Folder::class); - $node->method('getId')->willReturn(42); - $node->method('getName')->willReturn('myTarget'); - - $this->rootFolder->method('getUserFolder')->with('user0')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->willReturn($node); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $user0], - ['user1', $user1], - ]); - $this->groupManager->method('get')->with('group0')->willReturn($group0); - - $share = $this->provider->getShareById($id, 'user1'); - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_GROUP, $share->getShareType()); - $this->assertSame('group0', $share->getSharedWith()); - $this->assertSame('user0', $share->getSharedBy()); - $this->assertSame('user0', $share->getShareOwner()); - $this->assertSame($node, $share->getNode()); - $this->assertEquals(0, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals(null, $share->getExpirationDate()); - $this->assertEquals('userTarget', $share->getTarget()); - } - - public function testGetShareByIdLinkShare(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'password' => $qb->expr()->literal($passwordHash), - 'password_by_talk' => $qb->expr()->literal(true), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - 'token' => $qb->expr()->literal('token'), - 'expiration' => $qb->expr()->literal('2000-01-02 00:00:00'), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $id = $qb->getLastInsertId(); - - $ownerPath = $this->createMock(Folder::class); - $shareOwnerFolder = $this->createMock(Folder::class); - $shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['shareOwner', $shareOwnerFolder], - ]); - - $share = $this->provider->getShareById($id); - - $this->assertEquals($id, $share->getId()); - $this->assertEquals(IShare::TYPE_LINK, $share->getShareType()); - $this->assertNull($share->getSharedWith()); - $this->assertTrue($share->isPasswordHashed()); - $this->assertEquals($passwordHash, $share->getPassword()); - $this->assertEquals(true, $share->getSendPasswordByTalk()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals($ownerPath, $share->getNode()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals('token', $share->getToken()); - $this->assertEquals(\DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-02 00:00:00'), $share->getExpirationDate()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testDeleteSingleShare(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $id = (string)$qb->getLastInsertId(); - - $share = $this->createMock(IShare::class); - $share->method('getId')->willReturn($id); - - /** @var DefaultShareProvider $provider */ - $provider = $this->getMockBuilder(DefaultShareProvider::class) - ->setConstructorArgs([ - $this->dbConn, - $this->userManager, - $this->groupManager, - $this->rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ]) - ->onlyMethods(['getShareById']) - ->getMock(); - - $provider->delete($share); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $cursor = $qb->executeQuery(); - $result = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertEmpty($result); - } - - public function testDeleteSingleShareLazy(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $id = $qb->getLastInsertId(); - - $this->rootFolder->expects($this->never())->method($this->anything()); - - $share = $this->provider->getShareById($id); - $this->provider->delete($share); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $cursor = $qb->executeQuery(); - $result = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertEmpty($result); - } - - public function testDeleteGroupShareWithUserGroupShares(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = (string)$qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(2), - 'share_with' => $qb->expr()->literal('sharedWithUser'), - 'uid_owner' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - 'parent' => $qb->expr()->literal($id), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $share = $this->createMock(IShare::class); - $share->method('getId')->willReturn($id); - $share->method('getShareType')->willReturn(IShare::TYPE_GROUP); - - /** @var DefaultShareProvider $provider */ - $provider = $this->getMockBuilder(DefaultShareProvider::class) - ->setConstructorArgs([ - $this->dbConn, - $this->userManager, - $this->groupManager, - $this->rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ]) - ->onlyMethods(['getShareById']) - ->getMock(); - - $provider->delete($share); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $cursor = $qb->executeQuery(); - $result = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertEmpty($result); - } - - public function testGetChildren(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $qb->executeStatement(); - - // Get the id - $id = (string)$qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('user1'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('user2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2), - 'parent' => $qb->expr()->literal($id), - ]); - $qb->executeStatement(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('group1'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('user3'), - 'item_type' => $qb->expr()->literal('folder'), - 'file_source' => $qb->expr()->literal(3), - 'file_target' => $qb->expr()->literal('myTarget2'), - 'permissions' => $qb->expr()->literal(4), - 'parent' => $qb->expr()->literal($id), - ]); - $qb->executeStatement(); - - $ownerPath = $this->createMock(Folder::class); - $ownerFolder = $this->createMock(Folder::class); - $ownerFolder->method('getFirstNodeById')->willReturn($ownerPath); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['shareOwner', $ownerFolder], - ]); - - $share = $this->createMock(IShare::class); - $share->method('getId')->willReturn($id); - - $children = $this->provider->getChildren($share); - usort($children, fn (IShare $a, IShare $b) => $a->getId() <=> $b->getId()); - - $this->assertCount(2, $children); - - //Child1 - $this->assertEquals(IShare::TYPE_USER, $children[0]->getShareType()); - $this->assertEquals('user1', $children[0]->getSharedWith()); - $this->assertEquals('user2', $children[0]->getSharedBy()); - $this->assertEquals('shareOwner', $children[0]->getShareOwner()); - $this->assertEquals($ownerPath, $children[0]->getNode()); - $this->assertEquals(2, $children[0]->getPermissions()); - $this->assertEquals(null, $children[0]->getToken()); - $this->assertEquals(null, $children[0]->getExpirationDate()); - $this->assertEquals('myTarget1', $children[0]->getTarget()); - - //Child2 - $this->assertEquals(IShare::TYPE_GROUP, $children[1]->getShareType()); - $this->assertEquals('group1', $children[1]->getSharedWith()); - $this->assertEquals('user3', $children[1]->getSharedBy()); - $this->assertEquals('shareOwner', $children[1]->getShareOwner()); - $this->assertEquals($ownerPath, $children[1]->getNode()); - $this->assertEquals(4, $children[1]->getPermissions()); - $this->assertEquals(null, $children[1]->getToken()); - $this->assertEquals(null, $children[1]->getExpirationDate()); - $this->assertEquals('myTarget2', $children[1]->getTarget()); - } - - public function testCreateUserShare(): void { - $share = new Share($this->rootFolder, $this->userManager); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(100); - $path->method('getOwner')->willReturn($shareOwner); - - $ownerFolder = $this->createMock(Folder::class); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['sharedBy', $userFolder], - ['shareOwner', $ownerFolder], - ]); - - $userFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - $ownerFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - - $share->setShareType(IShare::TYPE_USER); - $share->setSharedWith('sharedWith'); - $share->setSharedBy('sharedBy'); - $share->setShareOwner('shareOwner'); - $share->setNode($path); - $share->setSharedWithDisplayName('Displayed Name'); - $share->setSharedWithAvatar('/path/to/image.svg'); - $share->setPermissions(1); - - $attrs = new ShareAttributes(); - $attrs->setAttribute('permissions', 'download', true); - $share->setAttributes($attrs); - - $share->setTarget('/target'); - - $share2 = $this->provider->create($share); - - $this->assertNotNull($share2->getId()); - $this->assertSame('ocinternal:' . $share2->getId(), $share2->getFullId()); - $this->assertSame(IShare::TYPE_USER, $share2->getShareType()); - $this->assertSame('sharedWith', $share2->getSharedWith()); - $this->assertSame('sharedBy', $share2->getSharedBy()); - $this->assertSame('shareOwner', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - $this->assertSame('/target', $share2->getTarget()); - $this->assertLessThanOrEqual(new \DateTime(), $share2->getShareTime()); - $this->assertSame($path, $share2->getNode()); - - // Data is kept after creation - $this->assertSame('Displayed Name', $share->getSharedWithDisplayName()); - $this->assertSame('/path/to/image.svg', $share->getSharedWithAvatar()); - $this->assertSame('Displayed Name', $share2->getSharedWithDisplayName()); - $this->assertSame('/path/to/image.svg', $share2->getSharedWithAvatar()); - - $this->assertSame( - [ - [ - 'scope' => 'permissions', - 'key' => 'download', - 'value' => true - ] - ], - $share->getAttributes()->toArray() - ); - } - - public function testCreateGroupShare(): void { - $share = new Share($this->rootFolder, $this->userManager); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $path = $this->createMock(Folder::class); - $path->method('getId')->willReturn(100); - $path->method('getOwner')->willReturn($shareOwner); - - $ownerFolder = $this->createMock(Folder::class); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['sharedBy', $userFolder], - ['shareOwner', $ownerFolder], - ]); - - $userFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - $ownerFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - - $share->setShareType(IShare::TYPE_GROUP); - $share->setSharedWith('sharedWith'); - $share->setSharedBy('sharedBy'); - $share->setShareOwner('shareOwner'); - $share->setNode($path); - $share->setPermissions(1); - $share->setSharedWithDisplayName('Displayed Name'); - $share->setSharedWithAvatar('/path/to/image.svg'); - $share->setTarget('/target'); - $attrs = new ShareAttributes(); - $attrs->setAttribute('permissions', 'download', true); - $share->setAttributes($attrs); - - $share2 = $this->provider->create($share); - - $this->assertNotNull($share2->getId()); - $this->assertSame('ocinternal:' . $share2->getId(), $share2->getFullId()); - $this->assertSame(IShare::TYPE_GROUP, $share2->getShareType()); - $this->assertSame('sharedWith', $share2->getSharedWith()); - $this->assertSame('sharedBy', $share2->getSharedBy()); - $this->assertSame('shareOwner', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - $this->assertSame('/target', $share2->getTarget()); - $this->assertLessThanOrEqual(new \DateTime(), $share2->getShareTime()); - $this->assertSame($path, $share2->getNode()); - - // Data is kept after creation - $this->assertSame('Displayed Name', $share->getSharedWithDisplayName()); - $this->assertSame('/path/to/image.svg', $share->getSharedWithAvatar()); - $this->assertSame('Displayed Name', $share2->getSharedWithDisplayName()); - $this->assertSame('/path/to/image.svg', $share2->getSharedWithAvatar()); - - $this->assertSame( - [ - [ - 'scope' => 'permissions', - 'key' => 'download', - 'value' => true - ] - ], - $share->getAttributes()->toArray() - ); - } - - public function testCreateLinkShare(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $share = new Share($this->rootFolder, $this->userManager); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $path = $this->createMock(Folder::class); - $path->method('getId')->willReturn(100); - $path->method('getOwner')->willReturn($shareOwner); - - $ownerFolder = $this->createMock(Folder::class); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap([ - ['sharedBy', $userFolder], - ['shareOwner', $ownerFolder], - ]); - - $userFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - $ownerFolder->method('getFirstNodeById') - ->with(100) - ->willReturn($path); - - $share->setShareType(IShare::TYPE_LINK); - $share->setSharedBy('sharedBy'); - $share->setShareOwner('shareOwner'); - $share->setNode($path); - $share->setPermissions(1); - $share->setPasswordHash($passwordHash); - $share->setSendPasswordByTalk(true); - $share->setToken('token'); - $expireDate = new \DateTime(); - $share->setExpirationDate($expireDate); - $share->setTarget('/target'); - - $share2 = $this->provider->create($share); - - $this->assertNotNull($share2->getId()); - $this->assertSame('ocinternal:' . $share2->getId(), $share2->getFullId()); - $this->assertSame(IShare::TYPE_LINK, $share2->getShareType()); - $this->assertSame('sharedBy', $share2->getSharedBy()); - $this->assertSame('shareOwner', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - $this->assertSame('/target', $share2->getTarget()); - $this->assertLessThanOrEqual(new \DateTime(), $share2->getShareTime()); - $this->assertSame($path, $share2->getNode()); - $this->assertTrue($share2->isPasswordHashed()); - $this->assertSame($passwordHash, $share2->getPassword()); - $this->assertSame(true, $share2->getSendPasswordByTalk()); - $this->assertSame('token', $share2->getToken()); - $this->assertEquals($expireDate->getTimestamp(), $share2->getExpirationDate()->getTimestamp()); - } - - public function testGetShareByToken(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'password' => $qb->expr()->literal($passwordHash), - 'password_by_talk' => $qb->expr()->literal(true), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - 'token' => $qb->expr()->literal('secrettoken'), - 'label' => $qb->expr()->literal('the label'), - ]); - $qb->executeStatement(); - $id = $qb->getLastInsertId(); - - $file = $this->createMock(File::class); - - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file); - - $share = $this->provider->getShareByToken('secrettoken'); - $this->assertEquals($id, $share->getId()); - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertSame('sharedBy', $share->getSharedBy()); - $this->assertSame('secrettoken', $share->getToken()); - $this->assertTrue($share->isPasswordHashed()); - $this->assertSame($passwordHash, $share->getPassword()); - $this->assertSame('the label', $share->getLabel()); - $this->assertSame(true, $share->getSendPasswordByTalk()); - $this->assertSame(null, $share->getSharedWith()); - } - - /** - * Assert that if no label is provided the label is correctly, - * as types on IShare, a string and not null - */ - public function testGetShareByTokenNullLabel(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'password' => $qb->expr()->literal($passwordHash), - 'password_by_talk' => $qb->expr()->literal(true), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - 'token' => $qb->expr()->literal('secrettoken'), - ]); - $qb->executeStatement(); - $id = $qb->getLastInsertId(); - - $file = $this->createMock(File::class); - - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file); - - $share = $this->provider->getShareByToken('secrettoken'); - $this->assertEquals($id, $share->getId()); - $this->assertSame('', $share->getLabel()); - } - - public function testGetShareByTokenNotFound(): void { - $this->expectException(ShareNotFound::class); - - $this->provider->getShareByToken('invalidtoken'); - } - - private function createTestStorageEntry($storageStringId) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('storages') - ->values([ - 'id' => $qb->expr()->literal($storageStringId), - ]); - $this->assertEquals(1, $qb->executeStatement()); - return $qb->getLastInsertId(); - } - - private function createTestFileEntry($path, $storage = 1) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->createNamedParameter($storage, IQueryBuilder::PARAM_INT), - 'path' => $qb->createNamedParameter($path), - 'path_hash' => $qb->createNamedParameter(md5($path)), - 'name' => $qb->createNamedParameter(basename($path)), - ]); - $this->assertEquals(1, $qb->executeStatement()); - return $qb->getLastInsertId(); - } - - public static function storageAndFileNameProvider(): array { - return [ - // regular file on regular storage - ['home::shareOwner', 'files/test.txt', 'files/test2.txt'], - // regular file on external storage - ['smb::whatever', 'files/test.txt', 'files/test2.txt'], - // regular file on external storage in trashbin-like folder, - ['smb::whatever', 'files_trashbin/files/test.txt', 'files_trashbin/files/test2.txt'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('storageAndFileNameProvider')] - public function testGetSharedWithUser($storageStringId, $fileName1, $fileName2): void { - $storageId = $this->createTestStorageEntry($storageStringId); - $fileId = $this->createTestFileEntry($fileName1, $storageId); - $fileId2 = $this->createTestFileEntry($fileName2, $storageId); - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith2'), - 'uid_owner' => $qb->expr()->literal('shareOwner2'), - 'uid_initiator' => $qb->expr()->literal('sharedBy2'), - 'item_type' => $qb->expr()->literal('file2'), - 'file_source' => $qb->expr()->literal($fileId2), - 'file_target' => $qb->expr()->literal('myTarget2'), - 'permissions' => $qb->expr()->literal(14), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $file = $this->createMock(File::class); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file); - - $share = $this->provider->getSharedWith('sharedWith', IShare::TYPE_USER, null, 1, 0); - $this->assertCount(1, $share); - - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('storageAndFileNameProvider')] - public function testGetSharedWithGroup($storageStringId, $fileName1, $fileName2): void { - $storageId = $this->createTestStorageEntry($storageStringId); - $fileId = $this->createTestFileEntry($fileName1, $storageId); - $fileId2 = $this->createTestFileEntry($fileName2, $storageId); - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner2'), - 'uid_initiator' => $qb->expr()->literal('sharedBy2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId2), - 'file_target' => $qb->expr()->literal('myTarget2'), - 'permissions' => $qb->expr()->literal(14), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = $qb->getLastInsertId(); - - $groups = []; - foreach (range(0, 100) as $i) { - $groups[] = 'group' . $i; - } - - $groups[] = 'sharedWith'; - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('sharedWith'); - $owner = $this->createMock(IUser::class); - $owner->method('getUID')->willReturn('shareOwner'); - $initiator = $this->createMock(IUser::class); - $initiator->method('getUID')->willReturn('sharedBy'); - - $this->userManager->method('get')->willReturnMap([ - ['sharedWith', $user], - ['shareOwner', $owner], - ['sharedBy', $initiator], - ]); - $this->groupManager - ->method('getUserGroupIds') - ->willReturnCallback(fn (IUser $user) => ($user->getUID() === 'sharedWith' ? $groups : [])); - - $file = $this->createMock(File::class); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file); - - $share = $this->provider->getSharedWith('sharedWith', IShare::TYPE_GROUP, null, 20, 1); - $this->assertCount(1, $share); - - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_GROUP, $share->getShareType()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('storageAndFileNameProvider')] - public function testGetSharedWithGroupUserModified($storageStringId, $fileName1, $fileName2): void { - $storageId = $this->createTestStorageEntry($storageStringId); - $fileId = $this->createTestFileEntry($fileName1, $storageId); - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = $qb->getLastInsertId(); - - /* - * Wrong share. Should not be taken by code. - */ - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(2), - 'share_with' => $qb->expr()->literal('user2'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId), - 'file_target' => $qb->expr()->literal('wrongTarget'), - 'permissions' => $qb->expr()->literal(31), - 'parent' => $qb->expr()->literal($id), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - /* - * Correct share. should be taken by code path. - */ - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(2), - 'share_with' => $qb->expr()->literal('user'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($fileId), - 'file_target' => $qb->expr()->literal('userTarget'), - 'permissions' => $qb->expr()->literal(0), - 'parent' => $qb->expr()->literal($id), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $groups = ['sharedWith']; - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user'); - $owner = $this->createMock(IUser::class); - $owner->method('getUID')->willReturn('shareOwner'); - $initiator = $this->createMock(IUser::class); - $initiator->method('getUID')->willReturn('sharedBy'); - - $this->userManager->method('get')->willReturnMap([ - ['user', $user], - ['shareOwner', $owner], - ['sharedBy', $initiator], - ]); - $this->groupManager - ->method('getUserGroupIds') - ->willReturnCallback(fn (IUser $user) => ($user->getUID() === 'user' ? $groups : [])); - - $file = $this->createMock(File::class); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file); - - $share = $this->provider->getSharedWith('user', IShare::TYPE_GROUP, null, -1, 0); - $this->assertCount(1, $share); - - $share = $share[0]; - $this->assertSame((string)$id, $share->getId()); - $this->assertSame('sharedWith', $share->getSharedWith()); - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertSame('sharedBy', $share->getSharedBy()); - $this->assertSame(IShare::TYPE_GROUP, $share->getShareType()); - $this->assertSame(0, $share->getPermissions()); - $this->assertSame('userTarget', $share->getTarget()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('storageAndFileNameProvider')] - public function testGetSharedWithUserWithNode($storageStringId, $fileName1, $fileName2): void { - $storageId = $this->createTestStorageEntry($storageStringId); - $fileId = $this->createTestFileEntry($fileName1, $storageId); - $fileId2 = $this->createTestFileEntry($fileName2, $storageId); - $this->addShareToDB(IShare::TYPE_USER, 'user0', 'user1', 'user1', - 'file', $fileId, 'myTarget', 31, null, null, null); - $id = $this->addShareToDB(IShare::TYPE_USER, 'user0', 'user1', 'user1', - 'file', $fileId2, 'myTarget', 31, null, null, null); - - $user0 = $this->createMock(IUser::class); - $user0->method('getUID')->willReturn('user0'); - $user0->method('getDisplayName')->willReturn('user0'); - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user0->method('getDisplayName')->willReturn('user0'); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $user0], - ['user1', $user1], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn($fileId2); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($fileId2)->willReturn($file); - - $share = $this->provider->getSharedWith('user0', IShare::TYPE_USER, $file, -1, 0); - $this->assertCount(1, $share); - - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertSame('user0', $share->getSharedWith()); - $this->assertSame('user1', $share->getShareOwner()); - $this->assertSame('user1', $share->getSharedBy()); - $this->assertSame($file, $share->getNode()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('storageAndFileNameProvider')] - public function testGetSharedWithGroupWithNode($storageStringId, $fileName1, $fileName2): void { - $storageId = $this->createTestStorageEntry($storageStringId); - $fileId = $this->createTestFileEntry($fileName1, $storageId); - $fileId2 = $this->createTestFileEntry($fileName2, $storageId); - $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user1', 'user1', - 'file', $fileId, 'myTarget', 31, null, null, null); - $id = $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user1', 'user1', - 'file', $fileId2, 'myTarget', 31, null, null, null); - - $user0 = $this->createMock(IUser::class); - $user0->method('getUID')->willReturn('user0'); - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $user0], - ['user1', $user1], - ]); - - $this->groupManager - ->method('getUserGroupIds') - ->willReturnCallback(fn (IUser $user) => ($user->getUID() === 'user0' ? ['group0'] : [])); - - $node = $this->createMock(Folder::class); - $node->method('getId')->willReturn($fileId2); - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($fileId2)->willReturn($node); - - $share = $this->provider->getSharedWith('user0', IShare::TYPE_GROUP, $node, -1, 0); - $this->assertCount(1, $share); - - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertSame('group0', $share->getSharedWith()); - $this->assertSame('user1', $share->getShareOwner()); - $this->assertSame('user1', $share->getSharedBy()); - $this->assertSame($node, $share->getNode()); - $this->assertEquals(IShare::TYPE_GROUP, $share->getShareType()); - } - - public static function shareTypesProvider(): array { - return [ - [IShare::TYPE_USER, false], - [IShare::TYPE_GROUP, false], - [IShare::TYPE_USER, true], - [IShare::TYPE_GROUP, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('shareTypesProvider')] - public function testGetSharedWithWithDeletedFile($shareType, $trashed): void { - if ($trashed) { - // exists in database but is in trash - $storageId = $this->createTestStorageEntry('home::shareOwner'); - $deletedFileId = $this->createTestFileEntry('files_trashbin/files/test.txt.d1465553223', $storageId); - } else { - // fileid that doesn't exist in the database - $deletedFileId = 123; - } - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal($shareType), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal($deletedFileId), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $file = $this->createMock(File::class); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with($deletedFileId)->willReturn($file); - - $groups = []; - foreach (range(0, 100) as $i) { - $groups[] = 'group' . $i; - } - - $groups[] = 'sharedWith'; - - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('sharedWith'); - $owner = $this->createMock(IUser::class); - $owner->method('getUID')->willReturn('shareOwner'); - $initiator = $this->createMock(IUser::class); - $initiator->method('getUID')->willReturn('sharedBy'); - - $this->userManager->method('get')->willReturnMap([ - ['sharedWith', $user], - ['shareOwner', $owner], - ['sharedBy', $initiator], - ]); - $this->groupManager - ->method('getUserGroupIds') - ->willReturnCallback(fn (IUser $user) => ($user->getUID() === 'sharedWith' ? $groups : [])); - - $share = $this->provider->getSharedWith('sharedWith', $shareType, null, 1, 0); - $this->assertCount(0, $share); - } - - public function testGetSharesBy(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('userTarget'), - 'permissions' => $qb->expr()->literal(0), - 'parent' => $qb->expr()->literal($id), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $file = $this->createMock(File::class); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file); - - $share = $this->provider->getSharesBy('sharedBy', IShare::TYPE_USER, null, false, 1, 0); - $this->assertCount(1, $share); - - /** @var IShare $share */ - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetSharesNode(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(43), - 'file_target' => $qb->expr()->literal('userTarget'), - 'permissions' => $qb->expr()->literal(0), - 'parent' => $qb->expr()->literal($id), - ]); - $this->assertEquals(1, $qb->executeStatement()); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(42); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file); - - $share = $this->provider->getSharesBy('sharedBy', IShare::TYPE_USER, $file, false, 1, 0); - $this->assertCount(1, $share); - - /** @var IShare $share */ - $share = $share[0]; - $this->assertEquals($id, $share->getId()); - $this->assertEquals('sharedWith', $share->getSharedWith()); - $this->assertEquals('shareOwner', $share->getShareOwner()); - $this->assertEquals('sharedBy', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals('myTarget', $share->getTarget()); - } - - public function testGetSharesReshare(): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('shareOwner'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget'), - 'permissions' => $qb->expr()->literal(13), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id1 = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith'), - 'uid_owner' => $qb->expr()->literal('shareOwner'), - 'uid_initiator' => $qb->expr()->literal('sharedBy'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('userTarget'), - 'permissions' => $qb->expr()->literal(0), - ]); - $this->assertEquals(1, $qb->executeStatement()); - $id2 = $qb->getLastInsertId(); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(42); - $this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file); - - $shares = $this->provider->getSharesBy('shareOwner', IShare::TYPE_USER, null, true, -1, 0); - $this->assertCount(2, $shares); - - /** @var IShare $share */ - $share = $shares[0]; - $this->assertEquals($id1, $share->getId()); - $this->assertSame('sharedWith', $share->getSharedWith()); - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertSame('shareOwner', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals('myTarget', $share->getTarget()); - - $share = $shares[1]; - $this->assertEquals($id2, $share->getId()); - $this->assertSame('sharedWith', $share->getSharedWith()); - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertSame('sharedBy', $share->getSharedBy()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals(0, $share->getPermissions()); - $this->assertEquals('userTarget', $share->getTarget()); - } - - public function testDeleteFromSelfGroupNoCustomShare(): void { - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('group'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn('group'); - $group->method('inGroup')->with($user2)->willReturn(true); - $group->method('getDisplayName')->willReturn('group-displayname'); - $this->groupManager->method('get')->with('group')->willReturn($group); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, 'user2'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(2))) - ->executeQuery(); - - $shares = $stmt->fetchAllAssociative(); - $stmt->closeCursor(); - - $this->assertCount(1, $shares); - $share2 = $shares[0]; - $this->assertEquals($id, $share2['parent']); - $this->assertEquals(0, $share2['permissions']); - $this->assertEquals('user2', $share2['share_with']); - } - - public function testDeleteFromSelfGroupAlreadyCustomShare(): void { - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('group'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(2), - 'share_with' => $qb->expr()->literal('user2'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2), - 'parent' => $qb->expr()->literal($id), - ])->executeStatement(); - $this->assertEquals(1, $stmt); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn('group'); - $group->method('inGroup')->with($user2)->willReturn(true); - $group->method('getDisplayName')->willReturn('group-displayname'); - $this->groupManager->method('get')->with('group')->willReturn($group); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, 'user2'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(2))) - ->executeQuery(); - - $shares = $stmt->fetchAllAssociative(); - $stmt->closeCursor(); - - $this->assertCount(1, $shares); - $share2 = $shares[0]; - $this->assertEquals($id, $share2['parent']); - $this->assertEquals(0, $share2['permissions']); - $this->assertEquals('user2', $share2['share_with']); - } - - public function testDeleteFromSelfGroupUserNotInGroup(): void { - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('group'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn('group'); - $group->method('inGroup')->with($user2)->willReturn(false); - $group->method('getDisplayName')->willReturn('group-displayname'); - $this->groupManager->method('get')->with('group')->willReturn($group); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, 'user2'); - } - - public function testDeleteFromSelfGroupDoesNotExist(): void { - $this->expectException(ProviderException::class); - $this->expectExceptionMessage('Group "group" does not exist'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('group'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $this->groupManager->method('get')->with('group')->willReturn(null); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, 'user2'); - } - - public function testDeleteFromSelfUser(): void { - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('user2'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user1->method('getDisplayName')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $user2->method('getDisplayName')->willReturn('user2'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, 'user2'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->executeQuery(); - - $shares = $stmt->fetchAllAssociative(); - $stmt->closeCursor(); - - $this->assertCount(0, $shares); - } - - public function testDeleteFromSelfUserNotRecipient(): void { - $this->expectException(ProviderException::class); - $this->expectExceptionMessage('Recipient does not match'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('user2'), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2) - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user1->method('getDisplayName')->willReturn('user1'); - $user2 = $this->createMock(IUser::class); - $user2->method('getUID')->willReturn('user2'); - $user2->method('getDisplayName')->willReturn('user2'); - $user3 = $this->createMock(IUser::class); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ['user2', $user2], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, $user3); - } - - public function testDeleteFromSelfLink(): void { - $this->expectException(ProviderException::class); - $this->expectExceptionMessage('Invalid shareType'); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(2), - 'token' => $qb->expr()->literal('token'), - ])->executeStatement(); - $this->assertEquals(1, $stmt); - $id = $qb->getLastInsertId(); - - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $this->userManager->method('get')->willReturnMap([ - ['user1', $user1], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(1); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file); - - $share = $this->provider->getShareById($id); - - $this->provider->deleteFromSelf($share, $user1); - } - - public function testUpdateUser(): void { - $id = $this->addShareToDB(IShare::TYPE_USER, 'user0', 'user1', 'user2', - 'file', 42, 'target', 31, null, null); - - $users = []; - for ($i = 0; $i < 6; $i++) { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user' . $i); - $user->method('getDisplayName')->willReturn('user' . $i); - $users['user' . $i] = $user; - } - - $this->userManager->method('get')->willReturnCallback( - function ($userId) use ($users) { - return $users[$userId]; - } - ); - - $file1 = $this->createMock(File::class); - $file1->method('getId')->willReturn(42); - $file2 = $this->createMock(File::class); - $file2->method('getId')->willReturn(43); - - $folder1 = $this->createMock(Folder::class); - $folder1->method('getFirstNodeById')->with(42)->willReturn($file1); - $folder2 = $this->createMock(Folder::class); - $folder2->method('getFirstNodeById')->with(43)->willReturn($file2); - - $this->rootFolder->method('getUserFolder')->willReturnMap([ - ['user2', $folder1], - ['user5', $folder2], - ]); - - $share = $this->provider->getShareById($id); - - $share->setSharedWith('user3'); - $share->setSharedBy('user4'); - $share->setShareOwner('user5'); - $share->setNode($file2); - $share->setPermissions(1); - - $share2 = $this->provider->update($share); - - $this->assertEquals($id, $share2->getId()); - $this->assertSame('user3', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $share2 = $this->provider->getShareById($id); - - $this->assertEquals($id, $share2->getId()); - $this->assertSame('user3', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - } - - public function testUpdateLink(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $id = $this->addShareToDB(IShare::TYPE_LINK, null, 'user1', 'user2', - 'file', 42, 'target', 31, null, null); - - $users = []; - for ($i = 0; $i < 6; $i++) { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user' . $i); - $users['user' . $i] = $user; - } - - $this->userManager->method('get')->willReturnCallback( - function ($userId) use ($users) { - return $users[$userId]; - } - ); - - $file1 = $this->createMock(File::class); - $file1->method('getId')->willReturn(42); - $file2 = $this->createMock(File::class); - $file2->method('getId')->willReturn(43); - - $folder1 = $this->createMock(Folder::class); - $folder1->method('getFirstNodeById')->with(42)->willReturn($file1); - $folder2 = $this->createMock(Folder::class); - $folder2->method('getFirstNodeById')->with(43)->willReturn($file2); - - $this->rootFolder->method('getUserFolder')->willReturnMap([ - ['user2', $folder1], - ['user5', $folder2], - ]); - - $share = $this->provider->getShareById($id); - - $share->setPasswordHash($passwordHash); - $share->setSendPasswordByTalk(true); - $share->setSharedBy('user4'); - $share->setShareOwner('user5'); - $share->setNode($file2); - $share->setPermissions(1); - - $share2 = $this->provider->update($share); - - $this->assertEquals($id, $share2->getId()); - $this->assertTrue($share2->isPasswordHashed()); - $this->assertEquals($passwordHash, $share2->getPassword()); - $this->assertSame(true, $share2->getSendPasswordByTalk()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $share2 = $this->provider->getShareById($id); - - $this->assertEquals($id, $share2->getId()); - $this->assertTrue($share2->isPasswordHashed()); - $this->assertEquals($passwordHash, $share2->getPassword()); - $this->assertSame(true, $share2->getSendPasswordByTalk()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - } - - public function testUpdateLinkRemovePassword(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $id = $this->addShareToDB(IShare::TYPE_LINK, 'foo', 'user1', 'user2', - 'file', 42, 'target', 31, null, null); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->update('share'); - $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); - $qb->set('password', $qb->createNamedParameter($passwordHash)); - $this->assertEquals(1, $qb->executeStatement()); - - $users = []; - for ($i = 0; $i < 6; $i++) { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user' . $i); - $users['user' . $i] = $user; - } - - $this->userManager->method('get')->willReturnCallback( - function ($userId) use ($users) { - return $users[$userId]; - } - ); - - $file1 = $this->createMock(File::class); - $file1->method('getId')->willReturn(42); - $file2 = $this->createMock(File::class); - $file2->method('getId')->willReturn(43); - - $folder1 = $this->createMock(Folder::class); - $folder1->method('getFirstNodeById')->with(42)->willReturn($file1); - $folder2 = $this->createMock(Folder::class); - $folder2->method('getFirstNodeById')->with(43)->willReturn($file2); - - $this->rootFolder->method('getUserFolder')->willReturnMap([ - ['user2', $folder1], - ['user5', $folder2], - ]); - - $share = $this->provider->getShareById($id); - - $share->setPassword(null); - $share->setSharedBy('user4'); - $share->setShareOwner('user5'); - $share->setNode($file2); - $share->setPermissions(1); - - $share2 = $this->provider->update($share); - - $this->assertEquals($id, $share2->getId()); - $this->assertFalse($share2->isPasswordHashed()); - $this->assertEquals(null, $share2->getPassword()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $share2 = $this->provider->getShareById($id); - - $this->assertEquals($id, $share2->getId()); - $this->assertFalse($share2->isPasswordHashed()); - $this->assertEquals(null, $share2->getPassword()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - } - - public function testUpdateGroupNoSub(): void { - $id = $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user1', 'user2', - 'file', 42, 'target', 31, null, null); - - $users = []; - for ($i = 0; $i < 6; $i++) { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user' . $i); - $users['user' . $i] = $user; - } - - $this->userManager->method('get')->willReturnCallback( - function ($userId) use ($users) { - return $users[$userId]; - } - ); - - $groups = []; - for ($i = 0; $i < 2; $i++) { - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn('group' . $i); - $group->method('getDisplayName')->willReturn('group-displayname' . $i); - $groups['group' . $i] = $group; - } - - $this->groupManager->method('get')->willReturnCallback( - function ($groupId) use ($groups) { - return $groups[$groupId]; - } - ); - - $file1 = $this->createMock(File::class); - $file1->method('getId')->willReturn(42); - $file2 = $this->createMock(File::class); - $file2->method('getId')->willReturn(43); - - $folder1 = $this->createMock(Folder::class); - $folder1->method('getFirstNodeById')->with(42)->willReturn($file1); - $folder2 = $this->createMock(Folder::class); - $folder2->method('getFirstNodeById')->with(43)->willReturn($file2); - - $this->rootFolder->method('getUserFolder')->willReturnMap([ - ['user2', $folder1], - ['user5', $folder2], - ]); - - $share = $this->provider->getShareById($id); - - $share->setSharedWith('group0'); - $share->setSharedBy('user4'); - $share->setShareOwner('user5'); - $share->setNode($file2); - $share->setPermissions(1); - - $share2 = $this->provider->update($share); - - $this->assertEquals($id, $share2->getId()); - // Group shares do not allow updating the recipient - $this->assertSame('group0', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $share2 = $this->provider->getShareById($id); - - $this->assertEquals($id, $share2->getId()); - // Group shares do not allow updating the recipient - $this->assertSame('group0', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - } - - public function testUpdateGroupSubShares(): void { - $id = $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user1', 'user2', - 'file', 42, 'target', 31, null, null); - - $id2 = $this->addShareToDB(2, 'user0', 'user1', 'user2', - 'file', 42, 'mytarget', 31, null, null, $id); - - $id3 = $this->addShareToDB(2, 'user3', 'user1', 'user2', - 'file', 42, 'mytarget2', 0, null, null, $id); - - $users = []; - for ($i = 0; $i < 6; $i++) { - $user = $this->createMock(IUser::class); - $user->method('getUID')->willReturn('user' . $i); - $users['user' . $i] = $user; - } - - $this->userManager->method('get')->willReturnCallback( - function ($userId) use ($users) { - return $users[$userId]; - } - ); - - $groups = []; - for ($i = 0; $i < 2; $i++) { - $group = $this->createMock(IGroup::class); - $group->method('getGID')->willReturn('group' . $i); - $group->method('getDisplayName')->willReturn('group-displayname' . $i); - $groups['group' . $i] = $group; - } - - $this->groupManager->method('get')->willReturnCallback( - function ($groupId) use ($groups) { - return $groups[$groupId]; - } - ); - - $file1 = $this->createMock(File::class); - $file1->method('getId')->willReturn(42); - $file2 = $this->createMock(File::class); - $file2->method('getId')->willReturn(43); - - $folder1 = $this->createMock(Folder::class); - $folder1->method('getFirstNodeById')->with(42)->willReturn($file1); - $folder2 = $this->createMock(Folder::class); - $folder2->method('getFirstNodeById')->with(43)->willReturn($file2); - - $this->rootFolder->method('getUserFolder')->willReturnMap([ - ['user2', $folder1], - ['user5', $folder2], - ]); - - $share = $this->provider->getShareById($id); - - $share->setSharedWith('group0'); - $share->setSharedBy('user4'); - $share->setShareOwner('user5'); - $share->setNode($file2); - $share->setPermissions(1); - - $share2 = $this->provider->update($share); - - $this->assertEquals($id, $share2->getId()); - // Group shares do not allow updating the recipient - $this->assertSame('group0', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $share2 = $this->provider->getShareById($id); - - $this->assertEquals($id, $share2->getId()); - // Group shares do not allow updating the recipient - $this->assertSame('group0', $share2->getSharedWith()); - $this->assertSame('user4', $share2->getSharedBy()); - $this->assertSame('user5', $share2->getShareOwner()); - $this->assertSame(1, $share2->getPermissions()); - - $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($id))) - ->orderBy('id') - ->executeQuery(); - - $shares = $stmt->fetchAllAssociative(); - - $this->assertSame('user0', $shares[0]['share_with']); - $this->assertSame('user4', $shares[0]['uid_initiator']); - $this->assertSame('user5', $shares[0]['uid_owner']); - $this->assertSame(1, (int)$shares[0]['permissions']); - - $this->assertSame('user3', $shares[1]['share_with']); - $this->assertSame('user4', $shares[1]['uid_initiator']); - $this->assertSame('user5', $shares[1]['uid_owner']); - $this->assertSame(0, (int)$shares[1]['permissions']); - - $stmt->closeCursor(); - } - - public function testMoveUserShare(): void { - $id = $this->addShareToDB(IShare::TYPE_USER, 'user0', 'user1', 'user1', 'file', - 42, 'mytaret', 31, null, null); - - $user0 = $this->createMock(IUser::class); - $user0->method('getUID')->willReturn('user0'); - $user0->method('getDisplayName')->willReturn('user0'); - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - $user1->method('getDisplayName')->willReturn('user1'); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $user0], - ['user1', $user1], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(42); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->willReturn($file); - - $share = $this->provider->getShareById($id, null); - - $share->setTarget('/newTarget'); - $this->provider->move($share, $user0); - - $share = $this->provider->getShareById($id, null); - $this->assertSame('/newTarget', $share->getTarget()); - } - - public function testMoveGroupShare(): void { - $id = $this->addShareToDB(IShare::TYPE_GROUP, 'group0', 'user1', 'user1', 'file', - 42, 'mytaret', 31, null, null); - - $user0 = $this->createMock(IUser::class); - $user0->method('getUID')->willReturn('user0'); - $user1 = $this->createMock(IUser::class); - $user1->method('getUID')->willReturn('user1'); - - $group0 = $this->createMock(IGroup::class); - $group0->method('getGID')->willReturn('group0'); - $group0->method('inGroup')->with($user0)->willReturn(true); - $group0->method('getDisplayName')->willReturn('group0-displayname'); - - $this->groupManager->method('get')->with('group0')->willReturn($group0); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $user0], - ['user1', $user1], - ]); - - $folder = $this->createMock(Folder::class); - $folder->method('getId')->willReturn(42); - - $this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf(); - $this->rootFolder->method('getFirstNodeById')->willReturn($folder); - - $share = $this->provider->getShareById($id, 'user0'); - - $share->setTarget('/newTarget'); - $this->provider->move($share, 'user0'); - - $share = $this->provider->getShareById($id, 'user0'); - $this->assertSame('/newTarget', $share->getTarget()); - // The USERGROUP subshare created on first move must be STATUS_ACCEPTED so - // MountProvider does not skip it (default DB value is STATUS_PENDING=0). - $this->assertSame(IShare::STATUS_ACCEPTED, $share->getStatus()); - - $share->setTarget('/ultraNewTarget'); - $this->provider->move($share, 'user0'); - - $share = $this->provider->getShareById($id, 'user0'); - $this->assertSame('/ultraNewTarget', $share->getTarget()); - // Second move hits the UPDATE branch (USERGROUP subshare already exists). - // STATUS_ACCEPTED must be preserved — the UPDATE only touches file_target. - $this->assertSame(IShare::STATUS_ACCEPTED, $share->getStatus()); - } - - public static function dataDeleteUser(): array { - return [ - [IShare::TYPE_USER, 'a', 'b', 'c', 'a', true], - [IShare::TYPE_USER, 'a', 'b', 'c', 'b', false], - [IShare::TYPE_USER, 'a', 'b', 'c', 'c', true], - [IShare::TYPE_USER, 'a', 'b', 'c', 'd', false], - [IShare::TYPE_GROUP, 'a', 'b', 'c', 'a', true], - [IShare::TYPE_GROUP, 'a', 'b', 'c', 'b', false], - // The group c is still valid but user c is deleted so group share stays - [IShare::TYPE_GROUP, 'a', 'b', 'c', 'c', false], - [IShare::TYPE_GROUP, 'a', 'b', 'c', 'd', false], - [IShare::TYPE_LINK, 'a', 'b', 'c', 'a', true], - // To avoid invisible link shares delete initiated link shares as well (see #22327) - [IShare::TYPE_LINK, 'a', 'b', 'c', 'b', true], - [IShare::TYPE_LINK, 'a', 'b', 'c', 'c', false], - [IShare::TYPE_LINK, 'a', 'b', 'c', 'd', false], - ]; - } - - /** - * - * @param int $type The shareType (user/group/link) - * @param string $owner The owner of the share (uid) - * @param string $initiator The initiator of the share (uid) - * @param string $recipient The recipient of the share (uid/gid/pass) - * @param string $deletedUser The user that is deleted - * @param bool $rowDeleted Is the row deleted in this setup - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDeleteUser')] - public function testDeleteUser($type, $owner, $initiator, $recipient, $deletedUser, $rowDeleted): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter($type)) - ->setValue('uid_owner', $qb->createNamedParameter($owner)) - ->setValue('uid_initiator', $qb->createNamedParameter($initiator)) - ->setValue('share_with', $qb->createNamedParameter($recipient)) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->executeStatement(); - - $id = $qb->getLastInsertId(); - - $this->provider->userDeleted($deletedUser, $type); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id)) - ); - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertCount($rowDeleted ? 0 : 1, $data); - } - - public static function dataDeleteUserGroup(): array { - return [ - ['a', 'b', 'c', 'a', true, true], - ['a', 'b', 'c', 'b', false, false], - ['a', 'b', 'c', 'c', false, true], - ['a', 'b', 'c', 'd', false, false], - ]; - } - - /** - * - * @param string $owner The owner of the share (uid) - * @param string $initiator The initiator of the share (uid) - * @param string $recipient The recipient of the usergroup share (uid) - * @param string $deletedUser The user that is deleted - * @param bool $groupShareDeleted - * @param bool $userGroupShareDeleted - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataDeleteUserGroup')] - public function testDeleteUserGroup($owner, $initiator, $recipient, $deletedUser, $groupShareDeleted, $userGroupShareDeleted): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)) - ->setValue('uid_owner', $qb->createNamedParameter($owner)) - ->setValue('uid_initiator', $qb->createNamedParameter($initiator)) - ->setValue('share_with', $qb->createNamedParameter('group')) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->executeStatement(); - $groupId = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(2)) - ->setValue('uid_owner', $qb->createNamedParameter($owner)) - ->setValue('uid_initiator', $qb->createNamedParameter($initiator)) - ->setValue('share_with', $qb->createNamedParameter($recipient)) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->executeStatement(); - $userGroupId = $qb->getLastInsertId(); - - $this->provider->userDeleted($deletedUser, IShare::TYPE_GROUP); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($userGroupId)) - ); - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - $this->assertCount($userGroupShareDeleted ? 0 : 1, $data); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($groupId)) - ); - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - $this->assertCount($groupShareDeleted ? 0 : 1, $data); - } - - public static function dataGroupDeleted(): array { - return [ - [ - [ - 'type' => IShare::TYPE_USER, - 'recipient' => 'user', - 'children' => [] - ], 'group', false - ], - [ - [ - 'type' => IShare::TYPE_USER, - 'recipient' => 'user', - 'children' => [] - ], 'user', false - ], - [ - [ - 'type' => IShare::TYPE_LINK, - 'recipient' => 'user', - 'children' => [] - ], 'group', false - ], - [ - [ - 'type' => IShare::TYPE_GROUP, - 'recipient' => 'group1', - 'children' => [ - 'foo', - 'bar' - ] - ], 'group2', false - ], - [ - [ - 'type' => IShare::TYPE_GROUP, - 'recipient' => 'group1', - 'children' => [ - 'foo', - 'bar' - ] - ], 'group1', true - ], - ]; - } - - /** - * - * @param $shares - * @param $groupToDelete - * @param $shouldBeDeleted - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGroupDeleted')] - public function testGroupDeleted($shares, $groupToDelete, $shouldBeDeleted): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter($shares['type'])) - ->setValue('uid_owner', $qb->createNamedParameter('owner')) - ->setValue('uid_initiator', $qb->createNamedParameter('initiator')) - ->setValue('share_with', $qb->createNamedParameter($shares['recipient'])) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->executeStatement(); - $ids = [$qb->getLastInsertId()]; - - foreach ($shares['children'] as $child) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(2)) - ->setValue('uid_owner', $qb->createNamedParameter('owner')) - ->setValue('uid_initiator', $qb->createNamedParameter('initiator')) - ->setValue('share_with', $qb->createNamedParameter($child)) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->setValue('parent', $qb->createNamedParameter($ids[0])) - ->executeStatement(); - $ids[] = $qb->getLastInsertId(); - } - - $this->provider->groupDeleted($groupToDelete); - - $qb = $this->dbConn->getQueryBuilder(); - $cursor = $qb->select('*') - ->from('share') - ->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) - ->executeQuery(); - $data = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertCount($shouldBeDeleted ? 0 : count($ids), $data); - } - - public static function dataUserDeletedFromGroup(): array { - return [ - ['group1', 'user1', true], - ['group1', 'user2', false], - ['group2', 'user1', false], - ]; - } - - /** - * Given a group share with 'group1' - * And a user specific group share with 'user1'. - * User $user is deleted from group $gid. - * - * - * @param string $group - * @param string $user - * @param bool $toDelete - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataUserDeletedFromGroup')] - public function testUserDeletedFromGroup($group, $user, $toDelete): void { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)) - ->setValue('uid_owner', $qb->createNamedParameter('owner')) - ->setValue('uid_initiator', $qb->createNamedParameter('initiator')) - ->setValue('share_with', $qb->createNamedParameter('group1')) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)); - $qb->executeStatement(); - $id1 = $qb->getLastInsertId(); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share') - ->setValue('share_type', $qb->createNamedParameter(2)) - ->setValue('uid_owner', $qb->createNamedParameter('owner')) - ->setValue('uid_initiator', $qb->createNamedParameter('initiator')) - ->setValue('share_with', $qb->createNamedParameter('user1')) - ->setValue('item_type', $qb->createNamedParameter('file')) - ->setValue('item_source', $qb->createNamedParameter(42)) - ->setValue('file_source', $qb->createNamedParameter(42)) - ->setValue('parent', $qb->createNamedParameter($id1)); - $qb->executeStatement(); - $id2 = $qb->getLastInsertId(); - - $this->provider->userDeletedFromGroup($user, $group); - - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id2))); - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAllAssociative(); - $cursor->closeCursor(); - - $this->assertCount($toDelete ? 0 : 1, $data); - } - - public function testGetSharesInFolder(): void { - $userManager = Server::get(IUserManager::class); - $groupManager = Server::get(IGroupManager::class); - $rootFolder = Server::get(IRootFolder::class); - - $provider = new DefaultShareProvider( - $this->dbConn, - $userManager, - $groupManager, - $rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ); - - $password = md5(time()); - - $u1 = $userManager->createUser('testShare1', $password); - $u2 = $userManager->createUser('testShare2', $password); - $u3 = $userManager->createUser('testShare3', $password); - - $g1 = $groupManager->createGroup('group1'); - - $u1Folder = $rootFolder->getUserFolder($u1->getUID()); - $folder1 = $u1Folder->newFolder('foo'); - $file1 = $folder1->newFile('bar'); - $folder2 = $folder1->newFolder('baz'); - - $shareManager = Server::get(IShareManager::class); - $share1 = $shareManager->newShare(); - $share1->setNode($folder1) - ->setSharedBy($u1->getUID()) - ->setSharedWith($u2->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_ALL); - $share1 = $this->provider->create($share1); - - $share2 = $shareManager->newShare(); - $share2->setNode($file1) - ->setSharedBy($u2->getUID()) - ->setSharedWith($u3->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_READ); - $share2 = $this->provider->create($share2); - - $share3 = $shareManager->newShare(); - $share3->setNode($folder2) - ->setSharedBy($u2->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ); - $share3 = $this->provider->create($share3); - - $share4 = $shareManager->newShare(); - $share4->setNode($folder2) - ->setSharedBy($u1->getUID()) - ->setSharedWith($g1->getGID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_GROUP) - ->setPermissions(Constants::PERMISSION_READ); - $share4 = $this->provider->create($share4); - - $result = $provider->getSharesInFolder($u1->getUID(), $folder1, false); - $this->assertCount(1, $result); - $shares = array_pop($result); - $this->assertCount(1, $shares); - $this->assertSame($folder2->getId(), $shares[0]->getNodeId()); - - $result = $provider->getSharesInFolder($u1->getUID(), $folder1, true); - $this->assertCount(2, $result); - - $file_shares = $result[$file1->getId()]; - $this->assertCount(1, $file_shares); - $this->assertSame($file1->getId(), $file_shares[0]->getNodeId()); - $this->assertSame(IShare::TYPE_USER, $file_shares[0]->getShareType()); - - $folder_shares = $result[$folder2->getId()]; - usort($folder_shares, fn (IShare $a, IShare $b) => $a->getId() <=> $b->getId()); - $this->assertCount(2, $folder_shares); - $this->assertSame($folder2->getId(), $folder_shares[0]->getNodeId()); - $this->assertSame($folder2->getId(), $folder_shares[1]->getNodeId()); - $this->assertSame(IShare::TYPE_LINK, $folder_shares[0]->getShareType()); - $this->assertSame(IShare::TYPE_GROUP, $folder_shares[1]->getShareType()); - - $provider->delete($share1); - $provider->delete($share2); - $provider->delete($share3); - $provider->delete($share4); - - $u1->delete(); - $u2->delete(); - $u3->delete(); - $g1->delete(); - } - - public function testGetAccessListNoCurrentAccessRequired(): void { - $userManager = Server::get(IUserManager::class); - $groupManager = Server::get(IGroupManager::class); - $rootFolder = Server::get(IRootFolder::class); - - $provider = new DefaultShareProvider( - $this->dbConn, - $userManager, - $groupManager, - $rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ); - - $u1 = $userManager->createUser('testShare1', 'test'); - $u2 = $userManager->createUser('testShare2', 'test'); - $u3 = $userManager->createUser('testShare3', 'test'); - $u4 = $userManager->createUser('testShare4', 'test'); - $u5 = $userManager->createUser('testShare5', 'test'); - - $g1 = $groupManager->createGroup('group1'); - $g1->addUser($u3); - $g1->addUser($u4); - - $u1Folder = $rootFolder->getUserFolder($u1->getUID()); - $folder1 = $u1Folder->newFolder('foo'); - $folder2 = $folder1->newFolder('baz'); - $file1 = $folder2->newFile('bar'); - - $result = $provider->getAccessList([$folder1, $folder2, $file1], false); - $this->assertCount(0, $result['users']); - $this->assertFalse($result['public']); - - $shareManager = Server::get(IShareManager::class); - $share1 = $shareManager->newShare(); - $share1->setNode($folder1) - ->setSharedBy($u1->getUID()) - ->setSharedWith($u2->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_ALL); - $share1 = $this->provider->create($share1); - $share1 = $provider->acceptShare($share1, $u2->getUid()); - - $share2 = $shareManager->newShare(); - $share2->setNode($folder2) - ->setSharedBy($u2->getUID()) - ->setSharedWith($g1->getGID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_GROUP) - ->setPermissions(Constants::PERMISSION_ALL); - $share2 = $this->provider->create($share2); - - $shareManager->deleteFromSelf($share2, $u4->getUID()); - - $share2 = $provider->acceptShare($share2, $u3->getUid()); - $share2 = $provider->acceptShare($share2, $u4->getUid()); - - $share3 = $shareManager->newShare(); - $share3->setNode($file1) - ->setSharedBy($u3->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ); - $share3 = $this->provider->create($share3); - - $share4 = $shareManager->newShare(); - $share4->setNode($file1) - ->setSharedBy($u3->getUID()) - ->setSharedWith($u5->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_READ); - $share4 = $this->provider->create($share4); - $share4 = $provider->acceptShare($share4, $u5->getUid()); - - $result = $provider->getAccessList([$folder1, $folder2, $file1], false); - - $this->assertCount(4, $result['users']); - $this->assertContains('testShare2', $result['users']); - $this->assertContains('testShare3', $result['users']); - $this->assertContains('testShare4', $result['users']); - $this->assertContains('testShare5', $result['users']); - $this->assertTrue($result['public']); - - $provider->delete($share1); - $provider->delete($share2); - $provider->delete($share3); - $provider->delete($share4); - - $u1->delete(); - $u2->delete(); - $u3->delete(); - $u4->delete(); - $u5->delete(); - $g1->delete(); - } - - public function testGetAccessListCurrentAccessRequired(): void { - $userManager = Server::get(IUserManager::class); - $groupManager = Server::get(IGroupManager::class); - $rootFolder = Server::get(IRootFolder::class); - - $provider = new DefaultShareProvider( - $this->dbConn, - $userManager, - $groupManager, - $rootFolder, - $this->mailer, - $this->defaults, - $this->l10nFactory, - $this->urlGenerator, - $this->timeFactory, - $this->logger, - $this->shareManager, - $this->config, - ); - - $u1 = $userManager->createUser('testShare1', 'test'); - $u2 = $userManager->createUser('testShare2', 'test'); - $u3 = $userManager->createUser('testShare3', 'test'); - $u4 = $userManager->createUser('testShare4', 'test'); - $u5 = $userManager->createUser('testShare5', 'test'); - - $g1 = $groupManager->createGroup('group1'); - $g1->addUser($u3); - $g1->addUser($u4); - - $u1Folder = $rootFolder->getUserFolder($u1->getUID()); - $folder1 = $u1Folder->newFolder('foo'); - $folder2 = $folder1->newFolder('baz'); - $file1 = $folder2->newFile('bar'); - - $result = $provider->getAccessList([$folder1, $folder2, $file1], false); - $this->assertCount(0, $result['users']); - $this->assertFalse($result['public']); - - $shareManager = Server::get(IShareManager::class); - $share1 = $shareManager->newShare(); - $share1->setNode($folder1) - ->setSharedBy($u1->getUID()) - ->setSharedWith($u2->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_ALL); - $share1 = $this->provider->create($share1); - $share1 = $provider->acceptShare($share1, $u2->getUid()); - - $share2 = $shareManager->newShare(); - $share2->setNode($folder2) - ->setSharedBy($u2->getUID()) - ->setSharedWith($g1->getGID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_GROUP) - ->setPermissions(Constants::PERMISSION_ALL); - $share2 = $this->provider->create($share2); - $share2 = $provider->acceptShare($share2, $u3->getUid()); - $share2 = $provider->acceptShare($share2, $u4->getUid()); - - $shareManager->deleteFromSelf($share2, $u4->getUID()); - - $share3 = $shareManager->newShare(); - $share3->setNode($file1) - ->setSharedBy($u3->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ); - $share3 = $this->provider->create($share3); - - $share4 = $shareManager->newShare(); - $share4->setNode($file1) - ->setSharedBy($u3->getUID()) - ->setSharedWith($u5->getUID()) - ->setShareOwner($u1->getUID()) - ->setShareType(IShare::TYPE_USER) - ->setPermissions(Constants::PERMISSION_READ); - $share4 = $this->provider->create($share4); - $share4 = $provider->acceptShare($share4, $u5->getUid()); - - $result = $provider->getAccessList([$folder1, $folder2, $file1], true); - - $this->assertCount(3, $result['users']); - $this->assertArrayHasKey('testShare2', $result['users']); - $this->assertArrayHasKey('testShare3', $result['users']); - $this->assertArrayHasKey('testShare5', $result['users']); - $this->assertTrue($result['public']); - - $provider->delete($share1); - $provider->delete($share2); - $provider->delete($share3); - $provider->delete($share4); - - $u1->delete(); - $u2->delete(); - $u3->delete(); - $u4->delete(); - $u5->delete(); - $g1->delete(); - } - - public function testGetAllShares(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'share_with' => $qb->expr()->literal('sharedWith1'), - 'uid_owner' => $qb->expr()->literal('shareOwner1'), - 'uid_initiator' => $qb->expr()->literal('sharedBy1'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(42), - 'file_target' => $qb->expr()->literal('myTarget1'), - 'permissions' => $qb->expr()->literal(13), - ]); - $qb->executeStatement(); - - $id1 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'share_with' => $qb->expr()->literal('sharedWith2'), - 'uid_owner' => $qb->expr()->literal('shareOwner2'), - 'uid_initiator' => $qb->expr()->literal('sharedBy2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(43), - 'file_target' => $qb->expr()->literal('myTarget2'), - 'permissions' => $qb->expr()->literal(14), - ]); - $qb->executeStatement(); - - $id2 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'token' => $qb->expr()->literal('token3'), - 'uid_owner' => $qb->expr()->literal('shareOwner3'), - 'uid_initiator' => $qb->expr()->literal('sharedBy3'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(44), - 'file_target' => $qb->expr()->literal('myTarget3'), - 'permissions' => $qb->expr()->literal(15), - ]); - $qb->executeStatement(); - - $id3 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_EMAIL), - 'share_with' => $qb->expr()->literal('shareOwner4'), - 'token' => $qb->expr()->literal('token4'), - 'uid_owner' => $qb->expr()->literal('shareOwner4'), - 'uid_initiator' => $qb->expr()->literal('sharedBy4'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(45), - 'file_target' => $qb->expr()->literal('myTarget4'), - 'permissions' => $qb->expr()->literal(16), - ]); - $qb->executeStatement(); - - $id4 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'token' => $qb->expr()->literal('token5'), - 'uid_owner' => $qb->expr()->literal('shareOwner5'), - 'uid_initiator' => $qb->expr()->literal('sharedBy5'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(46), - 'file_target' => $qb->expr()->literal('myTarget5'), - 'permissions' => $qb->expr()->literal(17), - ]); - $qb->executeStatement(); - - $id5 = $qb->getLastInsertId(); - - $ownerPath1 = $this->createMock(File::class); - $shareOwner1Folder = $this->createMock(Folder::class); - $shareOwner1Folder->method('getFirstNodeById')->willReturn($ownerPath1); - - $ownerPath2 = $this->createMock(File::class); - $shareOwner2Folder = $this->createMock(Folder::class); - $shareOwner2Folder->method('getFirstNodeById')->willReturn($ownerPath2); - - $ownerPath3 = $this->createMock(File::class); - $shareOwner3Folder = $this->createMock(Folder::class); - $shareOwner3Folder->method('getFirstNodeById')->willReturn($ownerPath3); - - $ownerPath4 = $this->createMock(File::class); - $shareOwner4Folder = $this->createMock(Folder::class); - $shareOwner4Folder->method('getFirstNodeById')->willReturn($ownerPath4); - - $ownerPath5 = $this->createMock(File::class); - $shareOwner5Folder = $this->createMock(Folder::class); - $shareOwner5Folder->method('getFirstNodeById')->willReturn($ownerPath5); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap( - [ - ['shareOwner1', $shareOwner1Folder], - ['shareOwner2', $shareOwner2Folder], - ['shareOwner3', $shareOwner3Folder], - ['shareOwner4', $shareOwner4Folder], - ['shareOwner5', $shareOwner5Folder], - ] - ); - - $shares = iterator_to_array($this->provider->getAllShares()); - $this->assertEquals(4, count($shares)); - - $share = $shares[0]; - - // We fetch the node so the root folder is eventually called - - $this->assertEquals($id1, $share->getId()); - $this->assertEquals(IShare::TYPE_USER, $share->getShareType()); - $this->assertEquals('sharedWith1', $share->getSharedWith()); - $this->assertEquals('sharedBy1', $share->getSharedBy()); - $this->assertEquals('shareOwner1', $share->getShareOwner()); - $this->assertEquals($ownerPath1, $share->getNode()); - $this->assertEquals(13, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals('myTarget1', $share->getTarget()); - - $share = $shares[1]; - - $this->assertEquals($id2, $share->getId()); - $this->assertEquals(IShare::TYPE_GROUP, $share->getShareType()); - $this->assertEquals('sharedWith2', $share->getSharedWith()); - $this->assertEquals('sharedBy2', $share->getSharedBy()); - $this->assertEquals('shareOwner2', $share->getShareOwner()); - $this->assertEquals($ownerPath2, $share->getNode()); - $this->assertEquals(14, $share->getPermissions()); - $this->assertEquals(null, $share->getToken()); - $this->assertEquals('myTarget2', $share->getTarget()); - - $share = $shares[2]; - - $this->assertEquals($id3, $share->getId()); - $this->assertEquals(IShare::TYPE_LINK, $share->getShareType()); - $this->assertEquals(null, $share->getSharedWith()); - $this->assertEquals('sharedBy3', $share->getSharedBy()); - $this->assertEquals('shareOwner3', $share->getShareOwner()); - $this->assertEquals($ownerPath3, $share->getNode()); - $this->assertEquals(15, $share->getPermissions()); - $this->assertEquals('token3', $share->getToken()); - $this->assertEquals('myTarget3', $share->getTarget()); - - $share = $shares[3]; - - $this->assertEquals($id5, $share->getId()); - $this->assertEquals(IShare::TYPE_LINK, $share->getShareType()); - $this->assertEquals(null, $share->getSharedWith()); - $this->assertEquals('sharedBy5', $share->getSharedBy()); - $this->assertEquals('shareOwner5', $share->getShareOwner()); - $this->assertEquals($ownerPath5, $share->getNode()); - $this->assertEquals(17, $share->getPermissions()); - $this->assertEquals('token5', $share->getToken()); - $this->assertEquals('myTarget5', $share->getTarget()); - } - - public function testGetSharesByPath(): void { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_USER), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'share_with' => $qb->expr()->literal('user2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - ]); - $qb->executeStatement(); - - $id1 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_GROUP), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'share_with' => $qb->expr()->literal('user2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - ]); - $qb->executeStatement(); - - $id2 = $qb->getLastInsertId(); - - $qb->insert('share') - ->values([ - 'share_type' => $qb->expr()->literal(IShare::TYPE_LINK), - 'uid_owner' => $qb->expr()->literal('user1'), - 'uid_initiator' => $qb->expr()->literal('user1'), - 'share_with' => $qb->expr()->literal('user2'), - 'item_type' => $qb->expr()->literal('file'), - 'file_source' => $qb->expr()->literal(1), - ]); - $qb->executeStatement(); - - $id3 = $qb->getLastInsertId(); - - $ownerPath1 = $this->createMock(File::class); - $shareOwner1Folder = $this->createMock(Folder::class); - $shareOwner1Folder->method('getFirstNodeById')->willReturn($ownerPath1); - - $ownerPath2 = $this->createMock(File::class); - $shareOwner2Folder = $this->createMock(Folder::class); - $shareOwner2Folder->method('getFirstNodeById')->willReturn($ownerPath2); - - $ownerPath3 = $this->createMock(File::class); - $shareOwner3Folder = $this->createMock(Folder::class); - $shareOwner3Folder->method('getFirstNodeById')->willReturn($ownerPath3); - - $this->rootFolder - ->method('getUserFolder') - ->willReturnMap( - [ - ['shareOwner1', $shareOwner1Folder], - ['shareOwner2', $shareOwner2Folder], - ['shareOwner3', $shareOwner3Folder], - ] - ); - - $node = $this->createMock(Node::class); - $node - ->expects($this->once()) - ->method('getId') - ->willReturn(1); - - $shares = $this->provider->getSharesByPath($node); - usort($shares, fn (IShare $a, IShare $b) => $a->getId() <=> $b->getId()); - $this->assertCount(3, $shares); - - $this->assertEquals($id1, $shares[0]->getId()); - $this->assertEquals(IShare::TYPE_USER, $shares[0]->getShareType()); - - $this->assertEquals($id2, $shares[1]->getId()); - $this->assertEquals(IShare::TYPE_GROUP, $shares[1]->getShareType()); - - $this->assertEquals($id3, $shares[2]->getId()); - $this->assertEquals(IShare::TYPE_LINK, $shares[2]->getShareType()); - } -} --- a/tests/lib/Share20/LegacyHooksTest.php +++ /dev/null @@ -1,357 +0,0 @@ -createMock(LoggerInterface::class); - $this->eventDispatcher = new EventDispatcher($symfonyDispatcher, Server::get(ContainerInterface::class), $logger); - $this->hooks = new LegacyHooks($this->eventDispatcher); - $this->manager = Server::get(IShareManager::class); - } - - public function testPreUnshare(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $info = $this->createMock(ICacheEntry::class); - $info->method('getMimeType')->willReturn('text/plain'); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setNodeCacheEntry($info); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['pre'])->getMock(); - Util::connectHook('OCP\Share', 'pre_unshare', $hookListner, 'pre'); - - $hookListnerExpectsPre = [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'awesomeUser', - 'itemparent' => null, - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'fileTarget' => 'myTarget', - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('pre') - ->with($hookListnerExpectsPre); - - $event = new BeforeShareDeletedEvent($share); - $this->eventDispatcher->dispatchTyped($event); - } - - public function testPostUnshare(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $info = $this->createMock(ICacheEntry::class); - $info->method('getMimeType')->willReturn('text/plain'); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setNodeCacheEntry($info); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['post'])->getMock(); - Util::connectHook('OCP\Share', 'post_unshare', $hookListner, 'post'); - - $hookListnerExpectsPost = [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'awesomeUser', - 'itemparent' => null, - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'fileTarget' => 'myTarget', - 'deletedShares' => [ - [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'awesomeUser', - 'itemparent' => null, - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'fileTarget' => 'myTarget', - ], - ], - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('post') - ->with($hookListnerExpectsPost); - - $event = new ShareDeletedEvent($share); - $this->eventDispatcher->dispatchTyped($event); - } - - public function testPostUnshareFromSelf(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $info = $this->createMock(ICacheEntry::class); - $info->method('getMimeType')->willReturn('text/plain'); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setNodeCacheEntry($info); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['postFromSelf'])->getMock(); - Util::connectHook('OCP\Share', 'post_unshareFromSelf', $hookListner, 'postFromSelf'); - - $hookListnerExpectsPostFromSelf = [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'awesomeUser', - 'itemparent' => null, - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'fileTarget' => 'myTarget', - 'itemTarget' => 'myTarget', - 'unsharedItems' => [ - [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'awesomeUser', - 'itemparent' => null, - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'fileTarget' => 'myTarget', - 'itemTarget' => 'myTarget', - ], - ], - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('postFromSelf') - ->with($hookListnerExpectsPostFromSelf); - - $event = new ShareDeletedFromSelfEvent($share); - $this->eventDispatcher->dispatchTyped($event); - } - - public function testPreShare(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $date = new \DateTime(); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setPermissions(Constants::PERMISSION_ALL) - ->setExpirationDate($date) - ->setPassword('password') - ->setToken('token'); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['preShare'])->getMock(); - Util::connectHook('OCP\Share', 'pre_shared', $hookListner, 'preShare'); - - $run = true; - $error = ''; - - $expected = [ - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_LINK, - 'shareWith' => 'awesomeUser', - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'itemTarget' => 'myTarget', - 'permissions' => Constants::PERMISSION_ALL, - 'expiration' => $date, - 'token' => 'token', - 'run' => &$run, - 'error' => &$error, - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('preShare') - ->with($expected); - - $event = new BeforeShareCreatedEvent($share); - $this->eventDispatcher->dispatchTyped($event); - } - - public function testPreShareError(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $date = new \DateTime(); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setPermissions(Constants::PERMISSION_ALL) - ->setExpirationDate($date) - ->setPassword('password') - ->setToken('token'); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['preShare'])->getMock(); - Util::connectHook('OCP\Share', 'pre_shared', $hookListner, 'preShare'); - - $run = true; - $error = ''; - - $expected = [ - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_LINK, - 'shareWith' => 'awesomeUser', - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'itemTarget' => 'myTarget', - 'permissions' => Constants::PERMISSION_ALL, - 'expiration' => $date, - 'token' => 'token', - 'run' => &$run, - 'error' => &$error, - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('preShare') - ->with($expected) - ->willReturnCallback(function ($data): void { - $data['run'] = false; - $data['error'] = 'I error'; - }); - - $event = new BeforeShareCreatedEvent($share); - $this->eventDispatcher->dispatchTyped($event); - - $this->assertTrue($event->isPropagationStopped()); - $this->assertSame('I error', $event->getError()); - } - - public function testPostShare(): void { - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $date = new \DateTime(); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setShareType(IShare::TYPE_LINK) - ->setSharedWith('awesomeUser') - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget') - ->setPermissions(Constants::PERMISSION_ALL) - ->setExpirationDate($date) - ->setPassword('password') - ->setToken('token'); - - $hookListner = $this->getMockBuilder(Dummy::class)->onlyMethods(['postShare'])->getMock(); - Util::connectHook('OCP\Share', 'post_shared', $hookListner, 'postShare'); - - $expected = [ - 'id' => 42, - 'itemType' => 'file', - 'itemSource' => 1, - 'shareType' => IShare::TYPE_LINK, - 'shareWith' => 'awesomeUser', - 'uidOwner' => 'sharedBy', - 'fileSource' => 1, - 'itemTarget' => 'myTarget', - 'fileTarget' => 'myTarget', - 'permissions' => Constants::PERMISSION_ALL, - 'expiration' => $date, - 'token' => 'token', - 'path' => null, - ]; - - $hookListner - ->expects($this->exactly(1)) - ->method('postShare') - ->with($expected); - - $event = new ShareCreatedEvent($share); - $this->eventDispatcher->dispatchTyped($event); - } -} --- a/tests/lib/Share20/ManagerTest.php +++ /dev/null @@ -1,5946 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->config = $this->createMock(IConfig::class); - $this->secureRandom = $this->createMock(ISecureRandom::class); - $this->mountManager = $this->createMock(IMountManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->knownUserService = $this->createMock(KnownUserService::class); - $this->connection = $this->createMock(IDBConnection::class); - - $this->shareDisabledChecker = new ShareDisableChecker($this->config, $this->userManager, $this->groupManager); - $this->dateTimeZone = $this->createMock(IDateTimeZone::class); - $this->timezone = new \DateTimeZone('Pacific/Auckland'); - $this->dateTimeZone->method('getTimeZone')->willReturnCallback(fn () => $this->timezone); - - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->l10nFactory = $this->createMock(IFactory::class); - $this->l = $this->createMock(IL10N::class); - $this->l->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - $this->l->method('n') - ->willReturnCallback(function ($singular, $plural, $count, $parameters = []) { - return vsprintf(str_replace('%n', $count, ($count === 1) ? $singular : $plural), $parameters); - }); - $this->l10nFactory->method('get')->willReturn($this->l); - - $this->factory = new DummyFactory(\OC::$server); - - $this->manager = $this->createManager($this->factory); - - $this->defaultProvider = $this->createMock(DefaultShareProvider::class); - $this->defaultProvider->method('identifier')->willReturn('default'); - $this->factory->setProvider($this->defaultProvider); - - $this->overwriteService(IRootFolder::class, $this->rootFolder); - $this->overwriteService(IUserManager::class, $this->userManager); - $this->overwriteService(IGroupManager::class, $this->groupManager); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [$this->manager]); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'groupManager', [$this->groupManager]); - self::invokePrivate(Server::get(RestrictInteractionListener::class), 'manager', [$this->manager]); - self::invokePrivate(Server::get(RestrictInteractionListener::class), 'rootFolder', [$this->rootFolder]); - } - - #[\Override] - protected function tearDown(): void { - $this->restoreService(IRootFolder::class); - $this->restoreService(IUserManager::class); - $this->restoreService(IGroupManager::class); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'groupManager', [Server::get(IGroupManager::class)]); - self::invokePrivate(Server::get(RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); - self::invokePrivate(Server::get(RestrictInteractionListener::class), 'rootFolder', [Server::get(IRootFolder::class)]); - - parent::tearDown(); - } - - private function createManager(IProviderFactory $factory): Manager { - return new Manager( - $this->logger, - $this->config, - $this->secureRandom, - Server::get(IHasher::class), - $this->mountManager, - $this->groupManager, - $this->l10nFactory, - $factory, - $this->userManager, - $this->rootFolder, - $this->dispatcher, - $this->userSession, - $this->knownUserService, - $this->shareDisabledChecker, - $this->dateTimeZone, - $this->appConfig, - $this->connection, - ); - } - - /** - * @return MockBuilder - */ - private function createManagerMock(): MockBuilder { - return $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->logger, - $this->config, - $this->secureRandom, - Server::get(IHasher::class), - $this->mountManager, - $this->groupManager, - $this->l10nFactory, - $this->factory, - $this->userManager, - $this->rootFolder, - $this->dispatcher, - $this->userSession, - $this->knownUserService, - $this->shareDisabledChecker, - $this->dateTimeZone, - $this->appConfig, - $this->connection, - ]); - } - - private function createFolderMock(string $folderPath): MockObject&Folder { - $folder = $this->createMock(Folder::class); - $folder->method('getPath')->willReturn($folderPath); - $folder->method('getRelativePath')->willReturnCallback( - fn (string $path): ?string => PathHelper::getRelativePath($folderPath, $path) - ); - return $folder; - } - - public function testDeleteNoShareId(): void { - $this->expectException(\InvalidArgumentException::class); - - $share = $this->manager->newShare(); - - $this->manager->deleteShare($share); - } - - public static function dataTestDelete(): array { - return [ - [IShare::TYPE_USER, 'sharedWithUser'], - [IShare::TYPE_GROUP, 'sharedWithGroup'], - [IShare::TYPE_LINK, ''], - [IShare::TYPE_REMOTE, 'foo@bar.com'], - ]; - } - - #[DataProvider('dataTestDelete')] - public function testDelete($shareType, $sharedWith): void { - $manager = $this->createManagerMock() - ->onlyMethods(['getShareById', 'deleteChildren', 'promoteReshares']) - ->getMock(); - - $manager->method('deleteChildren')->willReturn([]); - - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType($shareType) - ->setSharedWith($sharedWith) - ->setSharedBy('sharedBy') - ->setNode($path) - ->setTarget('myTarget'); - - $manager->expects($this->once())->method('deleteChildren')->with($share); - $manager->expects($this->once())->method('promoteReshares')->with($share); - - $this->defaultProvider - ->expects($this->once()) - ->method('delete') - ->with($share); - - $calls = [ - BeforeShareDeletedEvent::class, - ShareDeletedEvent::class, - ]; - $this->dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$calls, $share): void { - $expected = array_shift($calls); - $this->assertInstanceOf($expected, $event); - $this->assertEquals($share, $event->getShare()); - }); - - $manager->deleteShare($share); - } - - public function testDeleteLazyShare(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['getShareById', 'deleteChildren', 'promoteReshares']) - ->getMock(); - - $manager->method('deleteChildren')->willReturn([]); - - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('sharedWith') - ->setSharedBy('sharedBy') - ->setShareOwner('shareOwner') - ->setTarget('myTarget') - ->setNodeId(1) - ->setNodeType('file'); - - $this->rootFolder->expects($this->never())->method($this->anything()); - - $manager->expects($this->once())->method('deleteChildren')->with($share); - $manager->expects($this->once())->method('promoteReshares')->with($share); - - $this->defaultProvider - ->expects($this->once()) - ->method('delete') - ->with($share); - - $calls = [ - BeforeShareDeletedEvent::class, - ShareDeletedEvent::class, - ]; - $this->dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$calls, $share): void { - $expected = array_shift($calls); - $this->assertInstanceOf($expected, $event); - $this->assertEquals($share, $event->getShare()); - }); - - $manager->deleteShare($share); - } - - public function testDeleteNested(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['getShareById', 'promoteReshares']) - ->getMock(); - - $path = $this->createMock(File::class); - $path->method('getId')->willReturn(1); - - $share1 = $this->manager->newShare(); - $share1->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('sharedWith1') - ->setSharedBy('sharedBy1') - ->setNode($path) - ->setTarget('myTarget1'); - - $share2 = $this->manager->newShare(); - $share2->setId(43) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_GROUP) - ->setSharedWith('sharedWith2') - ->setSharedBy('sharedBy2') - ->setNode($path) - ->setTarget('myTarget2') - ->setParent(42); - - $share3 = $this->manager->newShare(); - $share3->setId(44) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('sharedBy3') - ->setNode($path) - ->setTarget('myTarget3') - ->setParent(43); - - $this->defaultProvider - ->method('getChildren') - ->willReturnMap([ - [$share1, [$share2]], - [$share2, [$share3]], - [$share3, []], - ]); - - $deleteCalls = [ - $share3, - $share2, - $share1, - ]; - $this->defaultProvider->expects($this->exactly(3)) - ->method('delete') - ->willReturnCallback(function ($share) use (&$deleteCalls): void { - $expected = array_shift($deleteCalls); - $this->assertEquals($expected, $share); - }); - - $dispatchCalls = [ - [BeforeShareDeletedEvent::class, $share1], - [BeforeShareDeletedEvent::class, $share2], - [BeforeShareDeletedEvent::class, $share3], - [ShareDeletedEvent::class, $share3], - [ShareDeletedEvent::class, $share2], - [ShareDeletedEvent::class, $share1], - ]; - $this->dispatcher->expects($this->exactly(6)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$dispatchCalls): void { - $expected = array_shift($dispatchCalls); - $this->assertInstanceOf($expected[0], $event); - $this->assertEquals($expected[1]->getId(), $event->getShare()->getId()); - }); - - $manager->deleteShare($share1); - } - - public function testDeleteFromSelf(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['getShareById']) - ->getMock(); - - $recipientId = 'unshareFrom'; - $share = $this->manager->newShare(); - $share->setId(42) - ->setProviderId('prov') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('sharedWith') - ->setSharedBy('sharedBy') - ->setShareOwner('shareOwner') - ->setTarget('myTarget') - ->setNodeId(1) - ->setNodeType('file'); - - $this->defaultProvider - ->expects($this->once()) - ->method('deleteFromSelf') - ->with($share, $recipientId); - - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with( - $this->callBack(function (ShareDeletedFromSelfEvent $e) use ($share) { - return $e->getShare() === $share; - }) - ); - - $manager->deleteFromSelf($share, $recipientId); - } - - public function testDeleteChildren(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['deleteShare']) - ->getMock(); - - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_USER); - - $child1 = $this->createMock(IShare::class); - $child1->method('getShareType')->willReturn(IShare::TYPE_USER); - $child2 = $this->createMock(IShare::class); - $child2->method('getShareType')->willReturn(IShare::TYPE_USER); - $child3 = $this->createMock(IShare::class); - $child3->method('getShareType')->willReturn(IShare::TYPE_USER); - - $shares = [ - $child1, - $child2, - $child3, - ]; - - $this->defaultProvider - ->expects($this->exactly(4)) - ->method('getChildren') - ->willReturnCallback(function ($_share) use ($share, $shares) { - if ($_share === $share) { - return $shares; - } - return []; - }); - - $calls = [ - $child1, - $child2, - $child3, - ]; - $this->defaultProvider->expects($this->exactly(3)) - ->method('delete') - ->willReturnCallback(function ($share) use (&$calls): void { - $expected = array_shift($calls); - $this->assertEquals($expected, $share); - }); - - $result = self::invokePrivate($manager, 'deleteChildren', [$share]); - $this->assertSame($shares, $result); - } - - public function testPromoteReshareFile(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalChecks']) - ->getMock(); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(42); - - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_USER); - $share->method('getNodeType')->willReturn('folder'); - $share->method('getSharedWith')->willReturn('userB'); - $share->method('getNode')->willReturn($file); - - $reShare = $this->createMock(IShare::class); - $reShare->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShare->method('getSharedBy')->willReturn('userB'); - $reShare->method('getSharedWith')->willReturn('userC'); - $reShare->method('getNode')->willReturn($file); - - $this->defaultProvider->method('getSharesBy') - ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare, $file) { - $this->assertEquals($file, $node); - if ($shareType === IShare::TYPE_USER) { - return match ($userId) { - 'userB' => [$reShare], - }; - } else { - return []; - } - }); - $manager->method('generalChecks')->willThrowException(new GenericShareException()); - - $manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare); - - $this->userManager->method('userExists')->willReturn(true); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder); - $userFolder->method('getFirstNodeById') - ->with(42) - ->willReturn($file); - - $qb = $this->createMock(IQueryBuilder::class); - $result = $this->createMock(IResult::class); - $qb->method('select') - ->willReturn($qb); - $qb->method('from') - ->willReturn($qb); - $qb->method('andWhere') - ->willReturn($qb); - $qb->method('expr') - ->willReturn($this->createMock(IExpressionBuilder::class)); - $qb->method('executeQuery') - ->willReturn($result); - $this->connection->method('getQueryBuilder') - ->willReturn($qb); - $result->method('fetchAssociative') - ->willReturnOnConsecutiveCalls( - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42], - false, - ); - - self::invokePrivate($manager, 'promoteReshares', [$share]); - } - - public function testPromoteReshare(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalChecks']) - ->getMock(); - - $folder = $this->createFolderMock('/path/to/folder'); - - $subFolder = $this->createFolderMock('/path/to/folder/sub'); - - $otherFolder = $this->createFolderMock('/path/to/otherfolder/'); - - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_USER); - $share->method('getNodeType')->willReturn('folder'); - $share->method('getSharedWith')->willReturn('userB'); - $share->method('getNode')->willReturn($folder); - - $reShare = $this->createMock(IShare::class); - $reShare->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShare->method('getSharedBy')->willReturn('userB'); - $reShare->method('getSharedWith')->willReturn('userC'); - $reShare->method('getNode')->willReturn($folder); - - $reShareInSubFolder = $this->createMock(IShare::class); - $reShareInSubFolder->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShareInSubFolder->method('getSharedBy')->willReturn('userB'); - $reShareInSubFolder->method('getNode')->willReturn($subFolder); - - $reShareInOtherFolder = $this->createMock(IShare::class); - $reShareInOtherFolder->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShareInOtherFolder->method('getSharedBy')->willReturn('userB'); - $reShareInOtherFolder->method('getNode')->willReturn($otherFolder); - - $this->defaultProvider->method('getSharesBy') - ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($folder, $subFolder, $reShare, $reShareInSubFolder, $reShareInOtherFolder) { - if ($shareType === IShare::TYPE_USER) { - if ($userId === 'userB') { - if ($node === $folder) { - return [$reShare]; - } - if ($node === $subFolder) { - return [$reShareInSubFolder]; - } - } - } - $this->fail(); - }); - $manager->method('generalChecks')->willThrowException(new GenericShareException()); - - $calls = [ - $reShare, - $reShareInSubFolder, - ]; - $manager->expects($this->exactly(2)) - ->method('updateShare') - ->willReturnCallback(function ($share) use (&$calls): IShare { - $expected = array_shift($calls); - $this->assertEquals($expected, $share); - return $expected; - }); - - $this->userManager->method('userExists')->willReturn(true); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder); - $userFolder->method('getFirstNodeById') - ->willReturnCallback(function ($id) use ($subFolder, $otherFolder, $folder) { - return match ($id) { - 41 => $subFolder, - 42 => $otherFolder, - 43 => $folder, - }; - }); - - $qb = $this->createMock(IQueryBuilder::class); - $result = $this->createMock(IResult::class); - $qb->method('select') - ->willReturn($qb); - $qb->method('from') - ->willReturn($qb); - $qb->method('andWhere') - ->willReturn($qb); - $qb->method('expr') - ->willReturn($this->createMock(IExpressionBuilder::class)); - $qb->method('executeQuery') - ->willReturn($result); - $this->connection->method('getQueryBuilder') - ->willReturn($qb); - $result->method('fetchAssociative') - ->willReturnOnConsecutiveCalls( - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 41], - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42], - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 43], - false, - ); - - self::invokePrivate($manager, 'promoteReshares', [$share]); - } - - public function testPromoteReshareWhenUserHasAnotherShare(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalChecks']) - ->getMock(); - - $folder = $this->createFolderMock('/path/to/folder'); - - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_USER); - $share->method('getNodeType')->willReturn('folder'); - $share->method('getSharedWith')->willReturn('userB'); - $share->method('getNode')->willReturn($folder); - - $reShare = $this->createMock(IShare::class); - $reShare->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShare->method('getNodeType')->willReturn('folder'); - $reShare->method('getSharedBy')->willReturn('userB'); - $reShare->method('getNode')->willReturn($folder); - - $this->defaultProvider->method('getSharesBy')->willReturn([$reShare]); - $manager->method('generalChecks'); - - /* No share is promoted because generalChecks does not throw */ - $manager->expects($this->never())->method('updateShare'); - - $this->userManager->method('userExists')->willReturn(true); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder); - $userFolder->method('getFirstNodeById') - ->with(42) - ->willReturn($folder); - - $qb = $this->createMock(IQueryBuilder::class); - $result = $this->createMock(IResult::class); - $qb->method('select') - ->willReturn($qb); - $qb->method('from') - ->willReturn($qb); - $qb->method('andWhere') - ->willReturn($qb); - $qb->method('expr') - ->willReturn($this->createMock(IExpressionBuilder::class)); - $qb->method('executeQuery') - ->willReturn($result); - $this->connection->method('getQueryBuilder') - ->willReturn($qb); - $result->method('fetchAssociative') - ->willReturnOnConsecutiveCalls( - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42], - false, - ); - - self::invokePrivate($manager, 'promoteReshares', [$share]); - } - - public function testPromoteReshareOfUsersInGroupShare(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalChecks']) - ->getMock(); - - $folder = $this->createFolderMock('/path/to/folder'); - - $userA = $this->createMock(IUser::class); - $userA->method('getUID')->willReturn('userA'); - - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_GROUP); - $share->method('getNodeType')->willReturn('folder'); - $share->method('getSharedWith')->willReturn('Group'); - $share->method('getNode')->willReturn($folder); - $share->method('getShareOwner')->willReturn($userA); - - $reShare1 = $this->createMock(IShare::class); - $reShare1->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShare1->method('getNodeType')->willReturn('folder'); - $reShare1->method('getSharedBy')->willReturn('userB'); - $reShare1->method('getNode')->willReturn($folder); - - $reShare2 = $this->createMock(IShare::class); - $reShare2->method('getShareType')->willReturn(IShare::TYPE_USER); - $reShare2->method('getNodeType')->willReturn('folder'); - $reShare2->method('getSharedBy')->willReturn('userC'); - $reShare2->method('getNode')->willReturn($folder); - - $userB = $this->createMock(IUser::class); - $userB->method('getUID')->willReturn('userB'); - $userC = $this->createMock(IUser::class); - $userC->method('getUID')->willReturn('userC'); - $group = $this->createMock(IGroup::class); - $group->method('getUsers')->willReturn([$userB, $userC]); - $this->groupManager->method('get')->with('Group')->willReturn($group); - - $this->defaultProvider->method('getSharesBy') - ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare1, $reShare2) { - if ($shareType === IShare::TYPE_USER) { - return match ($userId) { - 'userB' => [$reShare1], - 'userC' => [$reShare2], - }; - } else { - return []; - } - }); - $manager->method('generalChecks')->willThrowException(new GenericShareException()); - - $manager->method('getSharedWith')->willReturn([]); - - $this->userManager->method('userExists')->willReturn(true); - $userFolder = $this->createMock(Folder::class); - $this->rootFolder->method('getUserFolder')->with('userA')->willReturn($userFolder); - $userFolder->method('getFirstNodeById') - ->with(42) - ->willReturn($folder); - - $calls = [ - $reShare1, - $reShare2, - ]; - $manager->expects($this->exactly(2)) - ->method('updateShare') - ->willReturnCallback(function ($share) use (&$calls): IShare { - $expected = array_shift($calls); - $this->assertEquals($expected, $share); - return $expected; - }); - - $qb = $this->createMock(IQueryBuilder::class); - $result = $this->createMock(IResult::class); - $qb->method('select') - ->willReturn($qb); - $qb->method('from') - ->willReturn($qb); - $qb->method('andWhere') - ->willReturn($qb); - $qb->method('expr') - ->willReturn($this->createMock(IExpressionBuilder::class)); - $qb->method('executeQuery') - ->willReturn($result); - $this->connection->method('getQueryBuilder') - ->willReturn($qb); - $result->method('fetchAssociative') - ->willReturnOnConsecutiveCalls( - ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42], - ['uid_initiator' => 'userC', 'share_type' => IShare::TYPE_USER, 'uid_owner' => 'userA', 'file_source' => 42], - false, - ); - - self::invokePrivate($manager, 'promoteReshares', [$share]); - } - - public function testGetShareById(): void { - $share = $this->createMock(IShare::class); - - $this->defaultProvider - ->expects($this->once()) - ->method('getShareById') - ->with(42) - ->willReturn($share); - - $this->assertEquals($share, $this->manager->getShareById('default:42')); - } - - public function testGetShareByIdOfDisabledInitiatorAsShareOwner(): void { - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setShareOwner('owner') - ->setSharedBy('initiator') - ->setSharedWith('recipient'); - - $this->userManager->method('get')->willReturnMap([ - ['owner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ]); - - $this->defaultProvider - ->expects($this->once()) - ->method('getShareById') - ->with('42', 'owner') - ->willReturn($share); - - $this->assertSame($share, $this->manager->getShareById('default:42', 'owner')); - } - - public function testGetShareByIdOfDisabledInitiatorAsOtherUser(): void { - $this->expectException(ShareNotFound::class); - $this->expectExceptionMessage('The requested share does not exist anymore'); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setShareOwner('owner') - ->setSharedBy('initiator') - ->setSharedWith('recipient'); - - $this->userManager->method('get')->willReturnMap([ - ['owner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ]); - - $this->defaultProvider - ->expects($this->once()) - ->method('getShareById') - ->with('42', 'recipient') - ->willReturn($share); - - $this->manager->getShareById('default:42', 'recipient'); - } - - public function testGetExpiredShareById(): void { - $this->expectException(ShareNotFound::class); - - $manager = $this->createManagerMock() - ->onlyMethods(['deleteShare']) - ->getMock(); - - $date = new \DateTime(); - $date->setTime(0, 0, 0); - - $share = $this->manager->newShare(); - $share->setExpirationDate($date) - ->setShareType(IShare::TYPE_LINK); - - $this->defaultProvider->expects($this->once()) - ->method('getShareById') - ->with('42') - ->willReturn($share); - - $manager->expects($this->once()) - ->method('deleteShare') - ->with($share); - - $manager->getShareById('default:42'); - } - - public function testVerifyPasswordNullButEnforced(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Passwords are enforced for link and mail shares'); - - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', ''], - ]); - - $this->appConfig->method('getValueBool')->willReturnMap([ - ['core', 'shareapi_enforce_links_password', true], - ]); - - self::invokePrivate($this->manager, 'verifyPassword', [null]); - } - - public function testVerifyPasswordNotEnforcedGroup(): void { - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', '["admin"]'], - ['core', 'shareapi_enforce_links_password', 'no', 'yes'], - ]); - - // Create admin user - $user = $this->createMock(IUser::class); - $this->userSession->method('getUser')->willReturn($user); - $this->groupManager->method('getUserGroupIds')->with($user)->willReturn(['admin']); - - $result = self::invokePrivate($this->manager, 'verifyPassword', [null]); - $this->assertNull($result); - } - - public function testVerifyPasswordNotEnforcedMultipleGroups(): void { - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', '["admin", "special"]'], - ['core', 'shareapi_enforce_links_password', 'no', 'yes'], - ]); - - // Create admin user - $user = $this->createMock(IUser::class); - $this->userSession->method('getUser')->willReturn($user); - $this->groupManager->method('getUserGroupIds')->with($user)->willReturn(['special']); - - $result = self::invokePrivate($this->manager, 'verifyPassword', [null]); - $this->assertNull($result); - } - - public function testVerifyPasswordNull(): void { - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', ''], - ['core', 'shareapi_enforce_links_password', 'no', 'no'], - ]); - - $result = self::invokePrivate($this->manager, 'verifyPassword', [null]); - $this->assertNull($result); - } - - public function testVerifyPasswordHook(): void { - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', ''], - ['core', 'shareapi_enforce_links_password', 'no', 'no'], - ]); - - $this->dispatcher->expects($this->once())->method('dispatchTyped') - ->willReturnCallback(function (Event $event): void { - $this->assertInstanceOf(ValidatePasswordPolicyEvent::class, $event); - /** @var ValidatePasswordPolicyEvent $event */ - $this->assertSame('password', $event->getPassword()); - } - ); - - $result = self::invokePrivate($this->manager, 'verifyPassword', ['password']); - $this->assertNull($result); - } - - public function testVerifyPasswordHookFails(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('password not accepted'); - - $this->config->method('getAppValue')->willReturnMap([ - ['core', 'shareapi_enforce_links_password_excluded_groups', '', ''], - ['core', 'shareapi_enforce_links_password', 'no', 'no'], - ]); - - $this->dispatcher->expects($this->once())->method('dispatchTyped') - ->willReturnCallback(function (Event $event): void { - $this->assertInstanceOf(ValidatePasswordPolicyEvent::class, $event); - /** @var ValidatePasswordPolicyEvent $event */ - $this->assertSame('password', $event->getPassword()); - throw new HintException('password not accepted'); - } - ); - - self::invokePrivate($this->manager, 'verifyPassword', ['password']); - } - - public function createShare($id, int $type, ?Node $node, $sharedWith, $sharedBy, $shareOwner, - $permissions, $expireDate = null, $password = null, $attributes = null): IShare&MockObject { - $share = $this->createMock(IShare::class); - - $share->method('getShareType')->willReturn($type); - $share->method('getSharedWith')->willReturn($sharedWith); - $share->method('getSharedBy')->willReturn($sharedBy); - $share->method('getShareOwner')->willReturn($shareOwner); - if ($node) { - $share->method('getNode')->willReturn($node); - if ($node->getId()) { - $share->method('getNodeId')->willReturn($node->getId()); - } - } else { - $share->method('getNode')->willReturnCallback(function (): never { - throw new NotFoundException(); - }); - } - $share->method('getPermissions')->willReturn($permissions); - $share->method('getAttributes')->willReturn($attributes); - $share->method('getExpirationDate')->willReturn($expireDate); - $share->method('getPassword')->willReturn($password); - $share->method('isPasswordProtected')->willReturn(!empty($password)); - - return $share; - } - - private function createEnabledUser(bool $enabled): IUser&MockObject { - $user = $this->createMock(IUser::class); - $user->method('isEnabled')->willReturn($enabled); - - return $user; - } - - public static function dataGeneralChecks(): array { - $user0 = 'user0'; - $user2 = 'user1'; - $group0 = 'group0'; - - $file = [ - File::class, - [ - 'getId' => 108, - ], - 'default', - ]; - - $node = [ - Node::class, - [ - 'getId' => 108, - ], - 'default', - ]; - - $data = [ - [[null, IShare::TYPE_USER, $file, null, $user0, $user0, 31, null, null], 'Share recipient is not a valid user', true], - [[null, IShare::TYPE_USER, $file, $group0, $user0, $user0, 31, null, null], 'Share recipient is not a valid user', true], - [[null, IShare::TYPE_USER, $file, 'foo@bar.com', $user0, $user0, 31, null, null], 'Share recipient is not a valid user', true], - [[null, IShare::TYPE_GROUP, $file, null, $user0, $user0, 31, null, null], 'Share recipient is not a valid group', true], - [[null, IShare::TYPE_GROUP, $file, $user2, $user0, $user0, 31, null, null], 'Share recipient is not a valid group', true], - [[null, IShare::TYPE_GROUP, $file, 'foo@bar.com', $user0, $user0, 31, null, null], 'Share recipient is not a valid group', true], - [[null, IShare::TYPE_LINK, $file, $user2, $user0, $user0, 31, null, null], 'Share recipient should be empty', true], - [[null, IShare::TYPE_LINK, $file, $group0, $user0, $user0, 31, null, null], 'Share recipient should be empty', true], - [[null, IShare::TYPE_LINK, $file, 'foo@bar.com', $user0, $user0, 31, null, null], 'Share recipient should be empty', true], - [[null, -1, $file, null, $user0, $user0, 31, null, null], 'Unknown share type', true], - - [[null, IShare::TYPE_USER, $file, $user2, null, $user0, 31, null, null], 'Share initiator must be set', true], - [[null, IShare::TYPE_GROUP, $file, $group0, null, $user0, 31, null, null], 'Share initiator must be set', true], - [[null, IShare::TYPE_LINK, $file, null, null, $user0, 31, null, null], 'Share initiator must be set', true], - - [[null, IShare::TYPE_USER, $file, $user0, $user0, $user0, 31, null, null], 'Cannot share with yourself', true], - - [[null, IShare::TYPE_USER, null, $user2, $user0, $user0, 31, null, null], 'Shared path must be set', true], - [[null, IShare::TYPE_GROUP, null, $group0, $user0, $user0, 31, null, null], 'Shared path must be set', true], - [[null, IShare::TYPE_LINK, null, null, $user0, $user0, 31, null, null], 'Shared path must be set', true], - - [[null, IShare::TYPE_USER, $node, $user2, $user0, $user0, 31, null, null], 'Shared path must be either a file or a folder', true], - [[null, IShare::TYPE_GROUP, $node, $group0, $user0, $user0, 31, null, null], 'Shared path must be either a file or a folder', true], - [[null, IShare::TYPE_LINK, $node, null, $user0, $user0, 31, null, null], 'Shared path must be either a file or a folder', true], - ]; - - $nonShareAble = [ - Folder::class, - [ - 'getId' => 108, - 'isShareable' => false, - 'getPermissions' => Constants::PERMISSION_READ, - 'getPath' => 'path', - 'getName' => 'name', - 'getOwner' => $user0, - ], - 'default', - ]; - - $data[] = [[null, IShare::TYPE_USER, $nonShareAble, $user2, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; - $data[] = [[null, IShare::TYPE_GROUP, $nonShareAble, $group0, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; - $data[] = [[null, IShare::TYPE_LINK, $nonShareAble, null, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; - - $limitedPermissions = [ - File::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_READ | Constants::PERMISSION_SHARE, - 'getId' => 108, - 'getPath' => 'path', - 'getName' => 'name', - 'getOwner' => $user0, - ], - 'default', - ]; - - $data[] = [[null, IShare::TYPE_USER, $limitedPermissions, $user2, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_GROUP, $limitedPermissions, $group0, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_LINK, $limitedPermissions, null, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - - $limitedPermissions[1]['getMountPoint'] = IMovableMount::class; - - // increase permissions of a re-share - $data[] = [[null, IShare::TYPE_GROUP, $limitedPermissions, $group0, $user0, $user0, 19, null, null], 'You cannot share "path" with more permission than you have yourself.', true]; - $data[] = [[null, IShare::TYPE_USER, $limitedPermissions, $user2, $user0, $user0, 3, null, null], 'You cannot share "path" with more permission than you have yourself.', true]; - - $nonMoveableMountPermissions = [ - Folder::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_READ, - 'getId' => 108, - 'getPath' => 'path', - 'getName' => 'name', - 'getInternalPath' => '', - 'getOwner' => $user0, - ], - 'allPermissions', - ]; - - $data[] = [[null, IShare::TYPE_USER, $nonMoveableMountPermissions, $user2, $user0, $user0, 11, null, null], 'You cannot share "path" with more permission than you have yourself.', false]; - $data[] = [[null, IShare::TYPE_GROUP, $nonMoveableMountPermissions, $group0, $user0, $user0, 11, null, null], 'You cannot share "path" with more permission than you have yourself.', false]; - - $rootFolder = [ - Folder::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_ALL, - 'getId' => 42, - ], - 'none', - ]; - - $data[] = [[null, IShare::TYPE_USER, $rootFolder, $user2, $user0, $user0, 30, null, null], 'You cannot share your home folder.', true]; - $data[] = [[null, IShare::TYPE_GROUP, $rootFolder, $group0, $user0, $user0, 2, null, null], 'You cannot share your home folder.', true]; - $data[] = [[null, IShare::TYPE_LINK, $rootFolder, null, $user0, $user0, 16, null, null], 'You cannot share your home folder.', true]; - - $allPermissionsFiles = [ - File::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_ALL, - 'getId' => 108, - 'getOwner' => $user0, - ], - 'default', - ]; - - // test invalid CREATE or DELETE permissions - $data[] = [[null, IShare::TYPE_USER, $allPermissionsFiles, $user2, $user0, $user0, Constants::PERMISSION_ALL, null, null], 'File cannot be shared with delete permission.', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermissionsFiles, $group0, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_CREATE, null, null], 'File cannot be shared with create permission.', true]; - $data[] = [[null, IShare::TYPE_LINK, $allPermissionsFiles, null, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_DELETE, null, null], 'File cannot be shared with delete permission.', true]; - - $allPermissions = [ - Folder::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_ALL, - 'getId' => 108, - 'getOwner' => $user0, - ], - 'default', - ]; - - $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 30, null, null], 'File share needs at least read permission.', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 2, null, null], 'File share needs at least read permission.', true]; - - // test invalid permissions - $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 32, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 63, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_LINK, $allPermissions, null, $user0, $user0, -1, null, null], 'Valid permissions are required for sharing', true]; - - // working shares - $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 31, null, null], null, false]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 3, null, null], null, false]; - $data[] = [[null, IShare::TYPE_LINK, $allPermissions, null, $user0, $user0, 17, null, null], null, false]; - - $remoteFile = [ - Folder::class, - [ - 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE, - 'getId' => 108, - 'getOwner' => $user0, - ], - 'remote', - ]; - - $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 1, null, null], null, false]; - $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 3, null, null], null, false]; - $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 31, null, null], 'You cannot share "" with more permission than you have yourself.', true]; - - return $data; - } - - private function createNodeMock(string $class, array $methods, string $storageType): MockObject { - $mock = $this->createMock($class); - foreach ($methods as $methodName => $return) { - if ($methodName === 'getOwner') { - $uid = $return; - $return = $this->createMock(IUser::class); - $return->method('getUID') - ->willReturn($uid); - } elseif ($methodName === 'getMountPoint') { - $return = $this->createMock($return); - } - $mock->method($methodName)->willReturn($return); - } - switch ($storageType) { - case 'default': - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->with('\OCA\Files_Sharing\External\Storage') - ->willReturn(false); - break; - case 'allPermissions': - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->with('\OCA\Files_Sharing\External\Storage') - ->willReturn(false); - $storage->method('getPermissions')->willReturn(Constants::PERMISSION_ALL); - break; - case 'none': - $storage = false; - break; - case 'remote': - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->with('\OCA\Files_Sharing\External\Storage') - ->willReturn(true); - break; - default: - throw new \Exception('Unknown storage type ' . $storageType); - } - if ($storage === false) { - $mock->expects(self::never())->method('getStorage'); - } else { - $mock->method('getStorage') - ->willReturn($storage); - } - - return $mock; - } - - #[DataProvider('dataGeneralChecks')] - public function testGeneralChecks(array $shareParams, ?string $exceptionMessage, bool $exception): void { - if ($shareParams[2] !== null) { - $shareParams[2] = $this->createNodeMock(...$shareParams[2]); - } - $share = $this->createShare(...$shareParams); - - $thrown = null; - - $this->userManager->method('userExists')->willReturnCallBack(fn (string $userId) => $userId === 'user0' || $userId === 'user1'); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $this->createMock(IUser::class)], - ['user1', $this->createMock(IUser::class)], - ]); - - $this->groupManager->method('groupExists')->willReturnMap([ - ['group0', true], - ]); - - $userFolder = $this->createMock(Folder::class); - - $userFolder - ->method('getId') - ->willReturn(42); - - $userFolder - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - - // Id 108 is used in the data to refer to the node of the share. - try { - $node = $share->getNode(); - } catch (NotFoundException $e) { - $node = null; - } - - $userFolder - ->method('getById') - ->willReturnMap([ - [108, $node !== null ? [$node] : []], - [42, [$userFolder]], - ]); - - $userFolder - ->method('getFirstNodeById') - ->willReturnMap([ - [108, $node], - [42, $userFolder], - ]); - - $userFolder - ->method('getRelativePath') - ->willReturnArgument(0); - - $this->rootFolder - ->method('getUserFolder') - ->willReturn($userFolder); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ['files_sharing', 'outgoing_server2server_share_enabled', 'yes', 'yes'], - ]); - - try { - self::invokePrivate($this->manager, 'generalChecks', [$share]); - $thrown = false; - } catch (GenericShareException $e) { - $this->assertEquals($exceptionMessage, $e->getHint()); - $thrown = true; - } catch (\InvalidArgumentException $e) { - $this->assertEquals($exceptionMessage, $e->getMessage()); - $thrown = true; - } - - $this->assertSame($exception, $thrown); - } - - public function testGeneralCheckShareRoot(): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('You cannot share your home folder.'); - - $this->userManager->method('userExists')->willReturnMap([ - ['user0', true], - ['user1', true], - ]); - - $this->userManager->method('get')->willReturnMap([ - ['user0', $this->createMock(IUser::class)], - ['user1', $this->createMock(IUser::class)], - ]); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('isSubNode') - ->with($userFolder) - ->willReturn(false); - $userFolder - ->method('getId') - ->willReturn(42); - $userFolder - ->method('getById') - ->with(42) - ->willReturn([$userFolder]); - $userFolder - ->method('getFirstNodeById') - ->with(42) - ->willReturn($userFolder); - $userFolder - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $userFolder - ->method('isShareable') - ->willReturn(true); - - $this->rootFolder - ->method('getUserFolder') - ->willReturn($userFolder); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('user0') - ->setSharedBy('user1') - ->setNode($userFolder) - ->setPermissions(Constants::PERMISSION_ALL); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public static function validateExpirationDateInternalProvider() { - return [[IShare::TYPE_USER], [IShare::TYPE_REMOTE], [IShare::TYPE_REMOTE_GROUP]]; - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalInPast($shareType): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Expiration date is in the past'); - - // Expire date in the past - $past = new \DateTime(); - $past->sub(new \DateInterval('P1D')); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($past); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceButNotSet($shareType): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expiration date is enforced'); - - $share = $this->manager->newShare(); - $share->setProviderId('foo')->setId('bar'); - $share->setShareType($shareType); - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ]); - } - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceButNotEnabledAndNotSet($shareType): void { - $share = $this->manager->newShare(); - $share->setProviderId('foo')->setId('bar'); - $share->setShareType($shareType); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ]); - } - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertNull($share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceButNotSetNewShare($shareType): void { - $share = $this->manager->newShare(); - $share->setShareType($shareType); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'internal_defaultExpDays', '3', '3'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'remote_defaultExpDays', '3', '3'], - ]); - } - - $expected = new \DateTime('now', $this->timezone); - $expected->setTime(23, 59, 59); - $expected->add(new \DateInterval('P3D')); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertNotNull($share->getExpirationDate()); - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceRelaxedDefaultButNotSetNewShare($shareType): void { - $share = $this->manager->newShare(); - $share->setShareType($shareType); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'internal_defaultExpDays', '3', '1'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'remote_defaultExpDays', '3', '1'], - ]); - } - - $expected = new \DateTime('now', $this->timezone); - $expected->setTime(23, 59, 59); - $expected->add(new \DateInterval('P1D')); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertNotNull($share->getExpirationDate()); - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceTooFarIntoFuture($shareType): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Cannot set expiration date more than 3 days in the future'); - - $future = new \DateTime(); - $future->add(new \DateInterval('P7D')); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($future); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ]); - } - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalEnforceValid($shareType): void { - $future = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $future->add(new \DateInterval('P2D')); - $future->setTime(1, 2, 3); - - $expected = clone $future; - $expected->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($future); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ]); - } - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($future) { - return $data['expirationDate'] == $future; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalNoDefault($shareType): void { - $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $date->add(new \DateInterval('P5D')); - $date->setTime(1, 2, 3); - - $expected = clone $date; - $expected->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($date); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected && $data['passwordSet'] === false; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalNoDateNoDefault($shareType): void { - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) { - return $data['expirationDate'] === null && $data['passwordSet'] === true; - })); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setPassword('password'); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertNull($share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalNoDateDefault($shareType): void { - $share = $this->manager->newShare(); - $share->setShareType($shareType); - - $expected = new \DateTime('now', $this->timezone); - $expected->setTime(23, 59, 59); - $expected->add(new \DateInterval('P3D')); - $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'internal_defaultExpDays', '3', '3'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'remote_defaultExpDays', '3', '3'], - ]); - } - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalDefault($shareType): void { - $future = new \DateTime('now', $this->timezone); - $future->add(new \DateInterval('P5D')); - $future->setTime(1, 2, 3); - - $expected = clone $future; - $expected->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($future); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '3'], - ['core', 'internal_defaultExpDays', '3', '1'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '3'], - ['core', 'remote_defaultExpDays', '3', '1'], - ]); - } - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalHookModification($shareType): void { - $nextWeek = new \DateTime('now', $this->timezone); - $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(23, 59, 59); - - $save = clone $nextWeek; - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->willReturnCallback(function ($data): void { - $data['expirationDate']->sub(new \DateInterval('P2D')); - }); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($nextWeek); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $save->sub(new \DateInterval('P2D')); - $this->assertEquals($save, $share->getExpirationDate()); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalHookException($shareType): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Invalid date!'); - - $nextWeek = new \DateTime(); - $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setExpirationDate($nextWeek); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->willReturnCallback(function ($data): void { - $data['accepted'] = false; - $data['message'] = 'Invalid date!'; - }); - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - } - - #[DataProvider('validateExpirationDateInternalProvider')] - public function testValidateExpirationDateInternalExistingShareNoDefault($shareType): void { - $share = $this->manager->newShare(); - $share->setShareType($shareType); - $share->setId('42')->setProviderId('foo'); - - if ($shareType === IShare::TYPE_USER) { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_internal_expire_date', 'no', 'yes'], - ['core', 'shareapi_internal_expire_after_n_days', '7', '6'], - ]); - } else { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_default_remote_expire_date', 'no', 'yes'], - ['core', 'shareapi_remote_expire_after_n_days', '7', '6'], - ]); - } - - self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); - - $this->assertEquals(null, $share->getExpirationDate()); - } - - public function testValidateExpirationDateInPast(): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Expiration date is in the past'); - - // Expire date in the past - $past = new \DateTime(); - $past->sub(new \DateInterval('P1D')); - - $share = $this->manager->newShare(); - $share->setExpirationDate($past); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - } - - public function testValidateExpirationDateEnforceButNotSet(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expiration date is enforced'); - - $share = $this->manager->newShare(); - $share->setProviderId('foo')->setId('bar'); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', true], - ]); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - } - - public function testValidateExpirationDateEnforceButNotEnabledAndNotSet(): void { - $share = $this->manager->newShare(); - $share->setProviderId('foo')->setId('bar'); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enforce_expire_date', 'no', 'yes'], - ]); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertNull($share->getExpirationDate()); - } - - public function testValidateExpirationDateEnforceButNotSetNewShare(): void { - $share = $this->manager->newShare(); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ['core', 'link_defaultExpDays', '3', '3'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', true], - ]); - - $expected = new \DateTime('now', $this->timezone); - $expected->setTime(23, 59, 59); - $expected->add(new \DateInterval('P3D')); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertNotNull($share->getExpirationDate()); - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateEnforceRelaxedDefaultButNotSetNewShare(): void { - $share = $this->manager->newShare(); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ['core', 'link_defaultExpDays', '3', '1'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', true], - ]); - - $expected = new \DateTime('now', $this->timezone); - $expected->setTime(23, 59, 59); - $expected->add(new \DateInterval('P1D')); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertNotNull($share->getExpirationDate()); - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateEnforceTooFarIntoFuture(): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Cannot set expiration date more than 3 days in the future'); - - $future = new \DateTime(); - $future->add(new \DateInterval('P7D')); - - $share = $this->manager->newShare(); - $share->setExpirationDate($future); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', true], - ]); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - } - - public function testValidateExpirationDateEnforceValid(): void { - $future = new \DateTime('now', $this->timezone); - $future->add(new \DateInterval('P2D')); - $future->setTime(1, 2, 3); - - $expected = clone $future; - $expected->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setExpirationDate($future); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', true], - ]); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($future) { - return $data['expirationDate'] == $future; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateNoDefault(): void { - $date = new \DateTime('now', $this->timezone); - $date->add(new \DateInterval('P5D')); - $date->setTime(1, 2, 3); - - $expected = clone $date; - $expected->setTime(23, 59, 59); - $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - $share = $this->manager->newShare(); - $share->setExpirationDate($date); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected && $data['passwordSet'] === false; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateNoDateNoDefault(): void { - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) { - return $data['expirationDate'] === null && $data['passwordSet'] === true; - })); - - $share = $this->manager->newShare(); - $share->setPassword('password'); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertNull($share->getExpirationDate()); - } - - public function testValidateExpirationDateNoDateDefault(): void { - $share = $this->manager->newShare(); - - $expected = new \DateTime('now', $this->timezone); - $expected->add(new \DateInterval('P3D')); - $expected->setTime(23, 59, 59); - $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ['core', 'link_defaultExpDays', '3', '3'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', false], - ]); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateDefault(): void { - $future = new \DateTime('now', $this->timezone); - $future->add(new \DateInterval('P5D')); - $future->setTime(1, 2, 3); - - $expected = clone $future; - $expected->setTime(23, 59, 59); - $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - $share = $this->manager->newShare(); - $share->setExpirationDate($future); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ['core', 'link_defaultExpDays', '3', '1'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', false], - ]); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationNegativeOffsetTimezone(): void { - $this->timezone = new \DateTimeZone('Pacific/Tahiti'); - $future = new \DateTime(); - $future->add(new \DateInterval('P5D')); - - $expected = clone $future; - $expected->setTimezone($this->timezone); - $expected->setTime(23, 59, 59); - $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - $share = $this->manager->newShare(); - $share->setExpirationDate($future); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '3'], - ['core', 'link_defaultExpDays', '3', '1'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', false], - ]); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->with($this->callback(function ($data) use ($expected) { - return $data['expirationDate'] == $expected; - })); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($expected, $share->getExpirationDate()); - } - - public function testValidateExpirationDateHookModification(): void { - $nextWeek = new \DateTime('now', $this->timezone); - $nextWeek->add(new \DateInterval('P7D')); - - $save = clone $nextWeek; - $save->setTime(23, 59, 59); - $save->sub(new \DateInterval('P2D')); - $save->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->willReturnCallback(function ($data): void { - $data['expirationDate']->sub(new \DateInterval('P2D')); - }); - - $share = $this->manager->newShare(); - $share->setExpirationDate($nextWeek); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals($save, $share->getExpirationDate()); - } - - public function testValidateExpirationDateHookException(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Invalid date!'); - - $nextWeek = new \DateTime(); - $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(23, 59, 59); - - $share = $this->manager->newShare(); - $share->setExpirationDate($nextWeek); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('\OC\Share', 'verifyExpirationDate', $hookListener, 'listener'); - $hookListener->expects($this->once())->method('listener')->willReturnCallback(function ($data): void { - $data['accepted'] = false; - $data['message'] = 'Invalid date!'; - }); - - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - } - - public function testValidateExpirationDateExistingShareNoDefault(): void { - $share = $this->manager->newShare(); - - $share->setId('42')->setProviderId('foo'); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_expire_after_n_days', '7', '6'], - ]); - - $this->appConfig->method('getValueBool') - ->willReturnMap([ - ['core', 'shareapi_default_expire_date', true], - ['core', 'shareapi_enforce_expire_date', false], - ]); - self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); - - $this->assertEquals(null, $share->getExpirationDate()); - } - - public function testUserCreateChecksShareWithGroupMembersOnlyDifferentGroups(): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Sharing is only allowed with group members'); - - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('sharedBy') - ->willReturn($userFolder); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_USER) - ->setSharedBy('sharedBy') - ->setSharedWith('sharedWith') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL); - - $sharedBy = $this->createMock(IUser::class); - $sharedWith = $this->createMock(IUser::class); - - $this->groupManager - ->method('getUserGroupIds') - ->willReturnMap( - [ - [$sharedBy, ['group1']], - [$sharedWith, ['group2']], - ] - ); - - $this->userManager->method('userExists')->willReturnMap([ - ['sharedBy', true], - ['sharedWith', true], - ]); - - $this->userManager->method('get')->willReturnMap([ - ['sharedBy', $sharedBy], - ['sharedWith', $sharedWith], - ]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testUserCreateChecksShareWithGroupMembersOnlySharedGroup(): void { - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('sharedBy') - ->willReturn($userFolder); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_USER) - ->setSharedBy('sharedBy') - ->setSharedWith('sharedWith') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL); - - $sharedBy = $this->createMock(IUser::class); - $sharedBy - ->method('getUID') - ->willReturn('sharedBy'); - $sharedWith = $this->createMock(IUser::class); - - $this->groupManager - ->method('getUserGroupIds') - ->willReturnMap( - [ - [$sharedBy, ['group1', 'group3']], - [$sharedWith, ['group2', 'group3']], - ] - ); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['sharedBy', $sharedBy], - ['sharedWith', $sharedWith], - ]); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['sharedBy', true], - ['sharedWith', true], - ]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - $this->defaultProvider - ->method('getSharesByPath') - ->with($node) - ->willReturn([]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testUserCreateChecksIdenticalShareExists(): void { - $this->expectException(AlreadySharedException::class); - $this->expectExceptionMessage('Sharing name.txt failed, because this item is already shared with the account user'); - - $share = $this->manager->newShare(); - $share->setSharedWithDisplayName('user'); - $share2 = $this->manager->newShare(); - - $sharedWith = $this->createMock(IUser::class); - $path = $this->createMock(Node::class); - - $share->setSharedWith('sharedWith')->setNode($path) - ->setProviderId('foo')->setId('bar'); - - $share2->setSharedWith('sharedWith')->setNode($path) - ->setProviderId('foo')->setId('baz'); - - $this->defaultProvider - ->method('getSharesByPath') - ->with($path) - ->willReturn([$share2]); - - $path->method('getName') - ->willReturn('name.txt'); - - self::invokePrivate($this->manager, 'userCreateChecks', [$share]); - } - - public function testUserCreateChecksIdenticalPathSharedViaGroup(): void { - $this->expectException(AlreadySharedException::class); - $this->expectExceptionMessage('Sharing name2.txt failed, because this item is already shared with the account userName'); - - $share = $this->manager->newShare(); - - $sharedWith = $this->createMock(IUser::class); - $sharedWith->method('getUID')->willReturn('sharedWith'); - - $this->userManager->method('get')->with('sharedWith')->willReturn($sharedWith); - - $path = $this->createMock(Node::class); - - $share->setSharedWith('sharedWith') - ->setNode($path) - ->setShareOwner('shareOwner') - ->setSharedWithDisplayName('userName') - ->setProviderId('foo') - ->setId('bar'); - - $share2 = $this->manager->newShare(); - $share2->setShareType(IShare::TYPE_GROUP) - ->setShareOwner('shareOwner2') - ->setProviderId('foo') - ->setId('baz') - ->setSharedWith('group'); - - $group = $this->createMock(IGroup::class); - $group->method('inGroup') - ->with($sharedWith) - ->willReturn(true); - - $this->groupManager->method('get')->with('group')->willReturn($group); - - $this->defaultProvider - ->method('getSharesByPath') - ->with($path) - ->willReturn([$share2]); - - $path->method('getName') - ->willReturn('name2.txt'); - - self::invokePrivate($this->manager, 'userCreateChecks', [$share]); - } - - public function testUserCreateChecksIdenticalPathSharedViaDeletedGroup(): void { - $share = $this->manager->newShare(); - - $sharedWith = $this->createMock(IUser::class); - $sharedWith->method('getUID')->willReturn('sharedWith'); - - $this->userManager->method('get')->with('sharedWith')->willReturn($sharedWith); - - $path = $this->createMock(Node::class); - - $share->setSharedWith('sharedWith') - ->setNode($path) - ->setShareOwner('shareOwner') - ->setProviderId('foo') - ->setId('bar'); - - $share2 = $this->manager->newShare(); - $share2->setShareType(IShare::TYPE_GROUP) - ->setShareOwner('shareOwner2') - ->setProviderId('foo') - ->setId('baz') - ->setSharedWith('group'); - - $this->groupManager->method('get')->with('group')->willReturn(null); - - $this->defaultProvider - ->method('getSharesByPath') - ->with($path) - ->willReturn([$share2]); - - $this->assertNull($this->invokePrivate($this->manager, 'userCreateChecks', [$share])); - } - - public function testUserCreateChecksIdenticalPathNotSharedWithUser(): void { - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('sharedBy') - ->willReturn($userFolder); - - $share = $this->manager->newShare(); - $share - ->setShareType(IShare::TYPE_USER) - ->setSharedBy('sharedBy') - ->setSharedWith('sharedWith') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL) - ->setProviderId('foo') - ->setId('bar'); - - $sharedWith = $this->createMock(IUser::class); - $sharedBy = $this->createMock(IUser::class); - $sharedBy - ->method('getUID') - ->willReturn('sharedBy'); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['sharedWith', $sharedWith], - ['sharedBy', $sharedBy], - ]); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['sharedWith', true], - ['sharedBy', true], - ]); - - $share2 = $this->manager->newShare(); - $share2->setShareType(IShare::TYPE_GROUP) - ->setShareOwner('shareOwner2') - ->setProviderId('foo') - ->setId('baz'); - - $group = $this->createMock(IGroup::class); - $group->method('inGroup') - ->with($sharedWith) - ->willReturn(false); - - $this->groupManager->method('get')->with('group')->willReturn($group); - - $share2->setSharedWith('group'); - - $this->defaultProvider - ->method('getSharesByPath') - ->with($node) - ->willReturn([$share2]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testGroupCreateChecksShareWithGroupMembersGroupSharingNotAllowed(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group sharing is not allowed'); - - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $this->createMock(IUser::class)], - ]); - - $this->groupManager - ->method('groupExists') - ->willReturnMap([ - ['group1', true], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_GROUP) - ->setSharedBy('user1') - ->setSharedWith('group1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_group_sharing', 'yes', 'no'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testGroupCreateChecksShareWithGroupMembersOnlyNotInGroup(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Sharing is only allowed within your own groups'); - - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user') - ->willReturn($userFolder); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_GROUP) - ->setSharedBy('user') - ->setSharedWith('group') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL); - - $user = $this->createMock(IUser::class); - - $group = $this->createMock(IGroup::class); - $group - ->method('inGroup') - ->with($user) - ->willReturn(false); - - $this->groupManager - ->method('groupExists') - ->with('group') - ->willReturn(true); - - $this->groupManager - ->method('get') - ->with('group') - ->willReturn($group); - - $this->userManager - ->method('get') - ->with('user') - ->willReturn($user); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testGroupCreateChecksShareWithGroupMembersOnlyInGroup(): void { - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user') - ->willReturn($userFolder); - - $share = $this->manager->newShare(); - $share - ->setShareType(IShare::TYPE_GROUP) - ->setSharedBy('user') - ->setSharedWith('group') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_ALL); - - $user = $this->createMock(IUser::class); - $user - ->method('getUID') - ->willReturn('user'); - $group = $this->createMock(IGroup::class); - - $this->userManager - ->method('get') - ->with('user') - ->willReturn($user); - - $this->groupManager - ->method('groupExists') - ->with('group') - ->willReturn(true); - - $this->groupManager - ->method('get') - ->with('group') - ->willReturn($group); - - $group - ->method('inGroup') - ->with($user) - ->willReturn(true); - - $this->defaultProvider->method('getSharesByPath') - ->with($node) - ->willReturn([]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testGroupCreateChecksPathAlreadySharedWithSameGroup(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Path is already shared with this group'); - - $share = $this->manager->newShare(); - - $path = $this->createMock(Node::class); - $share->setSharedWith('sharedWith') - ->setNode($path) - ->setProviderId('foo') - ->setId('bar'); - - $share2 = $this->manager->newShare(); - $share2->setSharedWith('sharedWith') - ->setProviderId('foo') - ->setId('baz'); - - $this->defaultProvider->method('getSharesByPath') - ->with($path) - ->willReturn([$share2]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ]); - - self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); - } - - public function testGroupCreateChecksPathAlreadySharedWithDifferentGroup(): void { - $share = $this->manager->newShare(); - - $share->setSharedWith('sharedWith'); - - $path = $this->createMock(Node::class); - $share->setNode($path); - - $share2 = $this->manager->newShare(); - $share2->setSharedWith('sharedWith2'); - - $this->defaultProvider->method('getSharesByPath') - ->with($path) - ->willReturn([$share2]); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ]); - - self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); - } - - public function testLinkCreateChecksNoLinkSharesAllowed(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Public link sharing is not allowed.'); - - $node = $this->createMock(File::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $this->createMock(IUser::class)], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('user1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'no'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testFileLinkCreateChecksNoPublicUpload(): void { - $node = $this->createMock(File::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $user1 = $this->createMock(IUser::class); - $user1 - ->method('getUID') - ->willReturn('user1'); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('user1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ['core', 'shareapi_allow_public_upload', 'yes', 'no'] - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testFolderLinkCreateChecksNoPublicUpload(): void { - $this->expectException(GenericShareException::class); - $this->expectExceptionMessage('Public upload is not allowed.'); - - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $user1 = $this->createMock(IUser::class); - $user1 - ->method('getUID') - ->willReturn('user1'); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('user1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ['core', 'shareapi_allow_public_upload', 'yes', 'no'], - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testLinkCreateChecksPublicUpload(): void { - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $user1 = $this->createMock(IUser::class); - $user1 - ->method('getUID') - ->willReturn('user1'); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('user1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ['core', 'shareapi_allow_public_upload', 'yes', 'yes'] - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testLinkCreateChecksReadOnly(): void { - $node = $this->createMock(Folder::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ]); - - $user1 = $this->createMock(IUser::class); - $user1 - ->method('getUID') - ->willReturn('user1'); - - $this->userManager - ->method('get') - ->willReturnMap([ - ['user1', $user1], - ]); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setSharedBy('user1') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ); - - $this->config - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', 'yes'], - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ['core', 'shareapi_allow_public_upload', 'yes', 'yes'] - ]); - - self::invokePrivate($this->manager, 'generalChecks', [$share]); - } - - public function testPathCreateChecksContainsSharedMount(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('You cannot share a folder that contains other shares'); - - $path = $this->createMock(Folder::class); - $path->method('getPath')->willReturn('path'); - - $mount = $this->createMock(IMountPoint::class); - $storage = $this->createMock(IStorage::class); - $mount->method('getStorage')->willReturn($storage); - $storage->method('instanceOfStorage')->with('\OCA\Files_Sharing\ISharedStorage')->willReturn(true); - - $this->mountManager->method('findIn')->with('path')->willReturn([$mount]); - - self::invokePrivate($this->manager, 'pathCreateChecks', [$path]); - } - - public function testPathCreateChecksContainsNoSharedMount(): void { - $path = $this->createMock(Folder::class); - $path->method('getPath')->willReturn('path'); - - $mount = $this->createMock(IMountPoint::class); - $storage = $this->createMock(IStorage::class); - $mount->method('getStorage')->willReturn($storage); - $storage->method('instanceOfStorage')->with('\OCA\Files_Sharing\ISharedStorage')->willReturn(false); - - $this->mountManager->method('findIn')->with('path')->willReturn([$mount]); - - self::invokePrivate($this->manager, 'pathCreateChecks', [$path]); - } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] - public function testPathCreateChecksContainsNoFolder(): void { - $path = $this->createMock(File::class); - - self::invokePrivate($this->manager, 'pathCreateChecks', [$path]); - } - - public static function dataIsSharingDisabledForUser(): array { - $legacyList = 'group1,group2'; - $jsonList = json_encode(['group1', 'group2']); - - return [ - // No group restrictions configured - 'no restrictions, user without groups' => ['no', null, null, [], false], - 'no restrictions, user with groups' => ['no', null, null, ['group1'], false], - - // Empty legacy list, converted to json - 'block list, empty list, user without groups' => ['yes', '', json_encode(['']), [], false], - 'allow list, empty list, user without groups' => ['allow', '', json_encode(['']), [], true], - 'block list, empty list, user with groups' => ['yes', '', json_encode(['']), ['group1', 'group2'], false], - 'allow list, empty list, user with groups' => ['allow', '', json_encode(['']), ['group1', 'group2'], true], - - // Legacy comma separated list, converted to json - 'block list, legacy list, user without groups' => ['yes', $legacyList, $jsonList, [], false], - 'allow list, legacy list, user without groups' => ['allow', $legacyList, $jsonList, [], true], - 'block list, legacy list, user not in list' => ['yes', $legacyList, $jsonList, ['group3'], false], - 'allow list, legacy list, user not in list' => ['allow', $legacyList, $jsonList, ['group3'], true], - 'block list, legacy list, user partly in list' => ['yes', $legacyList, $jsonList, ['group1', 'group3'], true], - 'allow list, legacy list, user partly in list' => ['allow', $legacyList, $jsonList, ['group1', 'group3'], false], - 'block list, legacy list, user only in list' => ['yes', $legacyList, $jsonList, ['group1'], true], - 'allow list, legacy list, user only in list' => ['allow', $legacyList, $jsonList, ['group1'], false], - - // Json encoded list - 'block list, user without groups' => ['yes', $jsonList, null, [], false], - 'allow list, user without groups' => ['allow', $jsonList, null, [], true], - 'block list, user not in list' => ['yes', $jsonList, null, ['group3'], false], - 'allow list, user not in list' => ['allow', $jsonList, null, ['group3'], true], - 'block list, user partly in list' => ['yes', $jsonList, null, ['group1', 'group3'], true], - 'allow list, user partly in list' => ['allow', $jsonList, null, ['group1', 'group3'], false], - 'block list, user only in list' => ['yes', $jsonList, null, ['group2'], true], - 'allow list, user only in list' => ['allow', $jsonList, null, ['group2'], false], - ]; - } - - /** - * @param string $excludeGroups Value of the `shareapi_exclude_groups` setting - * @param ?string $groupList Value of the `shareapi_exclude_groups_list` setting - * @param ?string $setList Expected value the group list is migrated to, or null if no migration is expected - * @param string[] $groupIds Groups of the user - * @param bool $expected Whether sharing is expected to be disabled for the user - */ - #[DataProvider('dataIsSharingDisabledForUser')] - public function testIsSharingDisabledForUser(string $excludeGroups, ?string $groupList, ?string $setList, array $groupIds, bool $expected): void { - $user = $this->createMock(IUser::class); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_exclude_groups', 'no', $excludeGroups], - ['core', 'shareapi_exclude_groups_list', '', $groupList], - ]); - - if ($setList !== null) { - $this->config->expects($this->once()) - ->method('setAppValue') - ->with('core', 'shareapi_exclude_groups_list', $setList); - } else { - $this->config->expects($this->never()) - ->method('setAppValue'); - } - - $this->groupManager->method('getUserGroupIds') - ->with($user) - ->willReturn($groupIds); - - $this->userManager->method('get')->with('user')->willReturn($user); - - $res = $this->manager->sharingDisabledForUser('user'); - $this->assertEquals($expected, $res); - } - - public static function dataCanShare() { - $data = []; - - /* - * [expected, sharing enabled, disabled for user] - */ - - $data[] = [false, 'no', false]; - $data[] = [false, 'no', true]; - $data[] = [true, 'yes', false]; - $data[] = [false, 'yes', true]; - - return $data; - } - - /** - * - * @param bool $expected - * @param string $sharingEnabled - * @param bool $disabledForUser - */ - #[DataProvider('dataCanShare')] - public function testCanShare($expected, $sharingEnabled, $disabledForUser): void { - $this->config->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_enabled', 'yes', $sharingEnabled], - ]); - - $manager = $this->createManagerMock() - ->onlyMethods(['sharingDisabledForUser']) - ->getMock(); - - $manager->method('sharingDisabledForUser') - ->with('user1') - ->willReturn($disabledForUser); - - $this->overwriteService(IManager::class, $manager); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [$manager]); - - $node = $this->createMock(File::class); - $node - ->method('getId') - ->willReturn(108); - $node - ->method('getPermissions') - ->willReturn(Constants::PERMISSION_ALL); - $node - ->method('isShareable') - ->willReturn(true); - - $userFolder = $this->createMock(Folder::class); - $userFolder - ->method('getById') - ->with(108) - ->willReturn([$node]); - $userFolder - ->method('getFirstNodeById') - ->with(108) - ->willReturn($node); - - $this->rootFolder - ->method('getUserFolder') - ->with('user1') - ->willReturn($userFolder); - - $user1 = $this->createMock(IUser::class); - $user1 - ->method('getUID') - ->willReturn('user1'); - - $this->userManager - ->method('get') - ->with('user1') - ->willReturn($user1); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_USER) - ->setSharedBy('user1') - ->setSharedWith('user2') - ->setNode($node) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE); - - $this->userManager - ->method('userExists') - ->willReturnMap([ - ['user1', true], - ['user2', true], - ]); - - $exception = false; - try { - $res = self::invokePrivate($manager, 'generalChecks', [$share]); - } catch (\Exception $e) { - $exception = true; - } - - $this->assertEquals($expected, !$exception); - - $this->restoreService(IManager::class); - self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); - } - - public function testCreateShareUser(): void { - /** @var Manager&MockObject $manager */ - $manager = $this->createManagerMock() - ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($storage); - - $share = $this->createShare( - null, - IShare::TYPE_USER, - $path, - 'sharedWith', - 'sharedBy', - null, - Constants::PERMISSION_ALL); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - $manager->expects($this->once()) - ->method('userCreateChecks') - ->with($share); - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateInternal') - ->with($share) - ->willReturnArgument(0); - - $this->defaultProvider - ->expects($this->once()) - ->method('create') - ->with($share) - ->willReturnArgument(0); - - $share->expects($this->once()) - ->method('setShareOwner') - ->with('shareOwner'); - $share->expects($this->once()) - ->method('setTarget') - ->with('/target'); - - $manager->createShare($share); - } - - public function testCreateShareUserAlreadySharedReusesExistingShare(): void { - /** @var Manager&MockObject $manager */ - $manager = $this->createManagerMock() - ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks']) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($this->createMock(IStorage::class)); - - $share = $this->createShare(null, IShare::TYPE_USER, $path, 'sharedWith', 'sharedBy', null, Constants::PERMISSION_ALL); - $existingShare = $this->createShare('42', IShare::TYPE_USER, $path, 'sharedWith', 'shareOwner', 'shareOwner', Constants::PERMISSION_READ); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - $this->userManager->method('get')->willReturnMap([ - ['shareOwner', $this->createEnabledUser(true)], - ]); - - $manager->method('userCreateChecks') - ->willThrowException(new AlreadySharedException('Already shared', $existingShare)); - - $this->defaultProvider->expects($this->never()) - ->method('create'); - - $existingShare->expects($this->once()) - ->method('setNode') - ->with($path); - - $this->assertSame($existingShare, $manager->createShare($share)); - } - - public function testCreateShareUserAlreadySharedByDisabledInitiator(): void { - $this->expectException(AlreadySharedException::class); - - /** @var Manager&MockObject $manager */ - $manager = $this->createManagerMock() - ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks']) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($this->createMock(IStorage::class)); - - $share = $this->createShare(null, IShare::TYPE_USER, $path, 'sharedWith', 'shareOwner', null, Constants::PERMISSION_ALL); - $existingShare = $this->createShare('42', IShare::TYPE_USER, $path, 'sharedWith', 'initiator', 'shareOwner', Constants::PERMISSION_READ); - - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - $this->userManager->method('get')->willReturnMap([ - ['shareOwner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ]); - - $manager->method('userCreateChecks') - ->willThrowException(new AlreadySharedException('Already shared', $existingShare)); - - $this->defaultProvider->expects($this->never()) - ->method('create'); - $this->dispatcher->expects($this->never()) - ->method('dispatchTyped'); - - $manager->createShare($share); - } - - public function testCreateShareGroup(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['generalChecks', 'groupCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($storage); - - $share = $this->createShare( - null, - IShare::TYPE_GROUP, - $path, - 'sharedWith', - 'sharedBy', - null, - Constants::PERMISSION_ALL); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - $manager->expects($this->once()) - ->method('groupCreateChecks') - ->with($share); - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateInternal') - ->with($share) - ->willReturnArgument(0); - - $this->defaultProvider - ->expects($this->once()) - ->method('create') - ->with($share) - ->willReturnArgument(0); - - $share->expects($this->once()) - ->method('setShareOwner') - ->with('shareOwner'); - $share->expects($this->once()) - ->method('setTarget') - ->with('/target'); - - $manager->createShare($share); - } - - public function testCreateShareLink(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'generalChecks', - 'pathCreateChecks', - 'validateExpirationDateLink', - 'verifyPassword', - 'setLinkParent', - ]) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getId')->willReturn(1); - $path->method('getStorage')->willReturn($storage); - - $date = new \DateTime(); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setNode($path) - ->setSharedBy('sharedBy') - ->setPermissions(Constants::PERMISSION_ALL) - ->setExpirationDate($date) - ->setPassword('password'); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateLink') - ->with($share) - ->willReturn($share); - $manager->expects($this->once()) - ->method('verifyPassword') - ->with('password'); - $manager->expects($this->once()) - ->method('setLinkParent') - ->with($share); - - $this->secureRandom->method('generate') - ->willReturn('token'); - - $this->defaultProvider - ->expects($this->once()) - ->method('create') - ->with($share) - ->willReturnCallback(function (Share $share) { - return $share->setId(42); - }); - - $calls = [ - BeforeShareCreatedEvent::class, - ShareCreatedEvent::class, - ]; - $this->dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$calls, $date, $path): void { - $expected = array_shift($calls); - $this->assertInstanceOf($expected, $event); - $share = $event->getShare(); - - $this->assertEquals(IShare::TYPE_LINK, $share->getShareType(), 'getShareType'); - $this->assertEquals($path, $share->getNode(), 'getNode'); - $this->assertEquals('sharedBy', $share->getSharedBy(), 'getSharedBy'); - $this->assertEquals(Constants::PERMISSION_ALL, $share->getPermissions(), 'getPermissions'); - $this->assertEquals($date, $share->getExpirationDate(), 'getExpirationDate'); - $this->assertEquals('hashed', $share->getPassword(), 'getPassword'); - $this->assertEquals('token', $share->getToken(), 'getToken'); - - if ($expected === ShareCreatedEvent::class) { - $this->assertEquals('42', $share->getId(), 'getId'); - $this->assertEquals('/target', $share->getTarget(), 'getTarget'); - } - }); - - /** @var IShare $share */ - $share = $manager->createShare($share); - - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertEquals('/target', $share->getTarget()); - $this->assertSame($date, $share->getExpirationDate()); - $this->assertEquals('token', $share->getToken()); - $this->assertTrue($share->isPasswordHashed()); - $this->assertNotEmpty($share->getPassword()); - } - - public function testCreateShareMail(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'generalChecks', - 'pathCreateChecks', - 'validateExpirationDateLink', - 'verifyPassword', - 'setLinkParent', - ]) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getId')->willReturn(1); - $path->method('getStorage')->willReturn($storage); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_EMAIL) - ->setNode($path) - ->setSharedBy('sharedBy') - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateLink') - ->with($share) - ->willReturn($share); - $manager->expects($this->once()) - ->method('verifyPassword'); - $manager->expects($this->once()) - ->method('setLinkParent'); - - $this->secureRandom->method('generate') - ->willReturn('token'); - - $this->defaultProvider - ->expects($this->once()) - ->method('create') - ->with($share) - ->willReturnCallback(function (Share $share) { - return $share->setId(42); - }); - - $calls = [ - BeforeShareCreatedEvent::class, - ShareCreatedEvent::class, - ]; - $this->dispatcher->expects($this->exactly(2)) - ->method('dispatchTyped') - ->willReturnCallback(function ($event) use (&$calls, $path): void { - $expected = array_shift($calls); - $this->assertInstanceOf($expected, $event); - $share = $event->getShare(); - - $this->assertEquals(IShare::TYPE_EMAIL, $share->getShareType(), 'getShareType'); - $this->assertEquals($path, $share->getNode(), 'getNode'); - $this->assertEquals('sharedBy', $share->getSharedBy(), 'getSharedBy'); - $this->assertEquals(Constants::PERMISSION_ALL, $share->getPermissions(), 'getPermissions'); - $this->assertNull($share->getExpirationDate(), 'getExpirationDate'); - $this->assertNull($share->getPassword(), 'getPassword'); - $this->assertEquals('token', $share->getToken(), 'getToken'); - - if ($expected === ShareCreatedEvent::class) { - $this->assertEquals('42', $share->getId(), 'getId'); - $this->assertEquals('/target', $share->getTarget(), 'getTarget'); - } - }); - - /** @var IShare $share */ - $share = $manager->createShare($share); - - $this->assertSame('shareOwner', $share->getShareOwner()); - $this->assertEquals('/target', $share->getTarget()); - $this->assertEquals('token', $share->getToken()); - } - - public function testCreateShareHookError(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('I won\'t let you share'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'generalChecks', - 'userCreateChecks', - 'pathCreateChecks', - 'validateExpirationDateInternal', - ]) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $path = $this->createMock(File::class); - $path->method('getOwner')->willReturn($shareOwner); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($storage); - - $share = $this->createShare( - null, - IShare::TYPE_USER, - $path, - 'sharedWith', - 'sharedBy', - null, - Constants::PERMISSION_ALL); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - $manager->expects($this->once()) - ->method('userCreateChecks') - ->with($share); - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateInternal') - ->with($share) - ->willReturnArgument(0); - - $share->expects($this->once()) - ->method('setShareOwner') - ->with('shareOwner'); - $share->expects($this->once()) - ->method('setTarget') - ->with('/target'); - - // Pre share - $this->dispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with( - $this->isInstanceOf(BeforeShareCreatedEvent::class) - )->willReturnCallback(function (BeforeShareCreatedEvent $e): void { - $e->setError('I won\'t let you share!'); - $e->stopPropagation(); - } - ); - - $manager->createShare($share); - } - - public function testCreateShareOfIncomingFederatedShare(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) - ->getMock(); - - $shareOwner = $this->createMock(IUser::class); - $shareOwner->method('getUID')->willReturn('shareOwner'); - - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->with('OCA\Files_Sharing\External\Storage') - ->willReturn(true); - - $storage2 = $this->createMock(IStorage::class); - $storage2->method('instanceOfStorage') - ->with('OCA\Files_Sharing\External\Storage') - ->willReturn(false); - - $path = $this->createMock(File::class); - $path->expects($this->never())->method('getOwner'); - $path->method('getName')->willReturn('target'); - $path->method('getStorage')->willReturn($storage); - - $parent = $this->createMock(Folder::class); - $parent->method('getStorage')->willReturn($storage); - - $parentParent = $this->createMock(Folder::class); - $parentParent->method('getStorage')->willReturn($storage2); - $parentParent->method('getOwner')->willReturn($shareOwner); - - $path->method('getParent')->willReturn($parent); - $parent->method('getParent')->willReturn($parentParent); - - $share = $this->createShare( - null, - IShare::TYPE_USER, - $path, - 'sharedWith', - 'sharedBy', - null, - Constants::PERMISSION_ALL); - - $manager->expects($this->once()) - ->method('generalChecks') - ->with($share); - $manager->expects($this->once()) - ->method('userCreateChecks') - ->with($share); - $manager->expects($this->once()) - ->method('pathCreateChecks') - ->with($path); - $manager->expects($this->once()) - ->method('validateExpirationDateInternal') - ->with($share) - ->willReturnArgument(0); - - $this->defaultProvider - ->expects($this->once()) - ->method('create') - ->with($share) - ->willReturnArgument(0); - - $share->expects($this->once()) - ->method('setShareOwner') - ->with('shareOwner'); - $share->expects($this->once()) - ->method('setTarget') - ->with('/target'); - - $manager->createShare($share); - } - - public function testGetSharesBy(): void { - $share = $this->manager->newShare(); - - $node = $this->createMock(Folder::class); - - $this->defaultProvider->expects($this->once()) - ->method('getSharesBy') - ->with( - $this->equalTo('user'), - $this->equalTo(IShare::TYPE_USER), - $this->equalTo($node), - $this->equalTo(true), - $this->equalTo(1), - $this->equalTo(1) - )->willReturn([$share]); - - $shares = $this->manager->getSharesBy('user', IShare::TYPE_USER, $node, true, 1, 1); - - $this->assertCount(1, $shares); - $this->assertSame($share, $shares[0]); - } - - public function testGetSharesByOwnerless(): void { - $mount = $this->createMock(IShareOwnerlessMount::class); - - $node = $this->createMock(Folder::class); - $node - ->expects($this->once()) - ->method('getMountPoint') - ->willReturn($mount); - - $share = $this->manager->newShare(); - $share->setNode($node); - $share->setShareType(IShare::TYPE_USER); - - $this->defaultProvider - ->expects($this->once()) - ->method('getSharesByPath') - ->with($this->equalTo($node)) - ->willReturn([$share]); - - $shares = $this->manager->getSharesBy('user', IShare::TYPE_USER, $node, true, 1, 1); - - $this->assertCount(1, $shares); - $this->assertSame($share, $shares[0]); - } - - public function testGetSharesByOfDisabledInitiatorAsShareOwner(): void { - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setShareOwner('owner') - ->setSharedBy('initiator') - ->setSharedWith('recipient'); - - $this->userManager->method('get')->willReturnMap([ - ['owner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ]); - - $this->defaultProvider->expects($this->once()) - ->method('getSharesBy') - ->willReturn([$share]); - - $shares = $this->manager->getSharesBy('owner', IShare::TYPE_USER, $this->createMock(Folder::class), true, -1, 0); - - $this->assertCount(1, $shares); - $this->assertSame($share, $shares[0]); - } - - public function testGetSharesByOfDisabledInitiatorAsOtherUser(): void { - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setShareOwner('owner') - ->setSharedBy('initiator') - ->setSharedWith('recipient'); - - $this->userManager->method('get')->willReturnMap([ - ['owner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ['resharer', $this->createEnabledUser(true)], - ]); - - $this->defaultProvider->expects($this->once()) - ->method('getSharesBy') - ->willReturn([$share]); - - $shares = $this->manager->getSharesBy('resharer', IShare::TYPE_USER, $this->createMock(Folder::class), true, -1, 0); - - $this->assertCount(0, $shares); - } - - public function testGetSharedWithOfDisabledInitiator(): void { - $this->config->method('getAppValue') - ->willReturnMap([ - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setShareOwner('owner') - ->setSharedBy('initiator') - ->setSharedWith('recipient'); - - $this->userManager->method('get')->willReturnMap([ - ['owner', $this->createEnabledUser(true)], - ['initiator', $this->createEnabledUser(false)], - ]); - - $this->defaultProvider->expects($this->once()) - ->method('getSharedWith') - ->willReturn([$share]); - - $shares = $this->manager->getSharedWith('recipient', IShare::TYPE_USER); - - $this->assertCount(0, $shares); - } - - /** - * Test to ensure we correctly remove expired link shares - * - * We have 8 Shares and we want the 3 first valid shares. - * share 3-6 and 8 are expired. Thus at the end of this test we should - * have received share 1,2 and 7. And from the manager. Share 3-6 should be - * deleted (as they are evaluated). but share 8 should still be there. - */ - public function testGetSharesByExpiredLinkShares(): void { - $manager = $this->createManagerMock() - ->onlyMethods(['deleteShare']) - ->getMock(); - - /** @var IShare[] $shares */ - $shares = []; - - /* - * This results in an array of 8 IShare elements - */ - for ($i = 0; $i < 8; $i++) { - $share = $this->manager->newShare(); - $share->setId($i); - $shares[] = $share; - } - - $today = new \DateTime(); - $today->setTime(0, 0, 0); - - /* - * Set the expiration date to today for some shares - */ - $shares[2]->setExpirationDate($today); - $shares[3]->setExpirationDate($today); - $shares[4]->setExpirationDate($today); - $shares[5]->setExpirationDate($today); - - /** @var IShare[] $i */ - $shares2 = []; - for ($i = 0; $i < 8; $i++) { - $shares2[] = clone $shares[$i]; - } - - $node = $this->createMock(File::class); - - /* - * Simulate the getSharesBy call. - */ - $this->defaultProvider - ->method('getSharesBy') - ->willReturnCallback(function ($uid, $type, $node, $reshares, $limit, $offset) use (&$shares2) { - return array_slice($shares2, $offset, $limit); - }); - - /* - * Simulate the deleteShare call. - */ - $manager->method('deleteShare') - ->willReturnCallback(function ($share) use (&$shares2): void { - for ($i = 0; $i < count($shares2); $i++) { - if ($shares2[$i]->getId() === $share->getId()) { - array_splice($shares2, $i, 1); - break; - } - } - }); - - $res = $manager->getSharesBy('user', IShare::TYPE_LINK, $node, true, 3, 0); - - $this->assertCount(3, $res); - $this->assertEquals($shares[0]->getId(), $res[0]->getId()); - $this->assertEquals($shares[1]->getId(), $res[1]->getId()); - $this->assertEquals($shares[6]->getId(), $res[2]->getId()); - - $this->assertCount(4, $shares2); - $this->assertEquals(0, $shares2[0]->getId()); - $this->assertEquals(1, $shares2[1]->getId()); - $this->assertEquals(6, $shares2[2]->getId()); - $this->assertEquals(7, $shares2[3]->getId()); - $this->assertSame($today, $shares[3]->getExpirationDate()); - } - - public function testGetShareByToken(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ]); - - $factory = $this->createMock(IProviderFactory::class); - - $manager = $this->createManager($factory); - - $share = $this->createMock(IShare::class); - - $factory->expects($this->once()) - ->method('getProviderForType') - ->with(IShare::TYPE_LINK) - ->willReturn($this->defaultProvider); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('token') - ->willReturn($share); - - $ret = $manager->getShareByToken('token'); - $this->assertSame($share, $ret); - } - - public function testGetShareByTokenRoom(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'no'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ]); - - $factory = $this->createMock(IProviderFactory::class); - - $manager = $this->createManager($factory); - - $share = $this->createMock(IShare::class); - - $roomShareProvider = $this->createMock(IShareProvider::class); - - $factory->expects($this->any()) - ->method('getProviderForType') - ->willReturnCallback(function ($shareType) use ($roomShareProvider) { - if ($shareType !== IShare::TYPE_ROOM) { - throw new ProviderException(); - } - - return $roomShareProvider; - }); - - $roomShareProvider->expects($this->once()) - ->method('getShareByToken') - ->with('token') - ->willReturn($share); - - $ret = $manager->getShareByToken('token'); - $this->assertSame($share, $ret); - } - - public function testGetShareByTokenWithException(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ]); - - $factory = $this->createMock(IProviderFactory::class); - - $manager = $this->createManager($factory); - - $share = $this->createMock(IShare::class); - - $calls = [ - [IShare::TYPE_LINK], - [IShare::TYPE_REMOTE], - ]; - $factory->expects($this->exactly(2)) - ->method('getProviderForType') - ->willReturnCallback(function () use (&$calls) { - $expected = array_shift($calls); - $this->assertEquals($expected, func_get_args()); - return $this->defaultProvider; - }); - - $this->defaultProvider->expects($this->exactly(2)) - ->method('getShareByToken') - ->with('token') - ->willReturnOnConsecutiveCalls( - $this->throwException(new ShareNotFound()), - $share - ); - - $ret = $manager->getShareByToken('token'); - $this->assertSame($share, $ret); - } - - public function testGetShareByTokenHideDisabledUser(): void { - $this->expectException(ShareNotFound::class); - $this->expectExceptionMessage('The requested share does not exist anymore'); - - $this->config - ->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'yes', 'yes'], - ]); - - $this->l->expects($this->once()) - ->method('t') - ->willReturnArgument(0); - - $manager = $this->createManagerMock() - ->onlyMethods(['deleteShare']) - ->getMock(); - - $date = new \DateTime(); - $date->setTime(0, 0, 0); - $date->add(new \DateInterval('P2D')); - $share = $this->manager->newShare(); - $share->setExpirationDate($date); - $share->setShareOwner('owner'); - $share->setSharedBy('sharedBy'); - - $sharedBy = $this->createMock(IUser::class); - $owner = $this->createMock(IUser::class); - - $this->userManager->method('get')->willReturnMap([ - ['sharedBy', $sharedBy], - ['owner', $owner], - ]); - - $owner->expects($this->once()) - ->method('isEnabled') - ->willReturn(true); - $sharedBy->expects($this->once()) - ->method('isEnabled') - ->willReturn(false); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('expiredToken') - ->willReturn($share); - - $manager->expects($this->never()) - ->method('deleteShare'); - - $manager->getShareByToken('expiredToken'); - } - - public function testGetShareByTokenExpired(): void { - $this->expectException(ShareNotFound::class); - $this->expectExceptionMessage('The requested share does not exist anymore'); - - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('core', 'shareapi_allow_links', 'yes') - ->willReturn('yes'); - - $this->l->expects($this->once()) - ->method('t') - ->willReturnArgument(0); - - $manager = $this->createManagerMock() - ->onlyMethods(['deleteShare']) - ->getMock(); - - $date = new \DateTime(); - $date->setTime(0, 0, 0); - $share = $this->manager->newShare(); - $share->setExpirationDate($date); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('expiredToken') - ->willReturn($share); - - $manager->expects($this->once()) - ->method('deleteShare') - ->with($this->equalTo($share)); - - $manager->getShareByToken('expiredToken'); - } - - public function testGetShareByTokenNotExpired(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ]); - - $date = new \DateTime(); - $date->setTime(0, 0, 0); - $date->add(new \DateInterval('P2D')); - $share = $this->manager->newShare(); - $share->setExpirationDate($date); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('expiredToken') - ->willReturn($share); - - $res = $this->manager->getShareByToken('expiredToken'); - - $this->assertSame($share, $res); - } - - public function testGetShareByTokenWithPublicLinksDisabled(): void { - $this->expectException(ShareNotFound::class); - - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('core', 'shareapi_allow_links', 'yes') - ->willReturn('no'); - $this->manager->getShareByToken('validToken'); - } - - public function testGetShareByTokenPublicUploadDisabled(): void { - $this->config - ->expects($this->exactly(5)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_public_upload', 'yes', 'no'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - $share->setSharedWith('sharedWith'); - $share->setShareOwner('shareOwner'); - $folder = $this->createMock(\OC\Files\Node\Folder::class); - $share->setNode($folder); - - $shareOwner = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('shareOwner') - ->willReturn($shareOwner); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->willReturn('validToken') - ->willReturn($share); - - $res = $this->manager->getShareByToken('validToken'); - - $this->assertSame(Constants::PERMISSION_READ, $res->getPermissions()); - } - - public function testGetShareByTokenShareOwnerExcludedFromLinkShares(): void { - $this->expectException(ShareNotFound::class); - $this->expectExceptionMessage('The requested share does not exist anymore'); - - $this->config - ->expects($this->exactly(4)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '["excludedGroup"]'], - ]); - - $this->l->expects($this->once()) - ->method('t') - ->willReturnArgument(0); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ); - $share->setShareOwner('shareOwner'); - $file = $this->createMock(File::class); - $share->setNode($file); - - $shareOwner = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('shareOwner') - ->willReturn($shareOwner); - - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($shareOwner) - ->willReturn(['excludedGroup', 'otherGroup']); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('token') - ->willReturn($share); - - $this->manager->getShareByToken('token'); - } - - public function testGetShareByTokenShareOwnerNotExcludedFromLinkShares(): void { - $this->config - ->expects($this->exactly(4)) - ->method('getAppValue') - ->willReturnMap([ - ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], - ['core', 'shareapi_allow_links_exclude_groups', '[]', '["excludedGroup"]'], - ]); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK) - ->setPermissions(Constants::PERMISSION_READ); - $share->setShareOwner('shareOwner'); - $file = $this->createMock(File::class); - $share->setNode($file); - - $shareOwner = $this->createMock(IUser::class); - $this->userManager->expects($this->once()) - ->method('get') - ->with('shareOwner') - ->willReturn($shareOwner); - - $this->groupManager->expects($this->once()) - ->method('getUserGroupIds') - ->with($shareOwner) - ->willReturn(['allowedGroup', 'otherGroup']); - - $this->defaultProvider->expects($this->once()) - ->method('getShareByToken') - ->with('token') - ->willReturn($share); - - $res = $this->manager->getShareByToken('token'); - - $this->assertSame($share, $res); - } - - public function testCheckPasswordNoLinkShare(): void { - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_USER); - $this->assertFalse($this->manager->checkPassword($share, 'password')); - } - - public function testCheckPasswordNoPassword(): void { - $share = $this->createMock(IShare::class); - $share->method('getShareType')->willReturn(IShare::TYPE_LINK); - $this->assertFalse($this->manager->checkPassword($share, 'password')); - - $share->method('getPassword')->willReturn('password'); - $share->method('isPasswordProtected')->willReturn(true); - $this->assertFalse($this->manager->checkPassword($share, null)); - } - - public function testCheckPasswordInvalidPassword(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setPasswordHash($passwordHash); - - $this->assertFalse($this->manager->checkPassword($share, 'invalidpassword')); - } - - public function testCheckPasswordValidPassword(): void { - $passwordHash = Server::get(IHasher::class)->hash('password'); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setPasswordHash($passwordHash); - - $this->assertTrue($this->manager->checkPassword($share, 'password')); - } - - public static function dataHasherAlgorithm(): array { - $algorithms = [ - PASSWORD_BCRYPT, - ]; - - if (\defined('PASSWORD_ARGON2I')) { - $algorithms[] = PASSWORD_ARGON2I; - } - - if (\defined('PASSWORD_ARGON2ID')) { - $algorithms[] = PASSWORD_ARGON2ID; - } - - return array_map(static fn (string $algorithm): array => [$algorithm], $algorithms); - } - - #[DataProvider('dataHasherAlgorithm')] - public function testCheckPasswordUpdateShareNoRehash(string $algorithm): void { - $hasher = Server::get(IHasher::class); - $this->invokePrivate($hasher, 'forcedAlgorithm', [$algorithm]); - - $passwordHash = $hasher->hash('password'); - $this->assertEquals((match ($algorithm) { - PASSWORD_ARGON2ID => 3, - PASSWORD_ARGON2I => 2, - PASSWORD_BCRYPT => 1, - }), (int)(explode('|', $passwordHash)[0])); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setPasswordHash($passwordHash); - - $this->defaultProvider->expects($this->never())->method('update'); - - $this->assertTrue($this->manager->checkPassword($share, 'password')); - - $this->invokePrivate($hasher, 'forcedAlgorithm', [null]); - } - - #[DataProvider('dataHasherAlgorithm')] - public function testCheckPasswordUpdateShare(string $algorithm): void { - $hasher = Server::get(IHasher::class); - $this->invokePrivate($hasher, 'forcedAlgorithm', [$algorithm]); - - $passwordHash = $hasher->hash('password'); - $this->assertEquals((match ($algorithm) { - PASSWORD_ARGON2ID => 3, - PASSWORD_ARGON2I => 2, - PASSWORD_BCRYPT => 1, - }), (int)(explode('|', $passwordHash)[0])); - - $previousHasherOptions = $this->invokePrivate($hasher, 'options'); - - // Make sure the hasher wants to rehash the password - $this->invokePrivate( - $hasher, - 'options', - [ - [ - 'threads' => $previousHasherOptions['threads'] ?? 1, - 'memory_cost' => ($previousHasherOptions['memory_cost'] ?? PASSWORD_ARGON2_DEFAULT_MEMORY_COST) + 1, - 'time_cost' => ($previousHasherOptions['time_cost'] ?? PASSWORD_ARGON2_DEFAULT_TIME_COST) + 1, - 'cost' => ($previousHasherOptions['cost'] ?? PASSWORD_BCRYPT_DEFAULT_COST) + 1, - ], - ], - ); - - $share = $this->manager->newShare() - ->setShareType(IShare::TYPE_LINK) - ->setPasswordHash($passwordHash); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($this->callback(function (IShare $share) use ($passwordHash) { - return $share->getPassword() !== null && $share->isPasswordHashed() && $share->getPassword() !== $passwordHash; - })); - - $this->assertTrue($this->manager->checkPassword($share, 'password')); - - $this->invokePrivate($hasher, 'options', [$previousHasherOptions]); - $this->invokePrivate($hasher, 'forcedAlgorithm', [null]); - } - - public function testUpdateShareCantChangeShareType(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Cannot change share type'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById' - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_GROUP); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - - $share = $this->manager->newShare(); - $attrs = $this->manager->newShare()->newAttributes(); - $attrs->setAttribute('app1', 'perm1', true); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_USER); - - $manager->updateShare($share); - } - - public function testUpdateShareCantChangeRecipientForGroupShare(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Can only update recipient on user shares'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById' - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_GROUP) - ->setSharedWith('origGroup'); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_GROUP) - ->setSharedWith('newGroup'); - - $manager->updateShare($share); - } - - public function testUpdateShareCantShareWithOwner(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Cannot share with the share owner'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById' - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_USER) - ->setSharedWith('sharedWith'); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('newUser') - ->setShareOwner('newUser'); - - $manager->updateShare($share); - } - - public function testUpdateShareUser(): void { - $this->userManager->expects($this->any())->method('userExists')->willReturn(true); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'userCreateChecks', - 'pathCreateChecks', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_USER) - ->setSharedWith('origUser') - ->setPermissions(1); - - $node = $this->createMock(File::class); - $node->method('getId')->willReturn(100); - $node->method('getPath')->willReturn('/newUser/files/myPath'); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - - $share = $this->manager->newShare(); - $attrs = $this->manager->newShare()->newAttributes(); - $attrs->setAttribute('app1', 'perm1', true); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_USER) - ->setSharedWith('origUser') - ->setShareOwner('newUser') - ->setSharedBy('sharer') - ->setPermissions(31) - ->setAttributes($attrs) - ->setNode($node); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share) - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $this->rootFolder->method('getUserFolder')->with('newUser')->willReturnSelf(); - $this->rootFolder->method('getRelativePath')->with('/newUser/files/myPath')->willReturn('/myPath'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener2, 'post'); - $hookListener2->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'shareType' => IShare::TYPE_USER, - 'shareWith' => 'origUser', - 'uidOwner' => 'sharer', - 'permissions' => 31, - 'path' => '/myPath', - 'attributes' => $attrs->toArray(), - ]); - - $manager->updateShare($share); - } - - public function testUpdateShareGroup(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'groupCreateChecks', - 'pathCreateChecks', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_GROUP) - ->setSharedWith('origUser') - ->setPermissions(31); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - - $node = $this->createMock(File::class); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_GROUP) - ->setSharedWith('origUser') - ->setShareOwner('owner') - ->setNode($node) - ->setPermissions(31); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share) - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareLink(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'pathCreateChecks', - 'verifyPassword', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_LINK) - ->setPermissions(15); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_LINK) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('password') - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(15); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('validateExpirationDateLink')->with($share); - $manager->expects($this->once())->method('verifyPassword')->with('password'); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share) - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'date' => $tomorrow, - 'uidOwner' => 'owner', - ]); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'uidOwner' => 'owner', - 'token' => 'token', - 'disabled' => false, - ]); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareLinkEnableSendPasswordByTalkWithNoPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot enable sending the password by Talk with an empty password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'pathCreateChecks', - 'verifyPassword', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_LINK) - ->setPermissions(15); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_LINK) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword(null) - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(15); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->never())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMail(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('password') - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->once())->method('verifyPassword')->with('password'); - $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share, 'password') - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'date' => $tomorrow, - 'uidOwner' => 'owner', - ]); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'uidOwner' => 'owner', - 'token' => 'token', - 'disabled' => false, - ]); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalk(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword(null) - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('password') - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->once())->method('verifyPassword')->with('password'); - $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share, 'password') - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'date' => $tomorrow, - 'uidOwner' => 'owner', - ]); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'uidOwner' => 'owner', - 'token' => 'token', - 'disabled' => false, - ]); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalkWithDifferentPassword(): void { - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('anotherPasswordHash') - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('password') - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->once())->method('verifyPassword')->with('password'); - $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share, 'password') - ->willReturn($share); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'date' => $tomorrow, - 'uidOwner' => 'owner', - ]); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->once())->method('post')->with([ - 'itemType' => 'file', - 'itemSource' => 100, - 'uidOwner' => 'owner', - 'token' => 'token', - 'disabled' => false, - ]); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalkWithNoPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot enable sending the password by Talk with an empty password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword(null) - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword(null) - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->never())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalkRemovingPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot enable sending the password by Talk with an empty password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('passwordHash') - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword(null) - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->once())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalkRemovingPasswordWithEmptyString(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot enable sending the password by Talk with an empty password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('passwordHash') - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('') - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->once())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot enable sending the password by Talk without setting a new password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('password') - ->setSendPasswordByTalk(false); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('password') - ->setSendPasswordByTalk(true) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->never())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot disable sending the password by Talk without setting a new password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('passwordHash') - ->setSendPasswordByTalk(true); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('passwordHash') - ->setSendPasswordByTalk(false) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->never())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testUpdateShareMailDisableSendPasswordByTalkWithoutChangingPassword(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot disable sending the password by Talk without setting a new password'); - - $manager = $this->createManagerMock() - ->onlyMethods([ - 'getShareById', - 'generalChecks', - 'verifyPassword', - 'pathCreateChecks', - 'validateExpirationDateLink', - ]) - ->getMock(); - - $originalShare = $this->manager->newShare(); - $originalShare->setShareType(IShare::TYPE_EMAIL) - ->setPermissions(Constants::PERMISSION_ALL) - ->setPassword('passwordHash') - ->setSendPasswordByTalk(true); - - $tomorrow = new \DateTime(); - $tomorrow->setTime(0, 0, 0); - $tomorrow->add(new \DateInterval('P1D')); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(100); - - $share = $this->manager->newShare(); - $share->setProviderId('foo') - ->setId('42') - ->setShareType(IShare::TYPE_EMAIL) - ->setToken('token') - ->setSharedBy('owner') - ->setShareOwner('owner') - ->setPassword('passwordHash') - ->setSendPasswordByTalk(false) - ->setExpirationDate($tomorrow) - ->setNode($file) - ->setPermissions(Constants::PERMISSION_ALL); - - $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalChecks')->with($share); - $manager->expects($this->never())->method('verifyPassword'); - $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->never())->method('validateExpirationDateLink'); - - $this->defaultProvider->expects($this->never()) - ->method('update'); - - $hookListener = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListener, 'post'); - $hookListener->expects($this->never())->method('post'); - - $hookListener2 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_password', $hookListener2, 'post'); - $hookListener2->expects($this->never())->method('post'); - - $hookListener3 = $this->createMock(DummyShareManagerListener::class); - Util::connectHook('OCP\Share', 'post_update_permissions', $hookListener3, 'post'); - $hookListener3->expects($this->never())->method('post'); - - $manager->updateShare($share); - } - - public function testMoveShareLink(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot change target of link share'); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_LINK); - - $this->manager->moveShare($share, 'recipient'); - } - - public function testMoveShareUserNotRecipient(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid share recipient'); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER); - - $share->setSharedWith('sharedWith'); - - $this->manager->moveShare($share, 'recipient'); - } - - public function testMoveShareUser(): void { - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_USER) - ->setId('42') - ->setProviderId('foo'); - $this->userManager->method('get') - ->with('recipient') - ->willReturn($this->createMock(IUser::class)); - - $share->setSharedWith('recipient'); - - $this->defaultProvider->method('move')->with($share, 'recipient')->willReturnArgument(0); - - $this->manager->moveShare($share, 'recipient'); - } - - public function testMoveShareGroupNotRecipient(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid share recipient'); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_GROUP); - - $sharedWith = $this->createMock(IGroup::class); - $share->setSharedWith('shareWith'); - - $recipient = $this->createMock(IUser::class); - $sharedWith->method('inGroup')->with($recipient)->willReturn(false); - - $this->groupManager->method('get')->with('shareWith')->willReturn($sharedWith); - $this->userManager->method('get')->with('recipient')->willReturn($recipient); - - $this->manager->moveShare($share, 'recipient'); - } - - public function testMoveShareGroupNull(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Group "shareWith" does not exist'); - - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_GROUP); - $share->setSharedWith('shareWith'); - - $recipient = $this->createMock(IUser::class); - - $this->groupManager->method('get')->with('shareWith')->willReturn(null); - $this->userManager->method('get')->with('recipient')->willReturn($recipient); - - $this->manager->moveShare($share, 'recipient'); - } - - public function testMoveShareGroup(): void { - $share = $this->manager->newShare(); - $share->setShareType(IShare::TYPE_GROUP) - ->setId('42') - ->setProviderId('foo'); - - $group = $this->createMock(IGroup::class); - $share->setSharedWith('group'); - - $recipient = $this->createMock(IUser::class); - $group->method('inGroup')->with($recipient)->willReturn(true); - - $this->groupManager->method('get')->with('group')->willReturn($group); - $this->userManager->method('get')->with('recipient')->willReturn($recipient); - - $this->defaultProvider->method('move')->with($share, 'recipient')->willReturnArgument(0); - - $this->manager->moveShare($share, 'recipient'); - } - - #[DataProvider('dataTestShareProviderExists')] - public function testShareProviderExists($shareType, $expected): void { - $factory = $this->getMockBuilder('OCP\Share\IProviderFactory')->getMock(); - $factory->expects($this->any())->method('getProviderForType') - ->willReturnCallback(function ($id) { - if ($id === IShare::TYPE_USER) { - return true; - } - throw new ProviderException(); - }); - - $manager = $this->createManager($factory); - $this->assertSame($expected, - $manager->shareProviderExists($shareType) - ); - } - - public static function dataTestShareProviderExists() { - return [ - [IShare::TYPE_USER, true], - [42, false], - ]; - } - - public function testGetSharesInFolder(): void { - $factory = new DummyFactory2($this->createMock(ContainerInterface::class)); - - $manager = $this->createManager($factory); - - $factory->setProvider($this->defaultProvider); - $extraProvider = $this->createMock(IShareProvider::class); - $factory->setSecondProvider($extraProvider); - - $share1 = $this->createMock(IShare::class); - $share2 = $this->createMock(IShare::class); - $share3 = $this->createMock(IShare::class); - $share4 = $this->createMock(IShare::class); - - $folder = $this->createMock(Folder::class); - - $this->defaultProvider->method('getSharesInFolder') - ->with( - $this->equalTo('user'), - $this->equalTo($folder), - $this->equalTo(false) - )->willReturn([ - 1 => [$share1], - 2 => [$share2], - ]); - - $extraProvider->method('getSharesInFolder') - ->with( - $this->equalTo('user'), - $this->equalTo($folder), - $this->equalTo(false) - )->willReturn([ - 2 => [$share3], - 3 => [$share4], - ]); - - $result = $manager->getSharesInFolder('user', $folder, false); - - $expects = [ - 1 => [$share1], - 2 => [$share2, $share3], - 3 => [$share4], - ]; - - $this->assertSame($expects, $result); - } - - public function testGetSharesInFolderOwnerless(): void { - $factory = new DummyFactory2($this->createMock(ContainerInterface::class)); - - $manager = $this->createManager($factory); - - $factory->setProvider($this->defaultProvider); - $extraProvider = $this->createMock(IShareProviderSupportsAllSharesInFolder::class); - $factory->setSecondProvider($extraProvider); - - $share1 = $this->createMock(IShare::class); - $share2 = $this->createMock(IShare::class); - - $mount = $this->createMock(IShareOwnerlessMount::class); - - $folder = $this->createMock(Folder::class); - $folder - ->method('getMountPoint') - ->willReturn($mount); - - $this->defaultProvider - ->method('getAllSharesInFolder') - ->with($folder) - ->willReturn([1 => [$share1]]); - - $extraProvider - ->method('getAllSharesInFolder') - ->with($folder) - ->willReturn([1 => [$share2]]); - - $this->assertSame([ - 1 => [$share1, $share2], - ], $manager->getSharesInFolder('user', $folder)); - } - - public function testGetAccessList(): void { - $factory = new DummyFactory2($this->createMock(ContainerInterface::class)); - - $manager = $this->createManager($factory); - - $factory->setProvider($this->defaultProvider); - $extraProvider = $this->createMock(IShareProvider::class); - $factory->setSecondProvider($extraProvider); - - $nodeOwner = $this->createMock(IUser::class); - $nodeOwner->expects($this->once()) - ->method('getUID') - ->willReturn('user1'); - - $node = $this->createMock(Node::class); - $node->expects($this->once()) - ->method('getOwner') - ->willReturn($nodeOwner); - $node->method('getId') - ->willReturn(42); - - $userFolder = $this->createMock(Folder::class); - $file = $this->createMock(File::class); - $folder = $this->createMock(Folder::class); - - $owner = $this->createMock(IUser::class); - $owner->expects($this->once()) - ->method('getUID') - ->willReturn('owner'); - - $file->method('getParent') - ->willReturn($folder); - $file->method('getPath') - ->willReturn('/owner/files/folder/file'); - $file->method('getOwner') - ->willReturn($owner); - $file->method('getId') - ->willReturn(23); - $folder->method('getParent') - ->willReturn($userFolder); - $folder->method('getPath') - ->willReturn('/owner/files/folder'); - $userFolder->method('getFirstNodeById') - ->with($this->equalTo(42)) - ->willReturn($file); - $userFolder->method('getPath') - ->willReturn('/user1/files'); - - $this->userManager->method('userExists') - ->with($this->equalTo('user1')) - ->willReturn(true); - - $this->defaultProvider->method('getAccessList') - ->with( - $this->equalTo([$file, $folder]), - false - ) - ->willReturn([ - 'users' => [ - 'user1', - 'user2', - 'user3', - '123456', - ], - 'public' => true, - ]); - - $extraProvider->method('getAccessList') - ->with( - $this->equalTo([$file, $folder]), - false - ) - ->willReturn([ - 'users' => [ - 'user3', - 'user4', - 'user5', - '234567', - ], - 'remote' => true, - ]); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('user1')) - ->willReturn($userFolder); - - $expected = [ - 'users' => ['owner', 'user1', 'user2', 'user3', '123456', 'user4', 'user5', '234567'], - 'remote' => true, - 'public' => true, - ]; - - $result = $manager->getAccessList($node, true, false); - - $this->assertSame($expected['public'], $result['public']); - $this->assertSame($expected['remote'], $result['remote']); - $this->assertSame($expected['users'], $result['users']); - } - - public function testGetAccessListWithCurrentAccess(): void { - $factory = new DummyFactory2($this->createMock(ContainerInterface::class)); - - $manager = $this->createManager($factory); - - $factory->setProvider($this->defaultProvider); - $extraProvider = $this->createMock(IShareProvider::class); - $factory->setSecondProvider($extraProvider); - - $nodeOwner = $this->createMock(IUser::class); - $nodeOwner->expects($this->once()) - ->method('getUID') - ->willReturn('user1'); - - $node = $this->createMock(Node::class); - $node->expects($this->once()) - ->method('getOwner') - ->willReturn($nodeOwner); - $node->method('getId') - ->willReturn(42); - - $userFolder = $this->createMock(Folder::class); - $file = $this->createMock(File::class); - - $owner = $this->createMock(IUser::class); - $owner->expects($this->once()) - ->method('getUID') - ->willReturn('owner'); - $folder = $this->createMock(Folder::class); - - $file->method('getParent') - ->willReturn($folder); - $file->method('getPath') - ->willReturn('/owner/files/folder/file'); - $file->method('getOwner') - ->willReturn($owner); - $file->method('getId') - ->willReturn(23); - $folder->method('getParent') - ->willReturn($userFolder); - $folder->method('getPath') - ->willReturn('/owner/files/folder'); - $userFolder->method('getFirstNodeById') - ->with($this->equalTo(42)) - ->willReturn($file); - $userFolder->method('getPath') - ->willReturn('/user1/files'); - - $this->userManager->method('userExists') - ->with($this->equalTo('user1')) - ->willReturn(true); - - $this->defaultProvider->method('getAccessList') - ->with( - $this->equalTo([$file, $folder]), - true - ) - ->willReturn([ - 'users' => [ - 'user1' => [], - 'user2' => [], - 'user3' => [], - '123456' => [], - ], - 'public' => true, - ]); - - $extraProvider->method('getAccessList') - ->with( - $this->equalTo([$file, $folder]), - true - ) - ->willReturn([ - 'users' => [ - 'user3' => [], - 'user4' => [], - 'user5' => [], - '234567' => [], - ], - 'remote' => [ - 'remote1', - ], - ]); - - $this->rootFolder->method('getUserFolder') - ->with($this->equalTo('user1')) - ->willReturn($userFolder); - - $expected = [ - 'users' => [ - 'owner' => [ - 'node_id' => 23, - 'node_path' => '/folder/file' - ] - , 'user1' => [], 'user2' => [], 'user3' => [], '123456' => [], 'user4' => [], 'user5' => [], '234567' => []], - 'remote' => [ - 'remote1', - ], - 'public' => true, - ]; - - $result = $manager->getAccessList($node, true, true); - - $this->assertSame($expected['public'], $result['public']); - $this->assertSame($expected['remote'], $result['remote']); - $this->assertSame($expected['users'], $result['users']); - } - - public function testGetAllShares(): void { - $factory = new DummyFactory2($this->createMock(ContainerInterface::class)); - - $manager = $this->createManager($factory); - - $factory->setProvider($this->defaultProvider); - $extraProvider = $this->createMock(IShareProvider::class); - $factory->setSecondProvider($extraProvider); - - $share1 = $this->createMock(IShare::class); - $share2 = $this->createMock(IShare::class); - $share3 = $this->createMock(IShare::class); - $share4 = $this->createMock(IShare::class); - - $this->defaultProvider->method('getAllShares') - ->willReturnCallback(function () use ($share1, $share2) { - yield $share1; - yield $share2; - }); - $extraProvider->method('getAllShares') - ->willReturnCallback(function () use ($share3, $share4) { - yield $share3; - yield $share4; - }); - - // "yield from", used in "getAllShares()", does not reset the keys, so - // "use_keys" has to be disabled to collect all the values while - // ignoring the keys returned by the generator. - $result = iterator_to_array($manager->getAllShares(), $use_keys = false); - - $expects = [$share1, $share2, $share3, $share4]; - - $this->assertSame($expects, $result); - } - - public static function dataCurrentUserCanEnumerateTargetUser(): array { - return [ - 'Full match guest' => [true, true, false, false, false, false, false, true], - 'Full match user' => [false, true, false, false, false, false, false, true], - 'Enumeration off guest' => [true, false, false, false, false, false, false, false], - 'Enumeration off user' => [false, false, false, false, false, false, false, false], - 'Enumeration guest' => [true, false, true, false, false, false, false, true], - 'Enumeration user' => [false, false, true, false, false, false, false, true], - - // Restricted enumerations guests never works - 'Guest phone' => [true, false, true, true, false, false, false, false], - 'Guest group' => [true, false, true, false, true, false, false, false], - 'Guest both' => [true, false, true, true, true, false, false, false], - - // Restricted enumerations users - 'User phone but not known' => [false, false, true, true, false, false, false, false], - 'User phone known' => [false, false, true, true, false, true, false, true], - 'User group but no match' => [false, false, true, false, true, false, false, false], - 'User group with match' => [false, false, true, false, true, false, true, true], - ]; - } - - /** - * @param bool $expected - */ - #[DataProvider('dataCurrentUserCanEnumerateTargetUser')] - public function testCurrentUserCanEnumerateTargetUser(bool $currentUserIsGuest, bool $allowEnumerationFullMatch, bool $allowEnumeration, bool $limitEnumerationToPhone, bool $limitEnumerationToGroups, bool $isKnownToUser, bool $haveCommonGroup, bool $expected): void { - /** @var IManager|MockObject $manager */ - $manager = $this->createManagerMock() - ->onlyMethods([ - 'allowEnumerationFullMatch', - 'allowEnumeration', - 'limitEnumerationToPhone', - 'limitEnumerationToGroups', - ]) - ->getMock(); - - $manager->method('allowEnumerationFullMatch') - ->willReturn($allowEnumerationFullMatch); - $manager->method('allowEnumeration') - ->willReturn($allowEnumeration); - $manager->method('limitEnumerationToPhone') - ->willReturn($limitEnumerationToPhone); - $manager->method('limitEnumerationToGroups') - ->willReturn($limitEnumerationToGroups); - - $this->knownUserService->method('isKnownToUser') - ->with('current', 'target') - ->willReturn($isKnownToUser); - - $currentUser = null; - if (!$currentUserIsGuest) { - $currentUser = $this->createMock(IUser::class); - $currentUser->method('getUID') - ->willReturn('current'); - } - $targetUser = $this->createMock(IUser::class); - $targetUser->method('getUID') - ->willReturn('target'); - - if ($haveCommonGroup) { - $this->groupManager->method('getUserGroupIds') - ->willReturnMap([ - [$targetUser, ['gid1', 'gid2']], - [$currentUser, ['gid2', 'gid3']], - ]); - } else { - $this->groupManager->method('getUserGroupIds') - ->willReturnMap([ - [$targetUser, ['gid1', 'gid2']], - [$currentUser, ['gid3', 'gid4']], - ]); - } - - $this->assertSame($expected, $manager->currentUserCanEnumerateTargetUser($currentUser, $targetUser)); - } -} - -class DummyFactory implements IProviderFactory { - /** @var IShareProvider */ - protected $provider; - - public function __construct(ContainerInterface $serverContainer) { - } - - /** - * @param IShareProvider $provider - */ - public function setProvider($provider) { - $this->provider = $provider; - } - - /** - * @param string $id - * @return IShareProvider - */ - #[\Override] - public function getProvider($id) { - return $this->provider; - } - - /** - * @param int $shareType - * @return IShareProvider - */ - #[\Override] - public function getProviderForType($shareType) { - return $this->provider; - } - - /** - * @return IShareProvider[] - */ - #[\Override] - public function getAllProviders() { - return [$this->provider]; - } - - #[\Override] - public function registerProvider(string $shareProvier): void { - } -} - -class DummyFactory2 extends DummyFactory { - /** @var IShareProvider */ - private $provider2; - - /** - * @param IShareProvider $provider - */ - public function setSecondProvider($provider) { - $this->provider2 = $provider; - } - - #[\Override] - public function getAllProviders() { - return [$this->provider, $this->provider2]; - } - - #[\Override] - public function registerProvider(string $shareProvier): void { - } -} --- a/tests/lib/Share20/ShareByMailProviderTest.php +++ /dev/null @@ -1,305 +0,0 @@ -dbConn = Server::get(IDBConnection::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->mailer = $this->createMock(IMailer::class); - $this->l10n = $this->createMock(IL10N::class); - $this->defaults = $this->getMockBuilder(Defaults::class)->disableOriginalConstructor()->getMock(); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->activityManager = $this->createMock(\OCP\Activity\IManager::class); - $this->settingsManager = $this->createMock(SettingsManager::class); - $this->hasher = $this->createMock(IHasher::class); - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->shareManager = $this->createMock(\OCP\Share\IManager::class); - $this->secureRandom = $this->createMock(ISecureRandom::class); - $this->config = $this->createMock(IConfig::class); - - // Empty share table - $this->dbConn->getQueryBuilder()->delete('share')->executeStatement(); - - $this->provider = new ShareByMailProvider( - $this->config, - $this->dbConn, - $this->secureRandom, - $this->userManager, - $this->rootFolder, - $this->l10n, - $this->logger, - $this->mailer, - $this->urlGenerator, - $this->activityManager, - $this->settingsManager, - $this->defaults, - $this->hasher, - $this->eventDispatcher, - $this->shareManager, - $this->getEmailValidatorWithStrictEmailCheck(), - ); - } - - #[\Override] - protected function tearDown(): void { - $this->dbConn->getQueryBuilder()->delete('share')->executeStatement(); - $this->dbConn->getQueryBuilder()->delete('filecache')->runAcrossAllShards()->executeStatement(); - $this->dbConn->getQueryBuilder()->delete('storages')->executeStatement(); - parent::tearDown(); - } - - /** - * @param int $shareType - * @param string $sharedWith - * @param string $sharedBy - * @param string $shareOwner - * @param string $itemType - * @param int $fileSource - * @param string $fileTarget - * @param int $permissions - * @param $token - * @param $expiration - * @param $parent - * @return int - * - * @throws \OCP\DB\Exception - */ - private function addShareToDB($shareType, $sharedWith, $sharedBy, $shareOwner, - $itemType, $fileSource, $fileTarget, $permissions, $token, $expiration, - $parent) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('share'); - - if ($shareType) { - $qb->setValue('share_type', $qb->expr()->literal($shareType)); - } - if ($sharedWith) { - $qb->setValue('share_with', $qb->expr()->literal($sharedWith)); - } - if ($sharedBy) { - $qb->setValue('uid_initiator', $qb->expr()->literal($sharedBy)); - } - if ($shareOwner) { - $qb->setValue('uid_owner', $qb->expr()->literal($shareOwner)); - } - if ($itemType) { - $qb->setValue('item_type', $qb->expr()->literal($itemType)); - } - if ($fileSource) { - $qb->setValue('file_source', $qb->expr()->literal($fileSource)); - } - if ($fileTarget) { - $qb->setValue('file_target', $qb->expr()->literal($fileTarget)); - } - if ($permissions) { - $qb->setValue('permissions', $qb->expr()->literal($permissions)); - } - if ($token) { - $qb->setValue('token', $qb->expr()->literal($token)); - } - if ($expiration) { - $qb->setValue('expiration', $qb->createNamedParameter($expiration, IQueryBuilder::PARAM_DATETIME_MUTABLE)); - } - if ($parent) { - $qb->setValue('parent', $qb->expr()->literal($parent)); - } - - $this->assertEquals(1, $qb->executeStatement()); - return $qb->getLastInsertId(); - } - - public function testGetSharesByWithResharesAndNoNode(): void { - $this->addShareToDB( - IShare::TYPE_EMAIL, - 'external.one@domain.tld', - 'user1', - 'user1', - 'folder', - 42, - null, - 17, - 'foobar', - null, - null, - ); - $this->addShareToDB( - IShare::TYPE_EMAIL, - 'external.two@domain.tld', - 'user2', - 'user2', - 'folder', - 42, - null, - 17, - 'barfoo', - null, - null, - ); - - // Return own shares only if not asked for a specific node - /** @var IShare[] $actual */ - $actual = $this->provider->getSharesBy( - 'user1', - IShare::TYPE_EMAIL, - null, - true, - -1, - 0, - ); - - $this->assertCount(1, $actual); - - $this->assertEquals(IShare::TYPE_EMAIL, $actual[0]->getShareType()); - $this->assertEquals('user1', $actual[0]->getSharedBy()); - $this->assertEquals('user1', $actual[0]->getShareOwner()); - $this->assertEquals('external.one@domain.tld', $actual[0]->getSharedWith()); - } - - public function testGetSharesByWithResharesAndNode(): void { - $this->addShareToDB( - IShare::TYPE_EMAIL, - 'external.one@domain.tld', - 'user1', - 'user1', - 'folder', - 42, - null, - 17, - 'foobar', - null, - null, - ); - $this->addShareToDB( - IShare::TYPE_EMAIL, - 'external.two@domain.tld', - 'user2', - 'user2', - 'folder', - 42, - null, - 17, - 'barfoo', - null, - null, - ); - - $node = $this->createMock(Node::class); - $node->expects($this->once()) - ->method('getId') - ->willReturn(42); - - // Return all shares if asked for specific node - /** @var IShare[] $actual */ - $actual = $this->provider->getSharesBy( - 'user1', - IShare::TYPE_EMAIL, - $node, - true, - -1, - 0, - ); - - $this->assertCount(2, $actual); - - $this->assertEquals(IShare::TYPE_EMAIL, $actual[0]->getShareType()); - $this->assertEquals('user1', $actual[0]->getSharedBy()); - $this->assertEquals('user1', $actual[0]->getShareOwner()); - $this->assertEquals('external.one@domain.tld', $actual[0]->getSharedWith()); - - $this->assertEquals(IShare::TYPE_EMAIL, $actual[1]->getShareType()); - $this->assertEquals('user2', $actual[1]->getSharedBy()); - $this->assertEquals('user2', $actual[1]->getShareOwner()); - $this->assertEquals('external.two@domain.tld', $actual[1]->getSharedWith()); - } -} --- a/tests/lib/Share20/ShareHelperTest.php +++ /dev/null @@ -1,214 +0,0 @@ -manager = $this->createMock(IManager::class); - - $this->helper = new ShareHelper($this->manager); - } - - public static function dataGetPathsForAccessList(): array { - return [ - [[], [], false, [], [], false, [ - 'users' => [], - 'remotes' => [], - ]], - [['user1', 'user2'], ['user1' => 'foo', 'user2' => 'bar'], true, [], [], false, [ - 'users' => ['user1' => 'foo', 'user2' => 'bar'], - 'remotes' => [], - ]], - [[], [], false, ['remote1', 'remote2'], ['remote1' => 'qwe', 'remote2' => 'rtz'], true, [ - 'users' => [], - 'remotes' => ['remote1' => 'qwe', 'remote2' => 'rtz'], - ]], - [['user1', 'user2'], ['user1' => 'foo', 'user2' => 'bar'], true, ['remote1', 'remote2'], ['remote1' => 'qwe', 'remote2' => 'rtz'], true, [ - 'users' => ['user1' => 'foo', 'user2' => 'bar'], - 'remotes' => ['remote1' => 'qwe', 'remote2' => 'rtz'], - ]], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetPathsForAccessList')] - public function testGetPathsForAccessList(array $userList, array $userMap, $resolveUsers, array $remoteList, array $remoteMap, $resolveRemotes, array $expected): void { - $this->manager->expects($this->once()) - ->method('getAccessList') - ->willReturn([ - 'users' => $userList, - 'remote' => $remoteList, - ]); - - /** @var Node|\PHPUnit\Framework\MockObject\MockObject $node */ - $node = $this->createMock(Node::class); - /** @var ShareHelper|\PHPUnit\Framework\MockObject\MockObject $helper */ - $helper = $this->getMockBuilder(ShareHelper::class) - ->setConstructorArgs([$this->manager]) - ->onlyMethods(['getPathsForUsers', 'getPathsForRemotes']) - ->getMock(); - - $helper->expects($resolveUsers ? $this->once() : $this->never()) - ->method('getPathsForUsers') - ->with($node, $userList) - ->willReturn($userMap); - - $helper->expects($resolveRemotes ? $this->once() : $this->never()) - ->method('getPathsForRemotes') - ->with($node, $remoteList) - ->willReturn($remoteMap); - - $this->assertSame($expected, $helper->getPathsForAccessList($node)); - } - - public static function dataGetPathsForUsers(): array { - return [ - [[], [23 => 'TwentyThree', 42 => 'FortyTwo'], []], - [ - [ - 'test1' => ['node_id' => 16, 'node_path' => '/foo'], - 'test2' => ['node_id' => 23, 'node_path' => '/bar'], - 'test3' => ['node_id' => 42, 'node_path' => '/cat'], - 'test4' => ['node_id' => 48, 'node_path' => '/dog'], - ], - [16 => 'SixTeen', 23 => 'TwentyThree', 42 => 'FortyTwo'], - [ - 'test1' => '/foo/TwentyThree/FortyTwo', - 'test2' => '/bar/FortyTwo', - 'test3' => '/cat', - ], - ], - ]; - } - - /** - * - * @param array $users - * @param array $nodes - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetPathsForUsers')] - public function testGetPathsForUsers(array $users, array $nodes, array $expected): void { - $lastNode = null; - foreach ($nodes as $nodeId => $nodeName) { - /** @var Node|\PHPUnit\Framework\MockObject\MockObject $node */ - $node = $this->createMock(Node::class); - $node->expects($this->any()) - ->method('getId') - ->willReturn($nodeId); - $node->expects($this->any()) - ->method('getName') - ->willReturn($nodeName); - if ($lastNode === null) { - $node->expects($this->any()) - ->method('getParent') - ->willThrowException(new NotFoundException()); - } else { - $node->expects($this->any()) - ->method('getParent') - ->willReturn($lastNode); - } - $lastNode = $node; - } - - $this->assertEquals($expected, self::invokePrivate($this->helper, 'getPathsForUsers', [$lastNode, $users])); - } - - public static function dataGetPathsForRemotes(): array { - return [ - [[], [23 => 'TwentyThree', 42 => 'FortyTwo'], []], - [ - [ - 'test1' => ['node_id' => 16, 'token' => 't1'], - 'test2' => ['node_id' => 23, 'token' => 't2'], - 'test3' => ['node_id' => 42, 'token' => 't3'], - 'test4' => ['node_id' => 48, 'token' => 't4'], - ], - [ - 16 => '/admin/files/SixTeen', - 23 => '/admin/files/SixTeen/TwentyThree', - 42 => '/admin/files/SixTeen/TwentyThree/FortyTwo', - ], - [ - 'test1' => ['token' => 't1', 'node_path' => '/SixTeen'], - 'test2' => ['token' => 't2', 'node_path' => '/SixTeen/TwentyThree'], - 'test3' => ['token' => 't3', 'node_path' => '/SixTeen/TwentyThree/FortyTwo'], - ], - ], - ]; - } - - /** - * - * @param array $remotes - * @param array $nodes - * @param array $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetPathsForRemotes')] - public function testGetPathsForRemotes(array $remotes, array $nodes, array $expected): void { - $lastNode = null; - foreach ($nodes as $nodeId => $nodePath) { - /** @var Node|\PHPUnit\Framework\MockObject\MockObject $node */ - $node = $this->createMock(Node::class); - $node->expects($this->any()) - ->method('getId') - ->willReturn($nodeId); - $node->expects($this->any()) - ->method('getPath') - ->willReturn($nodePath); - if ($lastNode === null) { - $node->expects($this->any()) - ->method('getParent') - ->willThrowException(new NotFoundException()); - } else { - $node->expects($this->any()) - ->method('getParent') - ->willReturn($lastNode); - } - $lastNode = $node; - } - - $this->assertEquals($expected, self::invokePrivate($this->helper, 'getPathsForRemotes', [$lastNode, $remotes])); - } - - public static function dataGetMountedPath(): array { - return [ - ['/admin/files/foobar', '/foobar'], - ['/admin/files/foo/bar', '/foo/bar'], - ]; - } - - /** - * @param string $path - * @param string $expected - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetMountedPath')] - public function testGetMountedPath($path, $expected): void { - /** @var Node|\PHPUnit\Framework\MockObject\MockObject $node */ - $node = $this->createMock(Node::class); - $node->expects($this->once()) - ->method('getPath') - ->willReturn($path); - - $this->assertSame($expected, self::invokePrivate($this->helper, 'getMountedPath', [$node])); - } -} --- a/tests/lib/Share20/ShareReview/Events/ShareReviewAccessCheckEventTest.php +++ /dev/null @@ -1,96 +0,0 @@ -makeEvent(); - - $this->assertFalse($event->isHandled()); - $this->assertFalse($event->isGranted()); - $this->assertNull($event->getReason()); - } - - public function testConstructorPayload(): void { - $event = new ShareReviewAccessCheckEvent('Deck', '99'); - - $this->assertSame('Deck', $event->getSourceName()); - $this->assertSame('99', $event->getShareId()); - } - - public function testGrantAccess(): void { - $event = $this->makeEvent(); - $event->grantAccess(); - - $this->assertTrue($event->isHandled()); - $this->assertTrue($event->isGranted()); - $this->assertNull($event->getReason()); - $this->assertFalse($event->isPropagationStopped()); - } - - public function testDenyAccess(): void { - $event = $this->makeEvent(); - $event->denyAccess('not in group'); - - $this->assertTrue($event->isHandled()); - $this->assertFalse($event->isGranted()); - $this->assertSame('not in group', $event->getReason()); - } - - public function testDenyStopsPropagation(): void { - $event = $this->makeEvent(); - $event->denyAccess('no access'); - - $this->assertTrue($event->isPropagationStopped()); - } - - public function testGrantDoesNotStopPropagation(): void { - $event = $this->makeEvent(); - $event->grantAccess(); - - $this->assertFalse($event->isPropagationStopped()); - } - - public function testGrantThenDenyIsDenied(): void { - $event = $this->makeEvent(); - $event->grantAccess(); - $event->denyAccess('revoked'); - - $this->assertFalse($event->isGranted()); - $this->assertSame('revoked', $event->getReason()); - $this->assertTrue($event->isPropagationStopped()); - } - - public function testDenyThenGrantRemainesDenied(): void { - $event = $this->makeEvent(); - $event->denyAccess('not allowed'); - $event->grantAccess(); // must be ignored — deny wins - - $this->assertFalse($event->isGranted()); - $this->assertSame('not allowed', $event->getReason()); - } - - public function testMultipleGrantsAreIdempotent(): void { - $event = $this->makeEvent(); - $event->grantAccess(); - $event->grantAccess(); - - $this->assertTrue($event->isGranted()); - $this->assertFalse($event->isPropagationStopped()); - } -} --- a/tests/lib/Share20/ShareReview/RegisterShareReviewSourceEventTest.php +++ /dev/null @@ -1,42 +0,0 @@ -assertSame([], $event->getSources()); - } - - public function testRegisterSource(): void { - $sourceClass = $this->createMock(IShareReviewSource::class)::class; - - $event = new RegisterShareReviewSourceEvent(); - $event->registerSource($sourceClass); - - $this->assertSame([$sourceClass], $event->getSources()); - } - - public function testRegisterSourceKeepsDuplicates(): void { - $sourceClass = $this->createMock(IShareReviewSource::class)::class; - - $event = new RegisterShareReviewSourceEvent(); - $event->registerSource($sourceClass); - $event->registerSource($sourceClass); - - $this->assertSame([$sourceClass, $sourceClass], $event->getSources()); - } -} --- a/tests/lib/Share20/ShareReview/ShareReviewEntryTest.php +++ /dev/null @@ -1,69 +0,0 @@ -assertSame('42', $entry->id); - $this->assertSame('Board "Roadmap"', $entry->object); - $this->assertSame('alice', $entry->initiator); - $this->assertSame(IShare::TYPE_USER, $entry->type); - $this->assertSame('bob', $entry->recipient); - $this->assertSame(1783764000, $entry->lastModifiedTimestamp); - $this->assertSame($permissions, $entry->permissions); - $this->assertSame('board-share-42', $entry->action); - $this->assertTrue($entry->hasPassword); - $this->assertSame(1785837600, $entry->expirationTimestamp); - $this->assertSame('23', $entry->parent); - } - - public function testDefaults(): void { - $entry = new ShareReviewEntry( - id: '42', - object: '/folder/file.txt', - initiator: 'alice', - type: IShare::TYPE_LINK, - recipient: 'sToKeN', - lastModifiedTimestamp: 0, - ); - - $this->assertSame(0, $entry->lastModifiedTimestamp); - $this->assertSame([], $entry->permissions); - $this->assertSame('', $entry->action); - $this->assertFalse($entry->hasPassword); - $this->assertNull($entry->expirationTimestamp); - $this->assertNull($entry->parent); - } -} --- a/tests/lib/Share20/ShareReview/ShareReviewPermissionTest.php +++ /dev/null @@ -1,47 +0,0 @@ -assertSame('deck:manage', $permission->id); - $this->assertSame('Manage board', $permission->displayName); - $this->assertSame('Administer participants and board settings', $permission->hint); - $this->assertSame(30, $permission->priority); - } - - public function testDefaults(): void { - $permission = new ShareReviewPermission(ShareReviewPermission::FILES_READ, 'Read'); - - $this->assertSame('files:read', $permission->id); - $this->assertSame('Read', $permission->displayName); - $this->assertNull($permission->hint); - $this->assertSame(50, $permission->priority); - } - - public function testFilesIdentifiers(): void { - $this->assertSame('files:read', ShareReviewPermission::FILES_READ); - $this->assertSame('files:update', ShareReviewPermission::FILES_UPDATE); - $this->assertSame('files:create', ShareReviewPermission::FILES_CREATE); - $this->assertSame('files:delete', ShareReviewPermission::FILES_DELETE); - $this->assertSame('files:reshare', ShareReviewPermission::FILES_RESHARE); - } -} --- a/tests/lib/Share20/ShareTest.php +++ /dev/null @@ -1,68 +0,0 @@ -rootFolder = $this->createMock(IRootFolder::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->share = new Share($this->rootFolder, $this->userManager); - } - - public function testSetIdInt(): void { - $this->share->setId(42); - $this->assertEquals('42', $this->share->getId()); - } - - public function testSetIdString(): void { - $this->share->setId('foo'); - $this->assertEquals('foo', $this->share->getId()); - } - - public function testSetIdOnce(): void { - $this->expectException(IllegalIDChangeException::class); - $this->expectExceptionMessage('Not allowed to assign a new internal id to a share'); - - $this->share->setId('foo'); - $this->share->setId('bar'); - } - - public function testSetProviderIdString(): void { - $this->share->setProviderId('foo'); - $this->share->setId('bar'); - $this->assertEquals('foo:bar', $this->share->getFullId()); - } - - public function testSetProviderIdOnce(): void { - $this->expectException(IllegalIDChangeException::class); - $this->expectExceptionMessage('Not allowed to assign a new provider id to a share'); - - $this->share->setProviderId('foo'); - $this->share->setProviderId('bar'); - } -} --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ /dev/null @@ -1,4897 +0,0 @@ -assertGreaterThanOrEqual(SharingManager::timeToMs($before), SharingManager::timeToMs($time)); - $this->assertLessThanOrEqual(SharingManager::timeToMs($after), SharingManager::timeToMs($time)); - } - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->dbConnection = Server::get(IDBConnection::class); - - $this->manager = Server::get(ISharingManager::class); - - $this->l10nFactory = Server::get(IFactory::class); - - $userManager = Server::get(IUserManager::class); - - $owner = $userManager->createUser('owner', 'password'); - $this->assertNotFalse($owner); - $this->owner = $owner; - $this->owner->setDisplayName('Owner'); - - $user1 = $userManager->createUser('user1', 'password'); - $this->assertNotFalse($user1); - $this->user1 = $user1; - $this->user1->setDisplayName('User 1'); - - $user2 = $userManager->createUser('user2', 'password'); - $this->assertNotFalse($user2); - $this->user2 = $user2; - $this->user2->setDisplayName('User 2'); - - $this->registry = Server::get(ISharingRegistry::class); - $this->registry->clear(); - - $this->shareSourceType1 = new TestShareSourceType1(['source1' => 'Source 1']); - $this->registry->registerSourceType($this->shareSourceType1); - $this->shareSourceType2 = new TestShareSourceType2(['source2' => 'Source 2']); - $this->registry->registerSourceType($this->shareSourceType2); - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1' => 'Recipient 1', - ], - [ - $this->user1->getUID() => ['recipient1'], - ], - [ - new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null), - ], - ) - ); - $this->registry->registerRecipientType( - new TestShareRecipientType2( - [ - 'recipient2' => 'Recipient 2', - 'recipient3' => 'Recipient 3', - ], - [ - $this->user2->getUID() => ['recipient2'], - ], - [ - new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null), - ], - ) - ); - $this->registry->registerPropertyType(new TestSharePropertyType1(['valid1'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyType1::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyType1::class, TestShareRecipientType1::class); - $this->registry->registerPropertyType(new TestSharePropertyType2(['valid2'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyType2::class, TestShareSourceType2::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyType2::class, TestShareRecipientType2::class); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset1()); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset2()); - $this->registry->registerPermissionType(TestShareSourceType1::class, new TestSharePermissionType1()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset1::class); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset2::class); - $this->registry->registerPermissionType(TestShareSourceType2::class, new TestSharePermissionType2()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType2::class, TestSharePermissionPreset2::class); - $this->registry->registerPermissionType(null, Server::get(ReshareSharePermissionType::class)); - } - - #[\Override] - protected function tearDown(): void { - $openTransaction = false; - if ($this->dbConnection->inTransaction()) { - $this->dbConnection->rollBack(); - $openTransaction = true; - } - - $accessContext = new ShareAccessContext(overrideChecks: true); - - $this->dbConnection->beginTransaction(); - - foreach ($this->manager->getShares($accessContext, null, null, null, null, null, null) as $share) { - $this->manager->deleteShare($accessContext, $share); - } - - $this->dbConnection->commit(); - - $this->owner->delete(); - $this->user1->delete(); - $this->user2->delete(); - - $this->registry->clear(); - - foreach ([ - 'sharing_share', - 'sharing_share_permissions', - 'sharing_share_properties', - 'sharing_share_recipients', - 'sharing_share_recipient_permissions', - 'sharing_share_sources', - 'sharing_share_user_status', - ] as $table) { - $qb = $this->dbConnection->getQueryBuilder(); - $qb - ->select($qb->func()->count('*')) - ->from($table); - $this->assertEquals(0, $qb->executeQuery()->fetchOne(), $table); - } - - parent::tearDown(); - - if ($openTransaction) { - $this->fail('Open transaction was not committed.'); - } - } - - private function reloadShare(ShareAccessContext $accessContext, Share $share): Share { - $this->dbConnection->beginTransaction(); - $share = $this->manager->getShare($accessContext, $share->id); - $this->dbConnection->commit(); - return $share; - } - - public function testSearchRecipients(): void { - $this->registry->clear(); - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1a' => 'Recipient 1A', - 'recipient1b' => 'Recipient 1B', - 'recipient1c' => 'Recipient 1C', - ], - [], - [ - new ShareRecipient(TestShareRecipientType1::class, 'recipient1a', null), - new ShareRecipient(TestShareRecipientType1::class, 'recipient1b', null), - new ShareRecipient(TestShareRecipientType1::class, 'recipient1c', null), - ], - ) - ); - $this->registry->registerRecipientType( - new TestShareRecipientType2( - [ - 'recipient2a' => 'Recipient 2A', - 'recipient2b' => 'Recipient 2B', - 'recipient2c' => 'Recipient 2C', - ], - [], - [ - new ShareRecipient(TestShareRecipientType2::class, 'recipient2a', null), - new ShareRecipient(TestShareRecipientType2::class, 'recipient2b', null), - new ShareRecipient(TestShareRecipientType2::class, 'recipient2c', null), - ], - ) - ); - - $accessContext = new ShareAccessContext($this->owner); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1a', - 'instance' => null, - 'display_name' => 'Recipient 1A', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1b', - 'instance' => null, - 'display_name' => 'Recipient 1B', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1c', - 'instance' => null, - 'display_name' => 'Recipient 1C', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2a', - 'instance' => null, - 'display_name' => 'Recipient 2A', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2b', - 'instance' => null, - 'display_name' => 'Recipient 2B', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2c', - 'instance' => null, - 'display_name' => 'Recipient 2C', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, null, 'recipient', 10, 0)); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1a', - 'instance' => null, - 'display_name' => 'Recipient 1A', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1b', - 'instance' => null, - 'display_name' => 'Recipient 1B', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1c', - 'instance' => null, - 'display_name' => 'Recipient 1C', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, [TestShareRecipientType1::class], 'recipient', 10, 0)); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1a', - 'instance' => null, - 'display_name' => 'Recipient 1A', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, [TestShareRecipientType1::class], 'recipient', 1, 0)); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1b', - 'instance' => null, - 'display_name' => 'Recipient 1B', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1c', - 'instance' => null, - 'display_name' => 'Recipient 1C', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, [TestShareRecipientType1::class], 'recipient', 10, 1)); - } - - public function testSearchRecipientsUniqueDisplayNames(): void { - $this->registry->clear(); - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1' => 'Recipient', - ], - [], - [ - new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null), - ], - ) - ); - $this->registry->registerRecipientType( - new TestShareRecipientType2( - [ - 'recipient2' => 'Recipient', - 'recipient3' => 'Other', - ], - [], - [ - new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null), - new ShareRecipient(TestShareRecipientType2::class, 'recipient3', null), - ], - ) - ); - - $accessContext = new ShareAccessContext($this->owner); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient (TestShareRecipientType1: recipient1)', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient (TestShareRecipientType2: recipient2)', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient3', - 'instance' => null, - 'display_name' => 'Other', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, null, 'recipient', 10, 0)); - } - - public function testSearchRecipientsIcons(): void { - $this->registry->clear(); - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'svg' => 'SVG', - 'url' => 'URL', - ], - [], - [ - new ShareRecipient(TestShareRecipientType1::class, 'svg', null), - new ShareRecipient(TestShareRecipientType1::class, 'url', null), - ], - ) - ); - - $accessContext = new ShareAccessContext($this->owner); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'svg', - 'instance' => null, - 'display_name' => 'SVG', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'url', - 'instance' => null, - 'display_name' => 'URL', - 'icon' => [ - 'light' => 'https://example.com/light.png', - 'dark' => 'https://example.com/dark.png', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, null, 'icon', 10, 0)); - } - - public function testSearchRecipientsOmitExisting(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->dbConnection->commit(); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, null, 'recipient', 3, 0, $share)); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->dbConnection->commit(); - - $this->assertEquals([ - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => null, - 'permissions' => [], - ], - ], $this->searchRecipients($accessContext, null, 'recipient', 3, 0, $share)); - } - - public function testCreateShare(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $share = $this->createShare($accessContext); - $after = $this->manager->getTime(); - unset($share['id']); - $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); - unset($share['last_updated']); - $this->assertEquals([ - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [], - 'recipients' => [], - 'properties' => [], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - ], - 'permission_preset' => null, - ], $share); - } - - /** - * @return list, list, list, list, ?string}> - */ - public static function dataProviderUpdateShareState(): array { - return [ - [ - [new ShareSource(TestShareSourceType1::class, 'source1')], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)], - [new ShareProperty(TestSharePropertyTypeRequired::class, 'valid1')], - [new SharePermission(ReshareSharePermissionType::class, true)], - null, - ], - [ - [], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)], - [], - [new SharePermission(ReshareSharePermissionType::class, true)], - 'You need to add at least one source to make the share available.', - ], - [ - [new ShareSource(TestShareSourceType1::class, 'source1')], - [], - [], - [new SharePermission(ReshareSharePermissionType::class, true)], - 'You need to add at least one recipient to make the share available.', - ], - [ - [new ShareSource(TestShareSourceType1::class, 'source1')], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)], - [new ShareProperty(TestSharePropertyTypeRequired::class, null)], - [new SharePermission(ReshareSharePermissionType::class, true)], - 'You need to set a value for the TestSharePropertyTypeRequired', - ], - [ - [new ShareSource(TestShareSourceType1::class, 'source1')], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)], - [new ShareProperty(TestSharePropertyTypeRequired::class, 'valid1')], - [new SharePermission(ReshareSharePermissionType::class, false)], - 'You need to allow at least one permission to make the share available.', - ], - ]; - } - - /** - * @param list $sources - * @param list $recipients - * @param list $properties - * @param list $permissions - */ - #[DataProvider('dataProviderUpdateShareState')] - public function testUpdateShareState(array $sources, array $recipients, array $properties, array $permissions, ?string $errorMessage): void { - $this->registry->registerPropertyType(new TestSharePropertyTypeRequired(['valid1'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeRequired::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeRequired::class, TestShareRecipientType1::class); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - foreach ($sources as $source) { - $share = $this->manager->addShareSource($accessContext, $share, $source); - } - - foreach ($recipients as $recipient) { - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient); - } - - foreach ($properties as $property) { - $share = $this->manager->updateShareProperty($accessContext, $share, $property); - } - - foreach ($permissions as $permission) { - $share = $this->manager->updateSharePermission($accessContext, $share, $permission); - } - - $this->dbConnection->commit(); - - if ($errorMessage !== null) { - try { - $this->updateShareState($accessContext, $share, ShareState::Active); - $this->fail('Allowed to set share state active.'); - } catch (HintException $exception) { - $this->assertEquals($errorMessage, $exception->getHint()); - } - } else { - $before = $this->manager->getTime(); - $formatted = $this->updateShareState($accessContext, $share, ShareState::Active); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Active->value, $formatted['state']); - } - } - - public function testUpdateShareUserStatus(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $this->assertEquals(null, $share->userStatus); - - try { - $this->updateShareUserStatus($accessContext, $share, ShareUserStatus::Accepted); - $this->fail('Not allowed.'); - } catch (HintException $hintException) { - $this->assertEquals('Cannot set user status for the owner of the share.', $hintException->getHint()); - } - - $accessContext2 = new ShareAccessContext($this->user1); - - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->getShare($accessContext2, $share->id); - $this->dbConnection->commit(); - $this->assertDateBetween($before, $after, $share->lastUpdated); - $this->assertEquals(ShareUserStatus::Pending, $share2->userStatus); - - foreach (ShareUserStatus::cases() as $case) { - $userStatus = ShareUserStatus::from($case->value); - - $formatted = $this->updateShareUserStatus($accessContext2, $share2, $userStatus); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals($userStatus->value, $formatted['user_status']); - } - } - - public function testAddShareSource(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], $formatted['sources']); - } - - public function testAddShareSourceInteractionRestricted(): void { - $listener = function (RestrictInteractionEvent $event): void { - foreach ($event->resources as $resource) { - if ($resource instanceof TestInteractionResource && $resource->getID() === 'source1') { - throw new InteractionRestrictedException('Source not allowed.', 'You are not allowed to add this source.'); - } - } - }; - $eventDispatcher = Server::get(IEventDispatcher::class); - $eventDispatcher->addListener(RestrictInteractionEvent::class, $listener); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->reloadShare($accessContext, $share); - $this->dbConnection->commit(); - - try { - $this->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $this->fail('Not restricted.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to add this source.', $hintException->getHint()); - } - - $eventDispatcher->removeListener(RestrictInteractionEvent::class, $listener); - } - - public function testRemoveShareSource(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType2::class, 'source2')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType2::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->removeShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Active->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source 2', - 'icon' => [ - 'svg' => '', - ], - ], - ], $formatted['sources']); - - $before = $this->manager->getTime(); - $formatted = $this->removeShareSource($accessContext, $share, new ShareSource(TestShareSourceType2::class, 'source2')); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([], $formatted['sources']); - } - - /** - * @return list - */ - public static function dataRecipient1(): array { - return [ - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null), 'Recipient 1'], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', 'https://example.com'), 'recipient1'], - ]; - } - - /** - * @return list - */ - public static function dataRecipient2(): array { - return [ - [new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null), 'Recipient 2'], - [new ShareRecipient(TestShareRecipientType2::class, 'recipient2', 'https://example.org'), 'recipient2'], - ]; - } - - /** - * @return list - */ - public static function dataRecipient12(): array { - return [ - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null), 'Recipient 1', new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null), 'Recipient 2'], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', 'https://example.com'), 'recipient1', new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null), 'Recipient 2'], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null), 'Recipient 1', new ShareRecipient(TestShareRecipientType2::class, 'recipient2', 'https://example.org'), 'recipient2'], - [new ShareRecipient(TestShareRecipientType1::class, 'recipient1', 'https://example.com'), 'recipient1', new ShareRecipient(TestShareRecipientType2::class, 'recipient2', 'https://example.org'), 'recipient2'], - ]; - } - - #[DataProvider(methodName: 'dataRecipient1')] - public function testAddShareRecipient(ShareRecipient $recipient1, string $recipient1DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->addShareRecipient($accessContext, $share, $recipient1); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => $recipient1->value, - 'instance' => $recipient1->instance, - 'display_name' => $recipient1DisplayName, - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - #[DataProvider(methodName: 'dataRecipient1')] - public function testAddShareRecipientInteractionRestricted(ShareRecipient $recipient1, string $recipient1DisplayName): void { - $listener = function (RestrictInteractionEvent $event): void { - foreach ($event->receivers as $receiver) { - if ($receiver instanceof TestInteractionReceiver && $receiver->getID() === 'recipient1') { - throw new InteractionRestrictedException('Recipient not allowed.', 'You are not allowed to add this recipient.'); - } - } - }; - $eventDispatcher = Server::get(IEventDispatcher::class); - $eventDispatcher->addListener(RestrictInteractionEvent::class, $listener); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - - $this->dbConnection->commit(); - - try { - $this->addShareRecipient($accessContext, $share, $recipient1); - $this->fail('Interaction not restricted.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to add this recipient.', $hintException->getHint()); - } - - $eventDispatcher->removeListener(RestrictInteractionEvent::class, $listener); - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testAddChildShareRecipientWithoutResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->addShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - $this->fail('Able to add child recipient without reshare permission.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testAddChildShareRecipientWithResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - $accessContext2 = new ShareAccessContext($this->user1); - - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->getShare($accessContext2, $share->id); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->addShareRecipient($accessContext2, $share2, $recipient2); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => $recipient2->value, - 'instance' => $recipient2->instance, - 'display_name' => $recipient2DisplayName, - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'user1', - 'instance' => null, - 'display_name' => 'User 1', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/user1/64', - 'dark' => 'http://localhost/index.php/avatar/user1/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - #[DataProvider(methodName: 'dataRecipient12')] - public function testRemoveShareRecipient(ShareRecipient $recipient1, string $recipient1DisplayName, ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient1); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient2); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->removeShareRecipient($accessContext, $share, $recipient1); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Active->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType2::class, - 'value' => $recipient2->value, - 'instance' => $recipient2->instance, - 'display_name' => $recipient2DisplayName, - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - - $before = $this->manager->getTime(); - $formatted = $this->removeShareRecipient($accessContext, $share, $recipient2); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([], $formatted['recipients']); - } - - public function testRemoveSelfShareRecipientWithoutResharePermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->fail('Able to remove self recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - public function testRemoveSelfShareRecipientWithResharePermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->fail('Able to remove self recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testRemoveChildShareRecipientWithoutResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, false)); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - $this->fail('Able to remove child recipient without reshare permission.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testRemoveChildShareRecipientWithResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - - $this->dbConnection->commit(); - - $accessContext2 = new ShareAccessContext($this->user1); - - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->getShare($accessContext2, $share->id); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->removeShareRecipient($accessContext2, $share2, $recipient2); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testRemoveSiblingShareRecipientWithoutResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient2); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - $this->fail('Able to remove sibling recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - #[DataProvider(methodName: 'dataRecipient2')] - public function testRemoveSiblingShareRecipientWithResharePermission(ShareRecipient $recipient2, string $recipient2DisplayName): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient2); - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user1), $share, $recipient2); - $this->fail('Able to remove sibling recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - public function testRemoveParentShareRecipientWithoutResharePermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient( - new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null) - ); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, false)); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user2), $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->fail('Able to remove parent recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - public function testRemoveParentShareRecipientWithResharePermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient( - new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null) - ); - - $this->dbConnection->commit(); - - try { - $this->removeShareRecipient(new ShareAccessContext($this->user2), $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->fail('Able to remove parent recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } - } - - /** - * @return list - */ - public static function dataUpdateShareRecipientSecret(): array { - return [ - [true], - [false], - ]; - } - - #[DataProvider('dataUpdateShareRecipientSecret')] - public function testUpdateShareRecipientSecret(bool $isSecretUpdatable): void { - $this->registry->registerRecipientType( - new TestShareRecipientTypePublicSecret( - [ - 'recipient1' => 'Recipient 1', - ], - [], - true, - $isSecretUpdatable, - ) - ); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $recipient = new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient1', null); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient); - $this->dbConnection->commit(); - - if (!$isSecretUpdatable) { - try { - $this->updateShareRecipientSecret($accessContext, $share, $recipient, 'mysecret'); - $this->fail('Able to update recipient secret.'); - } catch (HintException $exception) { - $this->assertEquals('You are not allowed to edit this share.', $exception->getHint()); - } - } else { - $before = $this->manager->getTime(); - $formatted = $this->updateShareRecipientSecret($accessContext, $share, $recipient, 'mysecret'); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - [ - 'class' => TestShareRecipientTypePublicSecret::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '' - ], - 'secret' => [ - 'updatable' => true, - 'value' => 'mysecret', - 'url' => 'http://localhost/index.php/s/mysecret', - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - } - - /** - * @return list}> - */ - public static function dataProviderUpdateShareProperty(): array { - return [ - [[null, 'valid1']], - [['valid1', null]], - ]; - } - - /** - * @param list $values - */ - #[DataProvider('dataProviderUpdateShareProperty')] - public function testUpdateShareProperty(array $values): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->getShare($accessContext, $share->id); - - $this->dbConnection->commit(); - - foreach ($values as $value) { - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyType1::class, $value)); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => $value, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], $formatted['properties']); - } - } - - public function testUpdateSharePropertyRequired(): void { - $this->registry->registerPropertyType(new TestSharePropertyTypeRequired(['valid1', 'valid2'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeRequired::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeRequired::class, TestShareRecipientType1::class); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->getShare($accessContext, $share->id); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid1')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeRequired::class, - 'display_name' => 'TestSharePropertyTypeRequired', - 'hint' => 'hint TestSharePropertyTypeRequired', - 'priority' => 1, - 'advanced' => false, - 'required' => true, - 'value' => 'valid1', - 'type' => 'enum', - 'valid_values' => ['valid1', 'valid2'], - ], - ], $formatted['properties']); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid2')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Active->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeRequired::class, - 'display_name' => 'TestSharePropertyTypeRequired', - 'hint' => 'hint TestSharePropertyTypeRequired', - 'priority' => 1, - 'advanced' => false, - 'required' => true, - 'value' => 'valid2', - 'type' => 'enum', - 'valid_values' => ['valid1', 'valid2'], - ], - ], $formatted['properties']); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeRequired::class, null)); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeRequired::class, - 'display_name' => 'TestSharePropertyTypeRequired', - 'hint' => 'hint TestSharePropertyTypeRequired', - 'priority' => 1, - 'advanced' => false, - 'required' => true, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1', 'valid2'], - ], - ], $formatted['properties']); - } - - public function testUpdateSharePropertyModifyProperties(): void { - $this->registry->registerPropertyType( - new TestSharePropertyTypeModifyValue(['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load']) - ); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeModifyValue::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeModifyValue::class, TestShareRecipientType1::class); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $this->dbConnection->commit(); - - // We cannot test for modify-on-save, because we will always see modified-on-save as the returned value. - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeModifyValue::class, - 'display_name' => 'TestSharePropertyTypeModifyValue', - 'hint' => 'hint TestSharePropertyTypeModifyValue', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'modified-on-save', - 'type' => 'enum', - 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'], - ], - ], $formatted['properties']); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'old-value')); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save-old-value')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeModifyValue::class, - 'display_name' => 'TestSharePropertyTypeModifyValue', - 'hint' => 'hint TestSharePropertyTypeModifyValue', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'old-value', - 'type' => 'enum', - 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'], - ], - ], $formatted['properties']); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeModifyValue::class, - 'display_name' => 'TestSharePropertyTypeModifyValue', - 'hint' => 'hint TestSharePropertyTypeModifyValue', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'modified-on-save', - 'type' => 'enum', - 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'], - ], - ], $formatted['properties']); - - $before = $this->manager->getTime(); - $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-load')); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeModifyValue::class, - 'display_name' => 'TestSharePropertyTypeModifyValue', - 'hint' => 'hint TestSharePropertyTypeModifyValue', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'modified-on-load', - 'type' => 'enum', - 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'], - ], - ], $formatted['properties']); - } - - public function testUpdateSharePermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->getShare($accessContext, $share->id); - - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $this->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->reloadShare($accessContext, $share); - $formatted = $this->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => true, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $this->dbConnection->commit(); - - $before = $this->manager->getTime(); - $formatted = $this->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, false)); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Active->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(ShareState::Draft->value, $formatted['state']); - $this->assertEquals([ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - } - - public function testUpdateSharePermissionInteractionRestricted(): void { - $listener = function (RestrictInteractionEvent $event): void { - if ($event->action instanceof ShareAction && $event->action->unifiedSharingPermissions !== null && in_array( - TestSharePermissionType1::class, $event->action->unifiedSharingPermissions, true - )) { - throw new InteractionRestrictedException('Permission not allowed.', 'You are not allowed to enable this permission.'); - } - }; - $eventDispatcher = Server::get(IEventDispatcher::class); - $eventDispatcher->addListener(RestrictInteractionEvent::class, $listener); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $this->dbConnection->commit(); - - try { - $this->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $this->fail('Not restricted.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to enable this permission.', $hintException->getHint()); - } - - $eventDispatcher->removeListener(RestrictInteractionEvent::class, $listener); - } - - public function testUpdateShareRecipientPermission(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - $accessContext1 = new ShareAccessContext($this->user1); - $recipient2 = new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null); - $this->dbConnection->beginTransaction(); - $share1 = $this->manager->getShare($accessContext1, $share->id); - $share1 = $this->manager->addShareRecipient($accessContext1, $share1, $recipient2); - - $this->dbConnection->commit(); - $formatted = $this->updateShareRecipientPermission( - $accessContext1, $share1, $recipient2, new SharePermission(ReshareSharePermissionType::class, false) - ); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'user1', - 'instance' => null, - 'display_name' => 'User 1', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/user1/64', - 'dark' => 'http://localhost/index.php/avatar/user1/64/dark', - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - ], - ], - ], $formatted['recipients']); - - $accessContext2 = new ShareAccessContext($this->user2); - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->getShare($accessContext2, $share->id); - try { - $this->manager->addShareRecipient($accessContext2, $share2, new ShareRecipient(TestShareRecipientType2::class, 'recipient3', null)); - $this->fail('Adding share recipient without reshare permission should fail.'); - } catch (HintException $hintException) { - $this->assertEquals('You are not allowed to edit this share.', $hintException->getHint()); - } finally { - $this->dbConnection->commit(); - } - - $formatted = $this->updateShareRecipientPermission($accessContext1, $share1, $recipient2, new SharePermission(ReshareSharePermissionType::class, true)); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'user1', - 'instance' => null, - 'display_name' => 'User 1', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/user1/64', - 'dark' => 'http://localhost/index.php/avatar/user1/64/dark', - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => true, - 'priority' => 90, - ], - ], - ], - ], $formatted['recipients']); - - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->getShare($accessContext2, $share->id); - $this->manager->addShareRecipient($accessContext2, $share2, new ShareRecipient(TestShareRecipientType2::class, 'recipient3', null)); - $this->dbConnection->commit(); - } - - public function testSelectSharePermissionPreset(): void { - $this->registry->clear(); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset1()); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset2()); - $this->registry->registerPermissionType(null, new TestSharePermissionType1()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset1::class); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset2::class); - $this->registry->registerPermissionType(null, new TestSharePermissionType2()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType2::class, TestSharePermissionPreset2::class); - $this->registry->registerPermissionType(null, new TestSharePermissionType3()); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->getShare($accessContext, $share->id); - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertNull($formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType3::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType3', - 'hint' => 'hint TestSharePermissionType3', - 'presets' => [], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->selectSharePermissionPreset($accessContext, $share, TestSharePermissionPreset2::class); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(TestSharePermissionPreset2::class, $formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType3::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType3', - 'hint' => 'hint TestSharePermissionType3', - 'presets' => [], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType3::class, true)); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertNull($formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType3::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType3', - 'hint' => 'hint TestSharePermissionType3', - 'presets' => [], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->selectSharePermissionPreset($accessContext, $share, TestSharePermissionPreset1::class); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(TestSharePermissionPreset1::class, $formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType3::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType3', - 'hint' => 'hint TestSharePermissionType3', - 'presets' => [], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertNull($formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType3::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType3', - 'hint' => 'hint TestSharePermissionType3', - 'presets' => [], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - } - - public function testSelectSharePermissionPresetCompatible(): void { - $this->registry->clear(); - $this->registry->registerSourceType(new TestShareSourceType1(['source1' => 'Source 1'])); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset1()); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset2()); - $this->registry->registerPermissionType(TestShareSourceType1::class, new TestSharePermissionType1()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset1::class); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset2::class); - $this->registry->registerPermissionType(null, new TestSharePermissionType2()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType2::class, TestSharePermissionPreset2::class); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->getShare($accessContext, $share->id); - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertNull($formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->selectSharePermissionPreset($accessContext, $share, TestSharePermissionPreset2::class); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(TestSharePermissionPreset2::class, $formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->reloadShare($accessContext, $share); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertNull($formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - - $before = $this->manager->getTime(); - $formatted = $this->selectSharePermissionPreset($accessContext, $share, TestSharePermissionPreset2::class); - $after = $this->manager->getTime(); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals(TestSharePermissionPreset2::class, $formatted['permission_preset']); - $this->assertEquals([ - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => null, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], $formatted['permissions']); - } - - public function testDeleteShare(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - - $this->deleteShare($accessContext, $share); - - try { - $this->manager->getShare(new ShareAccessContext(overrideChecks: true), $share->id); - $this->fail('Share not deleted.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } finally { - $this->dbConnection->commit(); - } - } - - public function testGetShare(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->manager->getShare($accessContext, $share->id); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => false, - 'priority' => 1, - ], - ], - 'permission_preset' => null, - ], $formatted); - } - - public function testGetShareAsRecipientNotActive(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $this->dbConnection->commit(); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->fail('Draft share visible.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareAsRecipientActive(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => ShareUserStatus::Pending->value, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - } - - public function testGetShareAsRecipientWithArguments(): void { - $this->registry->registerRecipientType(new TestShareRecipientTypeArguments()); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientTypeArguments::class, 'secret', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->user1, arguments: [TestShareRecipientTypeArguments::class => 'secret']), - $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => ShareUserStatus::Pending->value, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientTypeArguments::class, - 'value' => 'secret', - 'instance' => null, - 'display_name' => 'secret', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/guest/secret/64', - 'dark' => 'http://localhost/index.php/avatar/guest/secret/64?darkTheme=1', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->fail('Share visible without arguments.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareWithSecretNotActive(): void { - $this->registry->registerRecipientType(new TestShareRecipientTypeArguments()); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientTypeArguments::class, 'secret', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - - $this->dbConnection->commit(); - $secret = $share->recipients[0]->secret; - $this->assertNotNull($secret); - - try { - $this->getShare(new ShareAccessContext(secret: $secret), $share->id); - $this->fail('Draft share visible with secret.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareWithSecretActive(): void { - $this->registry->registerRecipientType(new TestShareRecipientTypeArguments()); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientTypeArguments::class, 'secret', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $after = $this->manager->getTime(); - - $this->dbConnection->commit(); - $secret = $share->recipients[0]->secret; - $this->assertNotNull($secret); - - $formatted = $this->getShare(new ShareAccessContext(secret: $secret), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientTypeArguments::class, - 'value' => 'secret', - 'instance' => null, - 'display_name' => 'secret', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/guest/secret/64', - 'dark' => 'http://localhost/index.php/avatar/guest/secret/64?darkTheme=1', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - try { - $this->getShare(new ShareAccessContext(), $share->id); - $this->fail('Share visible without secret.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareAsNonRecipient(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->fail('Share visible as non-recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareAsRecipientFilteredProperties(): void { - $this->registry->registerPropertyType(new TestSharePropertyTypeFilter(['visible', 'filtered'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeFilter::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeFilter::class, TestShareRecipientType1::class); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeFilter::class, 'visible')); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => ShareUserStatus::Pending->value, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeFilter::class, - 'display_name' => 'TestSharePropertyTypeFilter', - 'hint' => 'hint TestSharePropertyTypeFilter', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'visible', - 'type' => 'enum', - 'valid_values' => ['visible', 'filtered'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $this->manager->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeFilter::class, 'filtered')); - $share = $this->reloadShare($accessContext, $share); - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->owner), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeFilter::class, - 'display_name' => 'TestSharePropertyTypeFilter', - 'hint' => 'hint TestSharePropertyTypeFilter', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => 'filtered', - 'type' => 'enum', - 'valid_values' => ['visible', 'filtered'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->fail('Share visible with active filter property.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testGetShareAsRecipientFilteredArguments(): void { - $this->registry->registerPropertyType(new TestSharePropertyTypeFilter(['visible', 'filtered'])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeFilter::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeFilter::class, TestShareRecipientType1::class); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => ShareUserStatus::Pending->value, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeFilter::class, - 'display_name' => 'TestSharePropertyTypeFilter', - 'hint' => 'hint TestSharePropertyTypeFilter', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['visible', 'filtered'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - $formatted = $this->getShare(new ShareAccessContext(currentUser: $this->owner, arguments: [TestSharePropertyTypeFilter::class => 'filtered']), - $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertEquals([ - 'id' => $share->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - [ - 'class' => TestSharePropertyTypeFilter::class, - 'display_name' => 'TestSharePropertyTypeFilter', - 'hint' => 'hint TestSharePropertyTypeFilter', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['visible', 'filtered'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], $formatted); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1, arguments: [TestSharePropertyTypeFilter::class => 'filtered']), $share->id); - $this->fail('Share visible with filtered value as recipient.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - /** - * @return list - */ - public static function dataGetShareWithPublicSecret(): array { - return [ - [true], - [false], - ]; - } - - #[DataProvider('dataGetShareWithPublicSecret')] - public function testGetShareWithPublicSecret(bool $isSecretPublic): void { - $this->registry->clear(); - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1' => 'Recipient 1', - ], - [], - [], - ) - ); - $this->registry->registerRecipientType( - new TestShareRecipientTypePublicSecret( - [ - 'recipient2' => 'Recipient 2', - ], - [], - $isSecretPublic, - false, - ) - ); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient2', null)); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - unset($formatted['last_updated']); - $this->assertIsList($formatted['recipients']); - $this->assertCount(2, $formatted['recipients']); - $this->assertEquals([ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], $formatted['recipients'][0]); - $this->assertIsArray($formatted['recipients'][1]); - if ($isSecretPublic) { - $this->assertArrayHasKey('secret', $formatted['recipients'][1]); - $this->assertIsArray($formatted['recipients'][1]['secret']); - - $this->assertArrayHasKey('updatable', $formatted['recipients'][1]['secret']); - $this->assertFalse($formatted['recipients'][1]['secret']['updatable']); - - $this->assertArrayHasKey('value', $formatted['recipients'][1]['secret']); - $this->assertIsString($formatted['recipients'][1]['secret']['value']); - $this->assertNotEmpty($formatted['recipients'][1]['secret']['value']); - - $this->assertArrayHasKey('url', $formatted['recipients'][1]['secret']); - $this->assertIsString($formatted['recipients'][1]['secret']['url']); - $this->assertMatchesRegularExpression('/http:\/\/localhost\/index\.php\/s\/.+/', $formatted['recipients'][1]['secret']['url']); - } else { - $this->assertArrayNotHasKey('url', $formatted['recipients'][1]); - } - } - - public function testGetShareWithSecret(): void { - $this->registry->clear(); - $this->registry->registerSourceType(new TestShareSourceType1(['source1' => 'Source'])); - $this->registry->registerRecipientType( - new TestShareRecipientTypePublicSecret( - [ - 'recipient1' => 'Recipient 1', - 'recipient2' => 'Recipient 2', - 'recipient3' => 'Recipient 3', - 'recipient4' => 'Recipient 4', - ], - [ - $this->user1->getUID() => ['recipient1'], - $this->user2->getUID() => ['recipient2'], - ], - true, - false, - ) - ); - $this->registry->registerPermissionType(null, Server::get(ReshareSharePermissionType::class)); - - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient( - new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient2', null) - ); - $share = $this->manager->addShareRecipient( - new ShareAccessContext($this->user1), $share, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient3', null) - ); - $share = $this->manager->addShareRecipient( - new ShareAccessContext($this->user2), $share, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient4', null) - ); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $formatted = $this->getShare(new ShareAccessContext($this->user2), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - - $this->assertArrayHasKey('recipients', $formatted); - $this->assertIsArray($formatted['recipients']); - $this->assertCount(2, $formatted['recipients']); - - // Parent is not visible - - // Self - $this->assertIsArray($formatted['recipients'][0]); - $this->assertArrayHasKey('value', $formatted['recipients'][0]); - $this->assertEquals('recipient2', $formatted['recipients'][0]['value']); - $this->assertArrayHasKey('secret', $formatted['recipients'][0]); - $this->assertIsArray($formatted['recipients'][0]['secret']); - $this->assertNotEmpty($formatted['recipients'][0]['secret']['value']); - - // Sibling is not visible - - // Child - $this->assertIsArray($formatted['recipients'][1]); - $this->assertArrayHasKey('value', $formatted['recipients'][1]); - $this->assertEquals('recipient4', $formatted['recipients'][1]['value']); - $this->assertArrayHasKey('secret', $formatted['recipients'][1]); - $this->assertIsArray($formatted['recipients'][1]['secret']); - $this->assertNotEmpty($formatted['recipients'][1]['secret']['value']); - } - - public function testGetShareUniqueDisplayNames(): void { - $this->registry->clear(); - $this->registry->registerSourceType(new TestShareSourceType1(['source1' => 'Source'])); - $this->registry->registerSourceType(new TestShareSourceType2(['source2' => 'Source', 'source3' => 'Other'])); - $this->registry->registerRecipientType(new TestShareRecipientType1(['recipient1' => 'Recipient'], [], [])); - $this->registry->registerRecipientType(new TestShareRecipientType2(['recipient2' => 'Recipient', 'recipient3' => 'Other'], [], [])); - - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType2::class, 'source2')); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType2::class, 'source3')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient3', null)); - - $this->dbConnection->commit(); - - $formatted = $this->getShare($accessContext, $share->id); - - $this->assertEquals([ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source (TestShareSourceType1: source1)', - 'icon' => [ - 'svg' => '', - ], - ], - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source (TestShareSourceType2: source2)', - 'icon' => [ - 'svg' => '', - ], - ], - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source3', - 'display_name' => 'Other', - 'icon' => [ - 'svg' => '', - ], - ], - ], $formatted['sources']); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient (TestShareRecipientType1: recipient1)', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient (TestShareRecipientType2: recipient2)', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient3', - 'instance' => null, - 'display_name' => 'Other', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - public function testGetShareDisabledOwner(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $this->owner->setEnabled(false); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user1), $share->id); - $this->fail('Share still visible.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - - $formatted = $this->getShare(new ShareAccessContext(overrideChecks: true), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], $formatted['owner']); - } - - public function testGetShareDisabledInitiator(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient( - new ShareAccessContext(currentUser: $this->user1), $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null) - ); - - $this->dbConnection->commit(); - $after = $this->manager->getTime(); - - $this->user1->setEnabled(false); - - try { - $this->getShare(new ShareAccessContext(currentUser: $this->user2), $share->id); - $this->fail('Share still visible.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - - $formatted = $this->getShare($accessContext, $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'user1', - 'instance' => null, - 'display_name' => 'User 1', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/user1/64', - 'dark' => 'http://localhost/index.php/avatar/user1/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - public function testGetShares(): void { - $accessContext = new ShareAccessContext($this->owner); - - $before1 = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share1 = $this->manager->createShare($accessContext); - $share1 = $this->manager->addShareSource($accessContext, $share1, new ShareSource(TestShareSourceType1::class, 'source1')); - $share1 = $this->manager->addShareRecipient($accessContext, $share1, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share1 = $this->manager->updateSharePermission($accessContext, $share1, new SharePermission(TestSharePermissionType1::class, true)); - - $this->dbConnection->commit(); - $after1 = $this->manager->getTime(); - - $before2 = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share2 = $this->manager->createShare($accessContext); - $share2 = $this->manager->addShareSource($accessContext, $share2, new ShareSource(TestShareSourceType2::class, 'source2')); - $share2 = $this->manager->addShareRecipient($accessContext, $share2, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $share2 = $this->manager->updateSharePermission($accessContext, $share2, new SharePermission(TestSharePermissionType2::class, true)); - $share2 = $this->manager->updateShareState($accessContext, $share2, ShareState::Active); - - $this->dbConnection->commit(); - $after2 = $this->manager->getTime(); - - $accessContext2 = new ShareAccessContext($this->user2); - $this->dbConnection->beginTransaction(); - $this->manager->updateShareUserStatus($accessContext2, $this->manager->getShare($accessContext2, $share2->id), ShareUserStatus::Accepted); - $this->dbConnection->commit(); - - $formatted = $this->getShares($accessContext, null, null, null, null, null, null); - $this->assertCount(2, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before1, $after1, $this->parseTime($formatted[0]['last_updated'])); - $this->assertIsArray($formatted[1]); - $this->assertDateBetween($before2, $after2, $this->parseTime($formatted[1]['last_updated'])); - unset($formatted[0]['last_updated'], $formatted[1]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share1->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], - [ - 'id' => $share2->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source 2', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType2::class, - 'display_name' => 'TestSharePropertyType2', - 'hint' => 'hint TestSharePropertyType2', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid2'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => TestShareSourceType2::class, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset2::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext, TestShareSourceType1::class, null, null, null, null, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before1, $after1, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share1->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext, TestShareSourceType1::class, 'source1', null, null, null, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before1, $after1, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share1->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext, TestShareSourceType1::class, 'non-existent', null, null, null, null); - $this->assertCount(0, $formatted); - - $formatted = $this->getShares($accessContext, null, null, ShareState::Draft, null, null, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before1, $after1, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share1->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext, null, null, ShareState::Active, null, null, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before2, $after2, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share2->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source 2', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType2::class, - 'display_name' => 'TestSharePropertyType2', - 'hint' => 'hint TestSharePropertyType2', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid2'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => TestShareSourceType2::class, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset2::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext2, null, null, null, ShareUserStatus::Pending, null, null); - $this->assertCount(0, $formatted); - - $formatted = $this->getShares($accessContext2, null, null, null, ShareUserStatus::Accepted, null, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before2, $after2, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share2->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => ShareUserStatus::Accepted->value, - 'sources' => [ - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source 2', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType2::class, - 'display_name' => 'TestSharePropertyType2', - 'hint' => 'hint TestSharePropertyType2', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid2'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => TestShareSourceType2::class, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset2::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext2, null, null, null, ShareUserStatus::Rejected, null, null); - $this->assertCount(0, $formatted); - - $formatted = $this->getShares($accessContext, null, null, null, null, $share1->id, null); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before2, $after2, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share2->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Active->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType2::class, - 'value' => 'source2', - 'display_name' => 'Source 2', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType2::class, - 'display_name' => 'TestSharePropertyType2', - 'hint' => 'hint TestSharePropertyType2', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid2'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType2::class, - 'source_class' => TestShareSourceType2::class, - 'display_name' => 'TestSharePermissionType2', - 'hint' => 'hint TestSharePermissionType2', - 'presets' => [TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset2::class, - ], - ], $formatted); - - $formatted = $this->getShares($accessContext, null, null, null, null, null, 1); - $this->assertCount(1, $formatted); - $this->assertIsArray($formatted[0]); - $this->assertDateBetween($before1, $after1, $this->parseTime($formatted[0]['last_updated'])); - unset($formatted[0]['last_updated']); - $this->assertEquals([ - [ - 'id' => $share1->id, - 'owner' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'state' => ShareState::Draft->value, - 'user_status' => null, - 'sources' => [ - [ - 'class' => TestShareSourceType1::class, - 'value' => 'source1', - 'display_name' => 'Source 1', - 'icon' => [ - 'svg' => '', - ], - ], - ], - 'recipients' => [ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], - 'properties' => [ - [ - 'class' => TestSharePropertyType1::class, - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'advanced' => false, - 'required' => false, - 'value' => null, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ], - ], - 'permissions' => [ - [ - 'class' => ReshareSharePermissionType::class, - 'source_class' => null, - 'display_name' => 'Share with others', - 'hint' => null, - 'presets' => [], - 'enabled' => false, - 'priority' => 90, - ], - [ - 'class' => TestSharePermissionType1::class, - 'source_class' => TestShareSourceType1::class, - 'display_name' => 'TestSharePermissionType1', - 'hint' => 'hint TestSharePermissionType1', - 'presets' => [TestSharePermissionPreset1::class, TestSharePermissionPreset2::class], - 'enabled' => true, - 'priority' => 1, - ], - ], - 'permission_preset' => TestSharePermissionPreset1::class, - ], - ], $formatted); - } - - public function testGetSharesSorted(): void { - $accessContext = new ShareAccessContext(currentUser: $this->owner); - - $this->dbConnection->beginTransaction(); - $share1 = $this->manager->createShare($accessContext); - $share2 = $this->manager->createShare($accessContext); - $this->dbConnection->commit(); - - $shares = $this->getShares($accessContext, null, null, null, null, null, null); - $this->assertIsArray($shares[0]); - $this->assertArrayHasKey('id', $shares[0]); - $this->assertIsArray($shares[1]); - $this->assertArrayHasKey('id', $shares[1]); - $this->assertEquals($share1->id, $shares[0]['id']); - $this->assertEquals($share2->id, $shares[1]['id']); - - $this->dbConnection->beginTransaction(); - $this->manager->addShareSource($accessContext, $share2, new ShareSource(TestShareSourceType2::class, 'source2')); - $this->manager->getShare($accessContext, $share2->id); - $this->manager->updateSharePermission($accessContext, $share2, new SharePermission(TestSharePermissionType2::class, true)); - - $this->dbConnection->commit(); - - $formatted = $this->getShares($accessContext, null, null, null, null, null, null); - $this->assertIsArray($formatted[0]); - $this->assertArrayHasKey('id', $formatted[0]); - $this->assertIsArray($formatted[1]); - $this->assertArrayHasKey('id', $formatted[1]); - $this->assertEquals($share2->id, $formatted[0]['id']); - $this->assertEquals($share1->id, $formatted[1]['id']); - } - - public function testOwnerDeleted(): void { - $accessContext = new ShareAccessContext(currentUser: $this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->owner->delete(); - $this->dbConnection->commit(); - - try { - $this->getShare(new ShareAccessContext(overrideChecks: true), $share->id); - $this->fail('Share still exists.'); - } catch (HintException $hintException) { - $this->assertEquals('Share not found.', $hintException->getHint()); - } - } - - public function testInitiatorDeleted(): void { - $accessContext = new ShareAccessContext($this->owner); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(ReshareSharePermissionType::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - $share = $this->manager->addShareRecipient( - new ShareAccessContext(currentUser: $this->user1), $share, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null) - ); - - $before = $this->manager->getTime(); - $this->user1->delete(); - $after = $this->manager->getTime(); - $this->dbConnection->commit(); - - $formatted = $this->getShare(new ShareAccessContext(overrideChecks: true), $share->id); - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - [ - 'class' => TestShareRecipientType1::class, - 'value' => 'recipient1', - 'instance' => null, - 'display_name' => 'Recipient 1', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - [ - 'class' => TestShareRecipientType2::class, - 'value' => 'recipient2', - 'instance' => null, - 'display_name' => 'Recipient 2', - 'icon' => [ - 'svg' => '', - ], - 'secret' => [ - 'updatable' => false, - ], - 'initiator' => [ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], - 'permissions' => [], - ], - ], $formatted['recipients']); - } - - public function testGetWithDirectAccess(): void { - $accessContext = new ShareAccessContext($this->owner); - $accessContext2 = new ShareAccessContext(currentUser: $this->user2); - - $before = $this->manager->getTime(); - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - $after = $this->manager->getTime(); - - // user2 has no direct access, no shares - $formattedShares = $this->getShares($accessContext2, TestShareSourceType1::class, 'source1', null, null, null, null); - $this->assertCount(0, $formattedShares); - - try { - $this->getShare($accessContext2, $share->id); - $this->fail('user has invalid share access'); - } catch (HintException) { - - } - - // give user2 direct access, can see shares - $this->shareSourceType1->userAccess[$this->user2->getUID()] = ['source1']; - $formattedShares = $this->getShares($accessContext2, TestShareSourceType1::class, 'source1', null, null, null, null); - - $this->assertCount(1, $formattedShares); - $formatted = $formattedShares[0]; - - $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated'])); - $this->assertEquals([ - 'user_id' => 'owner', - 'instance' => null, - 'display_name' => 'Owner', - 'icon' => [ - 'light' => 'http://localhost/index.php/avatar/owner/64', - 'dark' => 'http://localhost/index.php/avatar/owner/64/dark', - ], - ], $formatted['owner']); - - $singleFormatted = $this->getShare($accessContext2, $share->id); - $this->assertEquals($singleFormatted, $formatted); - - // add a source that user2 doesn't have access to, can't see share anymore - $this->dbConnection->beginTransaction(); - $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType2::class, 'source2')); - $this->dbConnection->commit(); - $formattedShares = $this->getShares(new ShareAccessContext(currentUser: $this->user2), TestShareSourceType1::class, 'source1', null, null, null, null); - $this->assertCount(0, $formattedShares); - } - - public function testEditWithDirectAccess(): void { - $accessContext = new ShareAccessContext($this->owner); - $accessContext2 = new ShareAccessContext(currentUser: $this->user2); - - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $share = $this->manager->addShareSource($accessContext, $share, new ShareSource(TestShareSourceType1::class, 'source1')); - $share = $this->manager->addShareRecipient($accessContext, $share, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - - $share = $this->manager->updateSharePermission($accessContext, $share, new SharePermission(TestSharePermissionType1::class, true)); - $share = $this->manager->updateShareState($accessContext, $share, ShareState::Active); - - $this->dbConnection->commit(); - - try { - $this->updateShareProperty($accessContext2, $share, new ShareProperty(TestSharePropertyType1::class, 'valid1')); - $this->fail('update allowed'); - } catch (HintException) { - - } - - // give user2 direct access - $this->shareSourceType1->userAccess[$this->user2->getUID()] = ['source1']; - $user2Share = $this->reloadShare($accessContext2, $share); - $this->assertEmpty($user2Share->recipients); - $formatted = $this->updateShareProperty($accessContext2, $user2Share, new ShareProperty(TestSharePropertyType1::class, 'valid1')); - - $this->assertEquals([ - [ - 'display_name' => 'TestSharePropertyType1', - 'hint' => 'hint TestSharePropertyType1', - 'priority' => 1, - 'required' => false, - 'advanced' => false, - 'value' => 'valid1', - 'class' => \Test\Sharing\TestSharePropertyType1::class, - 'type' => 'enum', - 'valid_values' => ['valid1'], - ] - ], $formatted['properties']); - } - - /** - * @return array{ - * shares: array{recipients: ShareRecipient[], owner: string}[], - * user: ShareUser, - * filterRecipientTypeClasses?: ?array, - * notInShare?: ?string, - * count?: int, - * offset?: int, - * expected?: string[] - * }[] - */ - public static function getRecipientsForUsersData(): array { - $initiator = new ShareUser('user1', null); - $initiator2 = new ShareUser('user2', null); - $recipient1 = new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null, null, $initiator); - $recipient2 = new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null, null, $initiator); - $recipient1By2 = new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null, null, $initiator2); - $owner = new ShareUser('owner', null); - - return [ - // querying for the owner, with no further filters - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'expected' => ['recipient1', 'recipient2']], - // querying for the owner, limit - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'count' => 1, 'expected' => ['recipient1']], - // querying for the owner, offset - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'offset' => 1, 'expected' => ['recipient2']], - // querying for the owner, offset past end - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'offset' => 2, 'expected' => []], - // querying for the initiator, with no further filters - ['shares' => [ - ['recipients' => [$recipient1], 'owner' => 'owner'] - ], 'user' => $initiator, 'expected' => ['recipient1']], - // querying for the wrong initiator - ['shares' => [ - ['recipients' => [$recipient1], 'owner' => 'owner'] - ], 'user' => $initiator2, 'expected' => []], - // querying for the owner, filter type - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'filterRecipientTypeClasses' => [TestShareRecipientType2::class], 'expected' => ['recipient2']], - // querying for the owner, multiple shares - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], - ['recipients' => [$recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'expected' => ['recipient2', 'recipient1']], - // querying for the owner, multiple shares, not in share - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], - 'excluded' => ['recipients' => [$recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'notInShare' => 'excluded', 'expected' => ['recipient1']], - // querying for the owner, multiple shares, offset - ['shares' => [ - ['recipients' => [$recipient1, $recipient2], 'owner' => 'owner'], - ['recipients' => [$recipient2], 'owner' => 'owner'] - ], 'user' => $owner, 'offset' => 1, 'expected' => ['recipient1']], - // user has both owned share and initiated recipients - ['shares' => [ - ['recipients' => [$recipient1], 'owner' => 'user2'], - ['recipients' => [$recipient1By2], 'owner' => 'owner'] - ], 'user' => $initiator2, 'offset' => 0, 'expected' => ['recipient1']], - ]; - } - - /** - * @param array{recipients: ShareRecipient[], owner: string}[] $shares - * @param string[] $expected , - */ - #[DataProvider('getRecipientsForUsersData')] - public function testGetRecipientsForUsers( - array $shares, - ShareUser $user, - ?array $filterRecipientTypeClasses = null, - ?string $notInShare = null, - int $count = 5, - int $offset = 0, - array $expected = [], - ): void { - /** @var Share[] $share */ - $createdShares = []; - $this->dbConnection->beginTransaction(); - foreach ($shares as $key => $shareData) { - $owner = $this->createMock(IUser::class); - $owner->method('getUID')->willReturn($shareData['owner']); - $ownerAccessContext = new ShareAccessContext($owner); - - $share = $this->manager->createShare($ownerAccessContext); - foreach ($shareData['recipients'] as $recipient) { - $share = $this->manager->addShareRecipient($ownerAccessContext, $share, $recipient); - } - - $createdShares[$key] = $share; - } - - $this->dbConnection->commit(); - - $accessUser = $this->createMock(IUser::class); - $accessUser->method('getUID')->willReturn($user->userId); - - $accessContext = new ShareAccessContext($accessUser); - - $notInShareId = ($notInShare !== null) ? $createdShares[$notInShare] : null; - $recipients = $this->searchRecipients($accessContext, $filterRecipientTypeClasses, '', $count, $offset, $notInShareId); - $recipientValues = array_map(fn (array $recipient): string => $recipient['value'], $recipients); - $this->assertEquals($expected, $recipientValues); - } -} --- a/tests/lib/Sharing/ClassMapperTest.php +++ /dev/null @@ -1,68 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->clearMappings(); - $this->classMapper = $this->getMapper(); - } - - private function clearMappings(): void { - $query = $this->connection->getTypedQueryBuilder(); - $cleanupClasses = [IDBConnection::class, ClassMapper::class]; - $query->delete('sharing_classmap') - ->where($query->expr()->in('class_name', $query->createNamedParameter($cleanupClasses, IQueryBuilder::PARAM_STR_ARRAY))); - $query->executeStatement(); - } - - private function getMapper(): ClassMapper { - return new ClassMapper($this->connection); - } - - public function testGetInsert(): void { - $id = $this->classMapper->getClassId(IDBConnection::class); - $this->assertEquals($id, $this->classMapper->getClassId(IDBConnection::class)); - $this->assertEquals(IDBConnection::class, $this->classMapper->getClassName($id)); - - $this->assertEquals($id, $this->getMapper()->getClassId(IDBConnection::class)); - } - - public function testInsertConcurrent(): void { - $concurrentMapper = $this->getMapper(); - - // trigger a load - $this->classMapper->getClassId(ClassMapper::class); - - $id = $concurrentMapper->getClassId(IDBConnection::class); - $this->assertEquals($id, $this->classMapper->getClassId(IDBConnection::class)); - $this->assertEquals(IDBConnection::class, $this->classMapper->getClassName($id)); - } - - public function testGetUnknownId(): void { - $this->expectException(\Exception::class); - $this->classMapper->getClassName(PHP_INT_MAX - 1); - } -} --- a/tests/lib/Sharing/Property/ABooleanSharePropertyTypeTest.php +++ /dev/null @@ -1,81 +0,0 @@ -propertyType = new TestBooleanSharePropertyType(); - } - - public function testValidateValue(): void { - $l10nFactory = Server::get(IFactory::class); - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'true')); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'false')); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, '')); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, 'invalid')); - } -} --- a/tests/lib/Sharing/Property/ADateSharePropertyTypeTest.php +++ /dev/null @@ -1,104 +0,0 @@ -minDate; - } - - #[\Override] - public function getMaxDate(Share $share): ?DateTimeImmutable { - return $this->maxDate; - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - throw new \RuntimeException(); - } - - #[\Override] - public function getHint(IFactory $l10nFactory, Share $share): ?string { - throw new \RuntimeException(); - } - - #[\Override] - public function getPriority(): int { - throw new \RuntimeException(); - } - - #[\Override] - public function isAdvanced(): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function isRequired(Share $share): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function getDefaultValue(Share $share): ?string { - throw new \RuntimeException(); - } -} - -final class ADateSharePropertyTypeTest extends TestCase { - private DateTimeImmutable $now; - - private ADateSharePropertyType $propertyType; - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->now = new DateTimeImmutable(); - $this->propertyType = new TestDateSharePropertyType( - $this->now->sub(new DateInterval('PT1M')), - $this->now->add(new DateInterval('PT1M')), - ); - } - - public function testValidateValue(): void { - $l10nFactory = Server::get(IFactory::class); - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, $this->now->format(DateTimeInterface::ATOM))); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, $this->now->sub(new DateInterval('PT2M'))->format(DateTimeInterface::ATOM))); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, $this->now->add(new DateInterval('PT2M'))->format(DateTimeInterface::ATOM))); - } -} --- a/tests/lib/Sharing/Property/AEnumSharePropertyTypeTest.php +++ /dev/null @@ -1,93 +0,0 @@ - $validValues */ - public array $validValues, - ) { - } - - /** - * @return non-empty-list - */ - #[\Override] - public function getValidValues(): array { - return $this->validValues; - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - throw new \RuntimeException(); - } - - #[\Override] - public function getHint(IFactory $l10nFactory, Share $share): ?string { - throw new \RuntimeException(); - } - - #[\Override] - public function getPriority(): int { - throw new \RuntimeException(); - } - - #[\Override] - public function isAdvanced(): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function isRequired(Share $share): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function getDefaultValue(Share $share): ?string { - throw new \RuntimeException(); - } -} - -final class AEnumSharePropertyTypeTest extends TestCase { - private AEnumSharePropertyType $propertyType; - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->propertyType = new TestEnumSharePropertyType(['valid']); - } - - public function testValidateValue(): void { - $l10nFactory = Server::get(IFactory::class); - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'valid')); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, 'invalid')); - } -} --- a/tests/lib/Sharing/Property/APasswordSharePropertyTypeTest.php +++ /dev/null @@ -1,123 +0,0 @@ -propertyType = new TestPasswordSharePropertyType(); - - $this->eventDispatcher = Server::get(IEventDispatcher::class); - $this->validatePasswordPolicyEventListener = static function (ValidatePasswordPolicyEvent $event): void { - if ($event->getPassword() !== 'secure') { - throw new HintException('insecure message', 'insecure hint'); - } - }; - - $this->eventDispatcher->addListener(ValidatePasswordPolicyEvent::class, $this->validatePasswordPolicyEventListener); - } - - #[\Override] - protected function tearDown(): void { - $this->eventDispatcher->removeListener(ValidatePasswordPolicyEvent::class, $this->validatePasswordPolicyEventListener); - - parent::tearDown(); - } - - public function testValidateValue(): void { - $l10nFactory = Server::get(IFactory::class); - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'secure')); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, '123')); - } - - public function testModifyValueOnFetch(): void { - $this->assertNull($this->propertyType->modifyValueOnLoad(null)); - $this->assertEquals(APasswordSharePropertyType::PLACEHOLDER, $this->propertyType->modifyValueOnLoad('')); - } - - public function testModifyValueOnSave(): void { - $this->assertNull($this->propertyType->modifyValueOnSave('old hash', null)); - - $this->assertEquals('old hash', $this->propertyType->modifyValueOnSave('old hash', APasswordSharePropertyType::PLACEHOLDER)); - - $newHash = $this->propertyType->modifyValueOnSave('old hash', 'password'); - $this->assertNotNull($newHash); - $this->assertNotEquals('old hash', $newHash); - $this->assertTrue(Server::get(IHasher::class)->verify('password', $newHash)); - } -} --- a/tests/lib/Sharing/Property/AStringSharePropertyTypeTest.php +++ /dev/null @@ -1,103 +0,0 @@ -minLength; - } - - #[\Override] - public function getMaxLength(): ?int { - return $this->maxLength; - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - throw new \RuntimeException(); - } - - #[\Override] - public function getHint(IFactory $l10nFactory, Share $share): ?string { - throw new \RuntimeException(); - } - - #[\Override] - public function getPriority(): int { - throw new \RuntimeException(); - } - - #[\Override] - public function isAdvanced(): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function isRequired(Share $share): bool { - throw new \RuntimeException(); - } - - #[\Override] - public function getDefaultValue(Share $share): ?string { - throw new \RuntimeException(); - } -} - -final class AStringSharePropertyTypeTest extends TestCase { - private AStringSharePropertyType $propertyType; - - #[\Override] - public function setUp(): void { - parent::setUp(); - - $this->propertyType = new TestStringSharePropertyType( - 3, - 5, - ); - } - - public function testValiStringValue(): void { - $l10nFactory = Server::get(IFactory::class); - $share = new Share( - '123', - new ShareUser('user', null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [], - [], - [], - ); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, 'ab')); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'abc')); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'abcd')); - $this->assertTrue($this->propertyType->validateValue($l10nFactory, $share, 'abcde')); - $this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, 'abcdef')); - } -} --- a/tests/lib/Sharing/ShareTest.php +++ /dev/null @@ -1,211 +0,0 @@ -registry = Server::get(ISharingRegistry::class); - $this->registry->clear(); - - $this->owner = $this->createUser('owner', 'password'); - $this->user1 = $this->createUser('user1', 'password'); - $this->user2 = $this->createUser('user2', 'password'); - } - - #[\Override] - public function tearDown(): void { - $this->registry->clear(); - - parent::tearDown(); - } - - public function testGetEffectiveRecipients(): void { - $recipient1 = new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null); - $recipient2 = new ShareRecipient(TestShareRecipientType1::class, 'recipient2', null); - $recipient3 = new ShareRecipient(TestShareRecipientType1::class, 'recipient3', null, 'secret'); - - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1' => 'Recipient 1', - 'recipient2' => 'Recipient 2', - 'recipient3' => 'Recipient 3', - ], - [ - $this->user1->getUID() => ['recipient1'], - $this->user2->getUID() => ['recipient2'], - ], - [], - ) - ); - - $share = new Share( - '123', - new ShareUser($this->owner->getUID(), null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - [ - $recipient1, - $recipient2, - $recipient3, - ], - [], - [], - ); - - $this->assertEquals([$recipient1], $share->getEffectiveRecipients(new ShareAccessContext($this->user1))); - $this->assertEquals([$recipient2], $share->getEffectiveRecipients(new ShareAccessContext($this->user2))); - $this->assertEquals([$recipient3], $share->getEffectiveRecipients(new ShareAccessContext(secret: 'secret'))); - $this->assertEquals([$recipient1, $recipient3], $share->getEffectiveRecipients(new ShareAccessContext($this->user1, 'secret'))); - } - - /** - * @return list, array, SharePermission>, bool}> - */ - public static function dataGetEffectiveEnabledPermissions(): array { - $recipient1 = new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null, null, null, [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)]); - $recipient2 = new ShareRecipient(TestShareRecipientType1::class, 'recipient2', null, null, null, [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, false)]); - - return [ - [ - [$recipient1], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)], - true, - ], - [ - [$recipient1], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, false)], - false, - ], - [ - [$recipient1], - [], - false, - ], - // - [ - [$recipient2], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)], - false, - ], - [ - [$recipient2], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, false)], - false, - ], - [ - [$recipient2], - [], - false, - ], - // - [ - [$recipient1, $recipient2], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)], - true, - ], - [ - [$recipient1, $recipient2], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, false)], - false, - ], - [ - [$recipient1, $recipient2], - [], - false, - ], - // - [ - [$recipient2, $recipient1], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)], - true, - ], - [ - [$recipient2, $recipient1], - [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, false)], - false, - ], - [ - [$recipient2, $recipient1], - [], - false, - ], - ]; - } - - /** - * @param list $recipients - * @param array, SharePermission> $permissions - */ - #[DataProvider('dataGetEffectiveEnabledPermissions')] - public function testGetEffectiveEnabledPermissions(array $recipients, array $permissions, bool $enabled): void { - $this->registry->registerRecipientType( - new TestShareRecipientType1( - [ - 'recipient1' => 'Recipient 1', - 'recipient2' => 'Recipient 2', - ], - [ - $this->user1->getUID() => ['recipient1', 'recipient2'], - ], - [], - ) - ); - - $share = new Share( - '123', - new ShareUser($this->owner->getUID(), null), - new DateTimeImmutable(), - ShareState::Active, - null, - [], - $recipients, - [], - $permissions, - ); - - $this->assertEquals( - $enabled - ? [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)] - : [], - $share->getEffectiveEnabledPermissions(new ShareAccessContext($this->user1)), - ); - } -} --- a/tests/lib/Sharing/SharingManagerTest.php +++ /dev/null @@ -1,306 +0,0 @@ -manager->getShare($accessContext, $share->id); - // don't compare time at sub-ms accuracy - $this->assertEquals(SharingManager::timeToMs($retrieved->lastUpdated), SharingManager::timeToMs($share->lastUpdated)); - - // now we compared the lastUpdated, make them the same to not fail the full comparison when there is a sub-ms lastUpdate difference - $retrieved = new Share( - $retrieved->id, - $retrieved->owner, - $share->lastUpdated, - $retrieved->state, - $retrieved->userStatus, - $retrieved->sources, - $retrieved->recipients, - $retrieved->properties, - $retrieved->permissions - ); - - // cache the enabled permissions for both - foreach ([$retrieved, $share] as $item) { - $item->getEffectiveEnabledPermissions(new ShareAccessContext(overrideChecks: true)); - $item->getEffectiveEnabledPermissions(new ShareAccessContext($this->owner)); - $item->getEffectiveEnabledPermissions(new ShareAccessContext($this->user1)); - $item->getEffectiveEnabledPermissions(new ShareAccessContext($this->user2)); - foreach ($item->recipients as $recipient) { - $recipient->getDisabledPermissions(); - } - - // ensure source metadata is loaded - foreach ($item->sources as $source) { - $source->format($this->registry, $this->l10nFactory, false); - } - } - - $this->assertEquals($retrieved, $share, 'share object not in sync with database'); - } - - #[\Override] - protected function searchRecipients(ShareAccessContext $accessContext, ?array $filterRecipientTypeClasses, string $query, int $limit, int $offset, ?Share $forShare = null): array { - try { - $this->dbConnection->beginTransaction(); - if ($forShare instanceof Share) { - $this->assertShareSyncedWithDb($accessContext, $forShare); - } - - /** @psalm-suppress ArgumentTypeCoercion */ - $recipients = ShareRecipient::formatMultiple($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $this->manager->searchRecipients($accessContext, $filterRecipientTypeClasses, $query, $limit, $offset, $forShare)); - $this->dbConnection->commit(); - return $recipients; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function createShare(ShareAccessContext $accessContext): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->createShare($accessContext); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateShareState(ShareAccessContext $accessContext, Share $share, ShareState $state): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareState($accessContext, $share, $state); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateShareUserStatus(ShareAccessContext $accessContext, Share $share, ShareUserStatus $userStatus): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareUserStatus($accessContext, $share, $userStatus); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function addShareSource(ShareAccessContext $accessContext, Share $share, ShareSource $source): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->addShareSource($accessContext, $share, $source); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function removeShareSource(ShareAccessContext $accessContext, Share $share, ShareSource $source): array { - try { - $this->dbConnection->beginTransaction(); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $this->manager->removeShareSource($accessContext, $share, $source); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function addShareRecipient(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->addShareRecipient($accessContext, $share, $recipient); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function removeShareRecipient(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->removeShareRecipient($accessContext, $share, $recipient); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateShareRecipientSecret(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, string $secret): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareRecipientSecret($accessContext, $share, $recipient, $secret); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateShareProperty(ShareAccessContext $accessContext, Share $share, ShareProperty $property): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareProperty($accessContext, $share, $property); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateSharePermission(ShareAccessContext $accessContext, Share $share, SharePermission $permission): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateSharePermission($accessContext, $share, $permission); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function updateShareRecipientPermission(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, SharePermission $permission): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->updateShareRecipientPermission($accessContext, $share, $recipient, $permission); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function selectSharePermissionPreset(ShareAccessContext $accessContext, Share $share, string $permissionPresetClass): array { - try { - $this->dbConnection->beginTransaction(); - /** @psalm-suppress ArgumentTypeCoercion */ - $share = $this->manager->selectSharePermissionPreset($accessContext, $share, $permissionPresetClass); - $this->assertShareSyncedWithDb($accessContext, $share); - $share = $share->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function deleteShare(ShareAccessContext $accessContext, Share $share): void { - try { - $this->dbConnection->beginTransaction(); - $this->assertShareSyncedWithDb($accessContext, $share); - $this->manager->deleteShare($accessContext, $share); - $this->dbConnection->commit(); - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function getShare(ShareAccessContext $accessContext, string $id): array { - try { - $this->dbConnection->beginTransaction(); - $share = $this->manager->getShare($accessContext, $id)->format($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext); - $this->dbConnection->commit(); - return $share; - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } - - #[\Override] - protected function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array { - try { - $this->dbConnection->beginTransaction(); - /** @psalm-suppress ArgumentTypeCoercion */ - $shares = $this->manager->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit); - $this->dbConnection->commit(); - return Share::formatMultiple($this->registry, Server::get(IFactory::class), Server::get(IURLGenerator::class), Server::get(IUserManager::class), $accessContext, $shares); - } catch (Exception $exception) { - $this->dbConnection->rollBack(); - throw $exception; - } - } -} --- a/tests/lib/Sharing/SharingRegistryTest.php +++ /dev/null @@ -1,171 +0,0 @@ -registry = Server::get(ISharingRegistry::class); - $this->registry->clear(); - } - - #[\Override] - protected function tearDown(): void { - $this->registry->clear(); - - parent::tearDown(); - } - - public function testClear(): void { - $this->registry->registerSourceType(new TestShareSourceType1([])); - $this->registry->registerRecipientType(new TestShareRecipientType1([], [], [])); - $this->registry->registerPropertyType(new TestSharePropertyType1([''])); - $this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyType1::class, TestShareSourceType1::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyType1::class, TestShareRecipientType1::class); - $this->registry->registerPermissionType(TestShareSourceType1::class, new TestSharePermissionType1()); - $this->registry->registerPermissionPreset(new TestSharePermissionPreset1()); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset(TestSharePermissionType1::class, TestSharePermissionPreset1::class); - - $this->registry->clear(); - - $this->assertEquals([], $this->registry->getSourceTypes()); - $this->assertEquals([], $this->registry->getRecipientTypes()); - $this->assertEquals([], $this->registry->getPropertyTypes()); - $this->assertEquals([], $this->registry->getPropertyTypeCompatibleSourceTypeClasses()); - $this->assertEquals([], $this->registry->getPropertyTypeCompatibleRecipientTypes()); - $this->assertEquals([], $this->registry->getPermissionTypes()); - $this->assertEquals([], $this->registry->getPermissionPresets()); - $this->assertEquals([], $this->registry->getPermissionTypeCompatiblePermissionPresetClasses()); - $this->assertEquals([], $this->registry->getPermissionPresetCompatiblePermissionTypeClasses()); - } - - public function testRegisterSourceType(): void { - $sourceType = new TestShareSourceType1([]); - $this->registry->registerSourceType($sourceType); - - $this->assertEquals([$sourceType::class => $sourceType], $this->registry->getSourceTypes()); - - $this->expectException(RuntimeException::class); - $this->registry->registerSourceType($sourceType); - } - - public function testRegisterRecipientType(): void { - $recipientType = new TestShareRecipientType1([], [], []); - $this->registry->registerRecipientType($recipientType); - - $this->assertEquals([$recipientType::class => $recipientType], $this->registry->getRecipientTypes()); - - $this->expectException(RuntimeException::class); - $this->registry->registerRecipientType($recipientType); - } - - public function testRegisterProperty(): void { - $sourceType = new TestShareSourceType1([]); - $this->registry->registerSourceType($sourceType); - - $recipientType = new TestShareRecipientType1([], [], []); - $this->registry->registerRecipientType($recipientType); - - $propertyType = new TestSharePropertyType1(['']); - $this->registry->registerPropertyType($propertyType); - $this->registry->markPropertyTypeCompatibleWithSourceType($propertyType::class, $sourceType::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType($propertyType::class, $recipientType::class); - - $this->assertEquals([$propertyType::class => $propertyType], $this->registry->getPropertyTypes()); - - $this->expectException(RuntimeException::class); - $this->registry->registerPropertyType($propertyType); - } - - public function testMarkPropertyCompatibleWithSourceType(): void { - $sourceType = new TestShareSourceType1([]); - $this->registry->registerSourceType($sourceType); - - $propertyType = new TestSharePropertyType1(['']); - $this->registry->registerPropertyType($propertyType); - $this->registry->markPropertyTypeCompatibleWithSourceType($propertyType::class, $sourceType::class); - $this->registry->markPropertyTypeCompatibleWithSourceType($propertyType::class, $sourceType::class); - - $this->assertEquals([$propertyType::class => [$sourceType::class]], $this->registry->getPropertyTypeCompatibleSourceTypeClasses()); - - $this->registry->markPropertyTypeCompatibleWithSourceType($propertyType::class, TestShareSourceType2::class); - $this->expectException(RuntimeException::class); - $this->registry->getPropertyTypeCompatibleSourceTypeClasses(); - } - - public function testMarkPropertyCompatibleWithRecipientType(): void { - $recipientType = new TestShareRecipientType1([], [], []); - $this->registry->registerRecipientType($recipientType); - - $propertyType = new TestSharePropertyType1(['']); - $this->registry->registerPropertyType($propertyType); - $this->registry->markPropertyTypeCompatibleWithRecipientType($propertyType::class, $recipientType::class); - $this->registry->markPropertyTypeCompatibleWithRecipientType($propertyType::class, $recipientType::class); - - $this->assertEquals([$propertyType::class => [$recipientType::class]], $this->registry->getPropertyTypeCompatibleRecipientTypes()); - - $this->registry->markPropertyTypeCompatibleWithRecipientType($propertyType::class, TestShareRecipientType2::class); - $this->expectException(RuntimeException::class); - $this->registry->getPropertyTypeCompatibleRecipientTypes(); - } - - public function testRegisterPermission(): void { - $sourceType = new TestShareSourceType1([]); - $this->registry->registerSourceType($sourceType); - - $permissionType1 = new TestSharePermissionType1(); - $this->registry->registerPermissionType($sourceType::class, $permissionType1); - $permissionType2 = new TestSharePermissionType2(); - $this->registry->registerPermissionType(null, $permissionType2); - - $this->assertEquals([$permissionType1::class => $permissionType1, $permissionType2::class => $permissionType2], $this->registry->getPermissionTypes()); - $this->assertEquals([$sourceType::class => [$permissionType1::class]], $this->registry->getSourceTypePermissionTypeClasses()); - $this->assertEquals([$permissionType2::class], $this->registry->getGenericPermissionTypeClasses()); - - $this->expectException(RuntimeException::class); - $this->registry->registerPermissionType($sourceType::class, $permissionType1); - } - - public function testRegisterPermissionPreset(): void { - $permissionPreset = new TestSharePermissionPreset1(); - $this->registry->registerPermissionPreset($permissionPreset); - - $this->assertEquals([$permissionPreset::class => $permissionPreset], $this->registry->getPermissionPresets()); - - $this->expectException(RuntimeException::class); - $this->registry->registerPermissionPreset($permissionPreset); - } - - public function testMarkPermissionTypeCompatibleWithPermissionPreset(): void { - $permissionPreset = new TestSharePermissionPreset1(); - $this->registry->registerPermissionPreset($permissionPreset); - - $permissionType = new TestSharePermissionType1(); - $this->registry->registerPermissionType(null, $permissionType); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset($permissionType::class, $permissionPreset::class); - $this->registry->markPermissionTypeCompatibleWithPermissionPreset($permissionType::class, $permissionPreset::class); - - $this->assertEquals([$permissionType::class => [$permissionPreset::class]], $this->registry->getPermissionTypeCompatiblePermissionPresetClasses()); - $this->assertEquals([$permissionPreset::class => [$permissionType::class]], $this->registry->getPermissionPresetCompatiblePermissionTypeClasses()); - - $this->registry->markPermissionTypeCompatibleWithPermissionPreset($permissionType::class, TestSharePermissionPreset2::class); - $this->expectException(RuntimeException::class); - $this->registry->getPermissionTypeCompatiblePermissionPresetClasses(); - } -} --- a/tests/lib/Sharing/TestInteractionReceiver.php +++ /dev/null @@ -1,24 +0,0 @@ -recipient; - } -} --- a/tests/lib/Sharing/TestInteractionResource.php +++ /dev/null @@ -1,24 +0,0 @@ -source; - } -} --- a/tests/lib/Sharing/TestSharePermissionPreset1.php +++ /dev/null @@ -1,27 +0,0 @@ - $parts */ - $parts = explode('\\', static::class); - return end($parts); - } - - #[\Override] - public function getHint(IFactory $l10nFactory): ?string { - return 'hint ' . $this->getDisplayName($l10nFactory); - } -} --- a/tests/lib/Sharing/TestSharePermissionPreset2.php +++ /dev/null @@ -1,13 +0,0 @@ - $parts */ - $parts = explode('\\', static::class); - return end($parts); - } - - #[\Override] - public function getHint(IFactory $l10nFactory): string { - return 'hint ' . $this->getDisplayName($l10nFactory); - } - - #[\Override] - public function getPriority(): int { - return 1; - } - - #[\Override] - public function isEnabledByDefault(): bool { - return false; - } -} --- a/tests/lib/Sharing/TestSharePermissionType2.php +++ /dev/null @@ -1,13 +0,0 @@ - $validValues */ - private readonly array $validValues, - ) { - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - /** @var non-empty-list $parts */ - $parts = explode('\\', static::class); - return end($parts); - } - - #[\Override] - public function getHint(IFactory $l10nFactory, Share $share): string { - return 'hint ' . $this->getDisplayName($l10nFactory); - } - - #[\Override] - public function getPriority(): int { - return 1; - } - - #[\Override] - public function isAdvanced(): bool { - return false; - } - - #[\Override] - public function isRequired(Share $share): bool { - return false; - } - - #[\Override] - public function getDefaultValue(Share $share): ?string { - return null; - } - - /** - * @return non-empty-list - */ - #[\Override] - public function getValidValues(): array { - return $this->validValues; - } -} --- a/tests/lib/Sharing/TestSharePropertyType2.php +++ /dev/null @@ -1,13 +0,0 @@ -arguments[self::class] ?? null) === 'filtered') { - return true; - } - - return ($property = $share->properties[self::class] ?? null) !== null && $property->value === 'filtered'; - } -} --- a/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php +++ /dev/null @@ -1,43 +0,0 @@ -getValidValues()[0]; - } - - #[\Override] - public function isRequired(Share $share): bool { - return true; - } -} --- a/tests/lib/Sharing/TestShareRecipientType1.php +++ /dev/null @@ -1,83 +0,0 @@ - $validRecipients */ - private readonly array $validRecipients, - /** @var array> $recipientValues */ - private readonly array $recipientValues, - /** @var list $searchRecipients */ - public array $searchRecipients, - ) { - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - /** @var non-empty-list $parts */ - $parts = explode('\\', static::class); - return end($parts); - } - - #[\Override] - public function validateRecipient(string $recipient): bool { - return array_key_exists($recipient, $this->validRecipients); - } - - /** - * @return list - */ - #[\Override] - public function getRecipients(?IUser $currentUser, mixed $arguments): array { - if (!$currentUser instanceof IUser) { - return []; - } - - return $this->recipientValues[$currentUser->getUID()] ?? []; - } - - #[\Override] - public function getRecipientDisplayName(string $recipient, ?string $instance): ?string { - if ($instance !== null) { - return null; - } - - return $this->validRecipients[$recipient]; - } - - #[\Override] - public function getRecipientIcon(string $recipient, ?string $instance): null|ShareIconSVG|ShareIconURL { - return match ($recipient) { - 'url' => new ShareIconURL('https://example.com/light.png', 'https://example.com/dark.png'), - default => new ShareIconSVG(''), - }; - } - - #[\Override] - public function getRecipientInteractionReceiver(string $recipient): InteractionReceiver { - return new TestInteractionReceiver($recipient); - } - - #[\Override] - public function searchRecipients(ShareAccessContext $accessContext, string $query, int $limit, int $offset): array { - return array_slice($this->searchRecipients, $offset, $limit); - } -} --- a/tests/lib/Sharing/TestShareRecipientType2.php +++ /dev/null @@ -1,13 +0,0 @@ - $parts */ - $parts = explode('\\', self::class); - return end($parts); - } - - #[\Override] - public function validateRecipient(string $recipient): bool { - return true; - } - - #[\Override] - public function getRecipients(?IUser $currentUser, mixed $arguments): array { - if (is_string($arguments)) { - return [$arguments]; - } - - return []; - } - - #[\Override] - public function getRecipientDisplayName(string $recipient, ?string $instance): ?string { - return null; - } - - #[\Override] - public function getRecipientIcon(string $recipient, ?string $instance): null|ShareIconSVG|ShareIconURL { - return null; - } - - #[\Override] - public function getRecipientInteractionReceiver(string $recipient): InteractionReceiver { - return new TestInteractionReceiver($recipient); - } -} --- a/tests/lib/Sharing/TestShareRecipientTypePublicSecret.php +++ /dev/null @@ -1,82 +0,0 @@ - $validRecipients */ - private array $validRecipients, - /** @var array> $recipientValues */ - private array $recipientValues, - private bool $isSecretPublic, - private bool $isSecretUpdatable, - ) { - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - /** @var non-empty-list $parts */ - $parts = explode('\\', self::class); - return end($parts); - } - - #[\Override] - public function validateRecipient(string $recipient): bool { - return array_key_exists($recipient, $this->validRecipients); - } - - /** - * @return list - */ - #[\Override] - public function getRecipients(?IUser $currentUser, mixed $arguments): array { - if (!$currentUser instanceof IUser) { - return []; - } - - return $this->recipientValues[$currentUser->getUID()] ?? []; - } - - #[\Override] - public function getRecipientDisplayName(string $recipient, ?string $instance): ?string { - return $this->validRecipients[$recipient]; - } - - #[\Override] - public function getRecipientIcon(string $recipient, ?string $instance): ShareIconSVG|ShareIconURL { - return match ($recipient) { - 'url' => new ShareIconURL('https://example.com/light.png', 'https://example.com/dark.png'), - default => new ShareIconSVG(''), - }; - } - - #[\Override] - public function isSecretPublic(string $recipient): bool { - return $this->isSecretPublic; - } - - #[\Override] - public function isSecretUpdatable(string $recipient): bool { - return $this->isSecretUpdatable; - } - - #[\Override] - public function getRecipientInteractionReceiver(string $recipient): InteractionReceiver { - return new TestInteractionReceiver($recipient); - } -} --- a/tests/lib/Sharing/TestShareSourceType1.php +++ /dev/null @@ -1,70 +0,0 @@ - $validSources */ - private readonly array $validSources, - /** @var array $validSources */ - public array $userAccess = [], - ) { - } - - #[\Override] - public function getDisplayName(IFactory $l10nFactory): string { - /** @var non-empty-list $parts */ - $parts = explode('\\', static::class); - return end($parts); - } - - #[\Override] - public function validateSource(IUser $owner, string $source): bool { - return array_key_exists($source, $this->validSources); - } - - #[\Override] - public function getSourceMetadata(string $source): ?IShareSourceMetadata { - if (isset($this->validSources[$source])) { - return new ShareSourceMetadata( - $this->validSources[$source], - new ShareIconSVG(''), - ); - } - - return null; - } - - #[\Override] - public function getSourcesMetadata(array $sources): array { - $metas = array_map($this->getSourceMetadata(...), $sources); - $metas = array_combine($sources, $metas); - return array_filter($metas); - } - - #[\Override] - public function getSourceInteractionResource(IUser $user, string $source): InteractionResource { - return new TestInteractionResource($source); - } - - #[\Override] - public function userHasDirectSharingAccessToSource(IUser $user, string $source): bool { - $userSources = $this->userAccess[$user->getUID()] ?? []; - return in_array($source, $userSources); - } -} --- a/tests/lib/Sharing/TestShareSourceType2.php +++ /dev/null @@ -1,13 +0,0 @@ -sequence = new APCuSequence(); - } -} --- a/tests/lib/Snowflake/DecoderTest.php +++ /dev/null @@ -1,72 +0,0 @@ -decoder = new SnowflakeDecoder(); - } - - #[DataProvider('provideSnowflakeIds')] - public function testDecode( - string $snowflakeId, - float $timestamp, - int $serverId, - int $sequenceId, - bool $isCli, - ): void { - $data = $this->decoder->decode($snowflakeId); - - $this->assertEquals($timestamp, (float)$data->getCreatedAt()->format('U.v')); - $this->assertEquals($serverId, $data->getServerId()); - $this->assertEquals($sequenceId, $data->getSequenceId()); - $this->assertEquals($isCli, $data->isCli()); - } - - public static function provideSnowflakeIds(): array { - $data = [ - ['4688076898113587', 1760368327.984, 392, 2099, true], - // Max milliseconds - ['4190109696', 1759276800.999, 0, 0, false], - // Max serverId - ['4186112', 1759276800.0, 511, 0, false], - // Max sequenceId - ['4095', 1759276800.0, 0, 4095, false], - // Max isCli - ['4096', 1759276800.0, 0, 0, true], - // Min - ['0', 1759276800, 0, 0, false], - // Other - ['250159983611680096', 1817521710, 392, 1376, true], - ]; - - // 32 bits can't handle large timestamps correctly - if (PHP_INT_SIZE === 8) { - // Max all (can't happen because ms are up to 999) - $data[] = ['9223372036854775807', 3906760448.023, 511, 4095, true]; - // Max all (real) - $data[] = ['9223372036754112511', 3906760447.999, 511, 4095, true]; - // Max seconds - $data[] = ['9223372032559808512', 3906760447, 0, 0, false]; - } - - return $data; - } -} --- a/tests/lib/Snowflake/FileSequenceTest.php +++ /dev/null @@ -1,40 +0,0 @@ -createMock(ITempManager::class); - $this->path = sys_get_temp_dir(); - $tempManager->method('getTempBaseDir')->willReturn($this->path); - $this->sequence = new FileSequence($tempManager); - } - - #[\Override] - public function tearDown():void { - $lockDirectory = $this->path . '/' . FileSequence::LOCK_FILE_DIRECTORY; - foreach (glob($lockDirectory . '/*') as $file) { - unlink($file); - } - - parent::tearDown(); - } -} --- a/tests/lib/Snowflake/GeneratorTest.php +++ /dev/null @@ -1,131 +0,0 @@ -decoder = new SnowflakeDecoder(); - - $this->sequence = $this->createMock(ISequence::class); - $this->sequence->method('isAvailable')->willReturn(true); - $this->sequence->method('nextId')->willReturn(421); - - $this->serverInfo = Server::get(IServerInfo::class); - } - - public function testGenerator(): void { - $generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo); - $snowflakeId = $generator->nextId(); - $data = $this->decoder->decode($generator->nextId()); - - $this->assertIsString($snowflakeId); - // Check timestamp - $this->assertGreaterThan(time() - 30, $data->getCreatedAt()->format('U')); - - // Check serverId - $this->assertGreaterThanOrEqual(0, $data->getServerId()); - $this->assertLessThanOrEqual(511, $data->getServerId()); - - // Check sequenceId - $this->assertGreaterThanOrEqual(0, $data->getSequenceId()); - $this->assertLessThanOrEqual(4095, $data->getSequenceId()); - - // Check CLI - $this->assertTrue($data->isCli()); - - // Check serverId - $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); - } - - public function testMinForTime(): void { - $generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo); - $now = time(); - $snowflakeId = $generator->minForTimeId($now); - $data = $this->decoder->decode($snowflakeId); - - $this->assertIsString($snowflakeId); - - // Check timestamp - $this->assertEquals($now - ISnowflakeGenerator::TS_OFFSET, $data->getSeconds()); - - // Check all other fields are at zero - $this->assertEquals(0, $data->getMilliseconds()); - $this->assertEquals(0, $data->getServerId()); - $this->assertEquals(0, $data->getSequenceId()); - $this->assertFalse($data->isCli()); - $this->assertEquals(0, $data->getServerId()); - } - - #[DataProvider('provideSnowflakeData')] - public function testGeneratorWithFixedTime(string $date, int $expectedSeconds, int $expectedMilliseconds): void { - $dt = new \DateTimeImmutable($date); - $timeFactory = $this->createMock(ITimeFactory::class); - $timeFactory->method('now')->willReturn($dt); - - $generator = new SnowflakeGenerator($timeFactory, $this->sequence, $this->serverInfo); - $data = $this->decoder->decode($generator->nextId()); - - $this->assertEquals($expectedSeconds, $data->getCreatedAt()->format('U') - ISnowflakeGenerator::TS_OFFSET); - $this->assertEquals($expectedMilliseconds, (int)$data->getCreatedAt()->format('v')); - $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); - } - - #[DataProvider('provideSnowflakeData')] - public function testGeneratorWithTimestampParameter(string $date, int $expectedSeconds, int $expectedMilliseconds): void { - $dt = new \DateTimeImmutable($date); - - $generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo); - $data = $this->decoder->decode($generator->nextId($dt)); - - $this->assertEquals($expectedSeconds, $data->getCreatedAt()->format('U') - ISnowflakeGenerator::TS_OFFSET); - $this->assertEquals($expectedMilliseconds, (int)$data->getCreatedAt()->format('v')); - $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); - } - - public static function provideSnowflakeData(): array { - $tests = [ - ['2025-10-01 00:00:00.000000', 0, 0], - ['2025-10-01 00:00:01.000000', 1, 0], - ['2025-10-01 00:00:00.001000', 0, 1], - ['2027-08-06 03:08:30.000975', 58244910, 0], - ['2030-06-21 12:59:33.100875', 149000373, 100], - ['2038-01-18 13:33:37.666666', 388157617, 666], - ]; - // Timestamp in 32 bits can't go after 2038. Add few cases for 64 bits. - if (PHP_INT_SIZE === 8) { - $tests[] = ['2039-12-31 23:59:59.999999', 449711999, 999]; - $tests[] = ['2086-06-21 12:59:33.010875', 1916225973, 10]; - } - - return $tests; - } -} --- a/tests/lib/Snowflake/ISequenceBase.php +++ /dev/null @@ -1,45 +0,0 @@ -sequence->isAvailable()) { - $this->markTestSkipped('Sequence ID generator ' . get_class($this->sequence) . 'is’nt available. Skip'); - } - - $nb = 1000; - $ids = []; - $server = 42; - for ($i = 0; $i < $nb; ++$i) { - $time = explode('.', (string)microtime(true)); - $seconds = (int)$time[0]; - $milliseconds = str_pad(substr($time[1] ?? '0', 0, 3), 3, '0'); - $id = $this->sequence->nextId($server, $seconds, (int)$milliseconds); - $ids[] = sprintf('%d_%s_%d', $seconds, $milliseconds, $id); - usleep(100); - } - - // Is it unique? - $this->assertCount($nb, array_unique($ids)); - // Is it sequential? - $sortedIds = $ids; - natsort($sortedIds); - $this->assertSame($sortedIds, $ids); - } -} --- a/tests/lib/SubAdminTest.php +++ /dev/null @@ -1,301 +0,0 @@ -users = []; - $this->groups = []; - - $this->userManager = Server::get(IUserManager::class); - $this->groupManager = Server::get(IGroupManager::class); - $this->dbConn = Server::get(IDBConnection::class); - $this->eventDispatcher = Server::get(IEventDispatcher::class); - - // Create 3 users and 3 groups - for ($i = 0; $i < 3; $i++) { - $this->users[] = $this->userManager->createUser('user' . $i, 'user'); - $this->groups[] = $this->groupManager->createGroup('group' . $i); - } - - // Create admin group - if (!$this->groupManager->groupExists('admin')) { - $this->groupManager->createGroup('admin'); - } - - // Create "orphaned" users and groups (scenario: temporarily disabled - // backend) - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('group_admin') - ->values([ - 'gid' => $qb->createNamedParameter($this->groups[0]->getGID()), - 'uid' => $qb->createNamedParameter('orphanedUser') - ]) - ->executeStatement(); - $qb->insert('group_admin') - ->values([ - 'gid' => $qb->createNamedParameter('orphanedGroup'), - 'uid' => $qb->createNamedParameter('orphanedUser') - ]) - ->executeStatement(); - $qb->insert('group_admin') - ->values([ - 'gid' => $qb->createNamedParameter('orphanedGroup'), - 'uid' => $qb->createNamedParameter($this->users[0]->getUID()) - ]) - ->executeStatement(); - } - - #[\Override] - protected function tearDown(): void { - foreach ($this->users as $user) { - $user->delete(); - } - - foreach ($this->groups as $group) { - $group->delete(); - } - - $qb = $this->dbConn->getQueryBuilder(); - $qb->delete('group_admin') - ->where($qb->expr()->eq('uid', $qb->createNamedParameter('orphanedUser'))) - ->orWhere($qb->expr()->eq('gid', $qb->createNamedParameter('orphanedGroup'))) - ->executeStatement(); - parent::tearDown(); - } - - public function testCreateSubAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - - // Look for subadmin in the database - $qb = $this->dbConn->getQueryBuilder(); - $result = $qb->select(['gid', 'uid']) - ->from('group_admin') - ->where($qb->expr()->eq('gid', $qb->createNamedParameter($this->groups[0]->getGID()))) - ->andWHere($qb->expr()->eq('uid', $qb->createNamedParameter($this->users[0]->getUID()))) - ->executeQuery() - ->fetchAssociative(); - $this->assertEquals( - [ - 'gid' => $this->groups[0]->getGID(), - 'uid' => $this->users[0]->getUID() - ], $result); - - // Delete subadmin - $qb->delete('group_admin') - ->where($qb->expr()->eq('gid', $qb->createNamedParameter($this->groups[0]->getGID()))) - ->andWHere($qb->expr()->eq('uid', $qb->createNamedParameter($this->users[0]->getUID()))) - ->executeStatement(); - } - - public function testDeleteSubAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - - // DB query should be empty - $qb = $this->dbConn->getQueryBuilder(); - $result = $qb->select(['gid', 'uid']) - ->from('group_admin') - ->where($qb->expr()->eq('gid', $qb->createNamedParameter($this->groups[0]->getGID()))) - ->andWHere($qb->expr()->eq('uid', $qb->createNamedParameter($this->users[0]->getUID()))) - ->executeQuery() - ->fetchAssociative(); - $this->assertEmpty($result); - } - - public function testGetSubAdminsGroups(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->createSubAdmin($this->users[0], $this->groups[1]); - - $result = $subAdmin->getSubAdminsGroups($this->users[0]); - - $this->assertContains($this->groups[0], $result); - $this->assertContains($this->groups[1], $result); - $this->assertNotContains($this->groups[2], $result); - $this->assertNotContains(null, $result); - - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[1]); - } - - public function testGetGroupsSubAdmins(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->createSubAdmin($this->users[1], $this->groups[0]); - - $result = $subAdmin->getGroupsSubAdmins($this->groups[0]); - - $this->assertContains($this->users[0], $result); - $this->assertContains($this->users[1], $result); - $this->assertNotContains($this->users[2], $result); - $this->assertNotContains(null, $result); - - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->deleteSubAdmin($this->users[1], $this->groups[0]); - } - - public function testGetAllSubAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->createSubAdmin($this->users[1], $this->groups[1]); - $subAdmin->createSubAdmin($this->users[2], $this->groups[1]); - - $result = $subAdmin->getAllSubAdmins(); - - $this->assertContains(['user' => $this->users[0], 'group' => $this->groups[0]], $result); - $this->assertContains(['user' => $this->users[1], 'group' => $this->groups[1]], $result); - $this->assertContains(['user' => $this->users[2], 'group' => $this->groups[1]], $result); - $this->assertNotContains(['user' => null, 'group' => null], $result); - } - - public function testIsSubAdminofGroup(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - - $this->assertTrue($subAdmin->isSubAdminOfGroup($this->users[0], $this->groups[0])); - $this->assertFalse($subAdmin->isSubAdminOfGroup($this->users[0], $this->groups[1])); - $this->assertFalse($subAdmin->isSubAdminOfGroup($this->users[1], $this->groups[0])); - - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - } - - public function testIsSubAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - - $this->assertTrue($subAdmin->isSubAdmin($this->users[0])); - $this->assertFalse($subAdmin->isSubAdmin($this->users[1])); - - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - } - - public function testIsSubAdminAsAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $this->groupManager->get('admin')->addUser($this->users[0]); - - $this->assertTrue($subAdmin->isSubAdmin($this->users[0])); - } - - public function testIsUserAccessible(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $this->groups[0]->addUser($this->users[1]); - $this->groups[1]->addUser($this->users[1]); - $this->groups[1]->addUser($this->users[2]); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->createSubAdmin($this->users[2], $this->groups[2]); - - $this->assertTrue($subAdmin->isUserAccessible($this->users[0], $this->users[1])); - $this->assertFalse($subAdmin->isUserAccessible($this->users[0], $this->users[2])); - $this->assertFalse($subAdmin->isUserAccessible($this->users[2], $this->users[0])); - - $subAdmin->deleteSubAdmin($this->users[0], $this->groups[0]); - $subAdmin->deleteSubAdmin($this->users[2], $this->groups[2]); - } - - public function testIsUserAccessibleAsUser(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $this->assertFalse($subAdmin->isUserAccessible($this->users[0], $this->users[1])); - } - - public function testIsUserAccessibleAdmin(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - $subAdmin->createSubAdmin($this->users[0], $this->groups[0]); - $this->groupManager->get('admin')->addUser($this->users[1]); - - $this->assertFalse($subAdmin->isUserAccessible($this->users[0], $this->users[1])); - } - - public function testPostDeleteUser(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - - $user = array_shift($this->users); - foreach ($this->groups as $group) { - $subAdmin->createSubAdmin($user, $group); - } - - $user->delete(); - $this->assertEmpty($subAdmin->getAllSubAdmins()); - } - - public function testPostDeleteGroup(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - - $group = array_shift($this->groups); - foreach ($this->users as $user) { - $subAdmin->createSubAdmin($user, $group); - } - - $group->delete(); - $this->assertEmpty($subAdmin->getAllSubAdmins()); - } - - public function testHooks(): void { - $subAdmin = new SubAdmin($this->userManager, $this->groupManager, $this->dbConn, $this->eventDispatcher); - - $test = $this; - $u = $this->users[0]; - $g = $this->groups[0]; - $count = 0; - - $this->eventDispatcher->addListener(SubAdminAddedEvent::class, function (SubAdminAddedEvent $event) use ($test, $u, $g, &$count): void { - $test->assertEquals($u->getUID(), $event->getUser()->getUID()); - $test->assertEquals($g->getGID(), $event->getGroup()->getGID()); - $count++; - }); - - $this->eventDispatcher->addListener(SubAdminRemovedEvent::class, function ($event) use ($test, $u, $g, &$count): void { - $test->assertEquals($u->getUID(), $event->getUser()->getUID()); - $test->assertEquals($g->getGID(), $event->getGroup()->getGID()); - $count++; - }); - - $subAdmin->createSubAdmin($u, $g); - $this->assertEquals(1, $count); - - $subAdmin->deleteSubAdmin($u, $g); - $this->assertEquals(2, $count); - } -} --- a/tests/lib/Support/CrashReport/RegistryTest.php +++ /dev/null @@ -1,106 +0,0 @@ -registry = new Registry(); - } - - /** - * Doesn't assert anything, just checks whether anything "explodes" - */ - public function testDelegateToNone(): void { - $exception = new Exception('test'); - - $this->registry->delegateReport($exception); - $this->addToAssertionCount(1); - } - - public function testRegisterLazy(): void { - $reporterClass = '\OCA\MyApp\Reporter'; - $reporter = $this->createMock(IReporter::class); - $this->overwriteService($reporterClass, $reporter); - $reporter->expects($this->once()) - ->method('report'); - $exception = new Exception('test'); - - $this->registry->registerLazy($reporterClass); - $this->registry->delegateReport($exception); - } - - /** - * Doesn't assert anything, just checks whether anything "explodes" - */ - public function testRegisterLazyCantLoad(): void { - $reporterClass = '\OCA\MyApp\Reporter'; - /* We do not register reporterClass in DI, so it will throw a QueryException queried */ - $exception = new Exception('test'); - - $this->registry->registerLazy($reporterClass); - $this->registry->delegateReport($exception); - $this->addToAssertionCount(1); - } - - public function testDelegateBreadcrumbCollection(): void { - $reporter1 = $this->createMock(IReporter::class); - $reporter2 = $this->createMock(ICollectBreadcrumbs::class); - $message = 'hello'; - $category = 'log'; - $reporter2->expects($this->once()) - ->method('collect') - ->with($message, $category); - - $this->registry->register($reporter1); - $this->registry->register($reporter2); - $this->registry->delegateBreadcrumb($message, $category); - } - - public function testDelegateToAll(): void { - $reporter1 = $this->createMock(IReporter::class); - $reporter2 = $this->createMock(IReporter::class); - $exception = new Exception('test'); - $reporter1->expects($this->once()) - ->method('report') - ->with($exception); - $reporter2->expects($this->once()) - ->method('report') - ->with($exception); - - $this->registry->register($reporter1); - $this->registry->register($reporter2); - $this->registry->delegateReport($exception); - } - - public function testDelegateMessage(): void { - $reporter1 = $this->createMock(IReporter::class); - $reporter2 = $this->createMock(IMessageReporter::class); - $message = 'hello'; - $reporter2->expects($this->once()) - ->method('reportMessage') - ->with($message, []); - - $this->registry->register($reporter1); - $this->registry->register($reporter2); - $this->registry->delegateMessage($message); - } -} --- a/tests/lib/Support/Subscription/DummySubscription.php +++ /dev/null @@ -1,48 +0,0 @@ -hasValidSubscription; - } - - /** - * @inheritDoc - */ - #[\Override] - public function hasExtendedSupport(): bool { - return $this->hasExtendedSupport; - } - - #[\Override] - public function isHardUserLimitReached(): bool { - return $this->isHardUserLimitReached; - } -} --- a/tests/lib/Support/Subscription/RegistryTest.php +++ /dev/null @@ -1,199 +0,0 @@ -config = $this->createMock(IConfig::class); - $this->serverContainer = $this->createMock(ContainerInterface::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->notificationManager = $this->createMock(IManager::class); - $this->registry = new Registry( - $this->config, - $this->serverContainer, - $this->userManager, - $this->groupManager, - $this->logger - ); - } - - /** - * Doesn't assert anything, just checks whether anything "explodes" - */ - public function testDelegateToNone(): void { - $this->registry->delegateHasValidSubscription(); - $this->addToAssertionCount(1); - } - - public function testDoubleRegistration(): void { - $this->expectException(AlreadyRegisteredException::class); - - /* @var ISubscription $subscription1 */ - $subscription1 = $this->createMock(ISubscription::class); - /* @var ISubscription $subscription2 */ - $subscription2 = $this->createMock(ISubscription::class); - $this->registry->register($subscription1); - $this->registry->register($subscription2); - } - - public function testNoSupportApp(): void { - $this->assertSame([], $this->registry->delegateGetSupportedApps()); - $this->assertSame(false, $this->registry->delegateHasValidSubscription()); - } - - public function testDelegateHasValidSubscription(): void { - /* @var ISubscription|\PHPUnit\Framework\MockObject\MockObject $subscription */ - $subscription = $this->createMock(ISubscription::class); - $subscription->expects($this->once()) - ->method('hasValidSubscription') - ->willReturn(true); - $this->registry->register($subscription); - - $this->assertSame(true, $this->registry->delegateHasValidSubscription()); - } - - public function testDelegateHasValidSubscriptionConfig(): void { - /* @var ISubscription|\PHPUnit\Framework\MockObject\MockObject $subscription */ - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('has_valid_subscription') - ->willReturn(true); - - $this->assertSame(true, $this->registry->delegateHasValidSubscription()); - } - - public function testDelegateHasExtendedSupport(): void { - /* @var ISubscription|\PHPUnit\Framework\MockObject\MockObject $subscription */ - $subscription = $this->createMock(ISubscription::class); - $subscription->expects($this->once()) - ->method('hasExtendedSupport') - ->willReturn(true); - $this->registry->register($subscription); - - $this->assertSame(true, $this->registry->delegateHasExtendedSupport()); - } - - public function testDelegateGetSupportedApps(): void { - /* @var ISupportedApps|\PHPUnit\Framework\MockObject\MockObject $subscription */ - $subscription = $this->createMock(ISupportedApps::class); - $subscription->expects($this->once()) - ->method('getSupportedApps') - ->willReturn(['abc']); - $this->registry->register($subscription); - - $this->assertSame(['abc'], $this->registry->delegateGetSupportedApps()); - } - - public function testSubscriptionService(): void { - $this->serverContainer->method('get') - ->with(DummySubscription::class) - ->willReturn(new DummySubscription(true, false, false)); - $this->registry->registerService(DummySubscription::class); - - $this->assertTrue($this->registry->delegateHasValidSubscription()); - $this->assertFalse($this->registry->delegateHasExtendedSupport()); - } - - public function testDelegateIsHardUserLimitReached(): void { - /* @var ISubscription|\PHPUnit\Framework\MockObject\MockObject $subscription */ - $subscription = $this->createMock(ISubscription::class); - $subscription->expects($this->once()) - ->method('hasValidSubscription') - ->willReturn(true); - $subscription->expects($this->once()) - ->method('isHardUserLimitReached') - ->willReturn(true); - $this->registry->register($subscription); - $dummyGroup = $this->createMock(IGroup::class); - $dummyGroup->expects($this->once()) - ->method('getUsers') - ->willReturn([]); - $this->groupManager->expects($this->once()) - ->method('get') - ->willReturn($dummyGroup); - - $this->assertSame(true, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); - } - - public function testDelegateIsHardUserLimitReachedWithoutSupportApp(): void { - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('one-click-instance') - ->willReturn(false); - - $this->assertSame(false, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); - } - - public static function dataForUserLimitCheck(): array { - return [ - // $userLimit, $userCount, $disabledUsers, $expectedResult - [35, 15, 2, false], - [35, 45, 15, false], - [35, 45, 5, true], - [35, 45, 55, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('dataForUserLimitCheck')] - public function testDelegateIsHardUserLimitReachedWithoutSupportAppAndUserCount($userLimit, $userCount, $disabledUsers, $expectedResult): void { - $this->config->expects($this->once()) - ->method('getSystemValueBool') - ->with('one-click-instance') - ->willReturn(true); - $this->config->expects($this->once()) - ->method('getSystemValueInt') - ->with('one-click-instance.user-limit') - ->willReturn($userLimit); - $this->config->expects($this->once()) - ->method('getUsersForUserValue') - ->with('core', 'enabled', 'false') - ->willReturn(array_fill(0, $disabledUsers, '')); - $this->userManager->expects($this->once()) - ->method('countUsersTotal') - ->willReturn($userCount); - - if ($expectedResult) { - $dummyGroup = $this->createMock(IGroup::class); - $dummyGroup->expects($this->once()) - ->method('getUsers') - ->willReturn([]); - $this->groupManager->expects($this->once()) - ->method('get') - ->willReturn($dummyGroup); - } - - $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); - } -} --- a/tests/lib/SystemConfigTest.php +++ /dev/null @@ -1,50 +0,0 @@ -config = $this->createMock(Config::class); - } - - public function testGetFilteredValueMasksTheEuroOfficeSecret(): void { - $this->config->method('getValue') - ->willReturnMap([ - ['config_extra_sensitive_values', [], []], - ['eurooffice', '', [ - 'editors_check_interval' => 0, - 'jwt_secret' => 'a-document-server-signing-key', - 'jwt_header' => 'AuthorizationJwt', - ]], - ]); - - $systemConfig = new SystemConfig($this->config); - - $this->assertSame([ - 'editors_check_interval' => 0, - 'jwt_secret' => IConfig::SENSITIVE_VALUE, - 'jwt_header' => 'AuthorizationJwt', - ], $systemConfig->getFilteredValue('eurooffice')); - } -} --- a/tests/lib/SystemReport/SystemReportManagerTest.php +++ /dev/null @@ -1,117 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->manager = new SystemReportManager( - $this->coordinator, - $this->container, - $this->logger, - ); - } - - /** - * @param \OC\AppFramework\Bootstrap\ServiceRegistration[] $registrations - */ - private function withRegisteredSections(array $registrations): void { - $context = $this->createMock(RegistrationContext::class); - $context->expects(self::atLeastOnce()) - ->method('getSystemReportSections') - ->willReturn($registrations); - - $this->coordinator->expects(self::atLeastOnce()) - ->method('getRegistrationContext') - ->willReturn($context); - } - - public function testGetSectionsReturnsNoneWhenNoneRegistered(): void { - $this->withRegisteredSections([]); - - $this->assertSame([], $this->manager->getSections()); - } - - public function testGetSectionsResolvesAndReturnsRegisteredSections(): void { - $section = $this->createMock(ISystemReportSection::class); - $section->expects(self::once()) - ->method('getDetails') - ->willReturn([]); - - $this->container->expects(self::once()) - ->method('get') - ->with($section::class) - ->willReturn($section); - - $this->withRegisteredSections([ - new ServiceRegistration('testing', $section::class), - ]); - - $this->assertSame([$section], $this->manager->getSections()); - } - - public function testGetSectionsSkipsSectionThrowingDuringCollection(): void { - $section = $this->createMock(ISystemReportSection::class); - $section->method('getDetails') - ->willThrowException(new \RuntimeException('boom')); - - $this->container->method('get') - ->with($section::class) - ->willReturn($section); - - $this->withRegisteredSections([ - new ServiceRegistration('testing', $section::class), - ]); - - $this->logger->expects(self::once()) - ->method('error'); - - $this->assertSame([], $this->manager->getSections()); - } - - public function testGetSectionsSkipsUnresolvableClass(): void { - $this->container->method('get') - ->with(\stdClass::class) - ->willThrowException($this->createStub(NotFoundExceptionInterface::class)); - - $this->withRegisteredSections([ - new ServiceRegistration('testing', \stdClass::class), - ]); - - $this->logger->expects(self::once()) - ->method('error'); - - $this->assertSame([], $this->manager->getSections()); - } -} --- a/tests/lib/SystemTag/SystemTagManagerTest.php +++ /dev/null @@ -1,586 +0,0 @@ -connection = Server::get(IDBConnection::class); - - $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->groupManager = $this->createMock(IGroupManager::class); - $this->userSession = $this->createMock(IUserSession::class); - $this->appConfig = $this->createMock(IAppConfig::class); - - $this->tagManager = new SystemTagManager( - $this->connection, - $this->groupManager, - $this->dispatcher, - $this->userSession, - $this->appConfig, - ); - $this->pruneTagsTables(); - } - - #[\Override] - protected function tearDown(): void { - $this->pruneTagsTables(); - \OC::$CLI = true; - parent::tearDown(); - } - - protected function pruneTagsTables() { - $query = $this->connection->getQueryBuilder(); - $query->delete(SystemTagObjectMapper::RELATION_TABLE)->executeStatement(); - $query->delete(SystemTagManager::TAG_TABLE)->executeStatement(); - } - - public static function getAllTagsDataProvider(): array { - return [ - [ - // no tags at all - [] - ], - [ - // simple - [ - ['one', false, false], - ['two', false, false], - ] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('getAllTagsDataProvider')] - public function testGetAllTags($testTags): void { - $testTagsById = []; - foreach ($testTags as $testTag) { - $tag = $this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]); - $testTagsById[$tag->getId()] = $tag; - } - - $tagList = $this->tagManager->getAllTags(); - - $this->assertCount(count($testTags), $tagList); - - foreach ($testTagsById as $testTagId => $testTag) { - $this->assertTrue(isset($tagList[$testTagId])); - $this->assertSameTag($tagList[$testTagId], $testTag); - } - } - - public static function getAllTagsFilteredDataProvider(): array { - return [ - [ - [ - // no tags at all - ], - null, - null, - [] - ], - // filter by visible only - [ - // none visible - [ - ['one', false, false], - ['two', false, false], - ], - true, - null, - [] - ], - [ - // one visible - [ - ['one', true, false], - ['two', false, false], - ], - true, - null, - [ - ['one', true, false], - ] - ], - [ - // one invisible - [ - ['one', true, false], - ['two', false, false], - ], - false, - null, - [ - ['two', false, false], - ] - ], - // filter by name pattern - [ - [ - ['one', true, false], - ['one_different', false, false], - ['two', true, false], - ], - null, - 'on', - [ - ['one', true, false], - ['one_different', false, false], - ] - ], - // filter by name pattern and visibility - [ - // one visible - [ - ['one', true, false], - ['two', true, false], - ['one_different', false, false], - ], - true, - 'on', - [ - ['one', true, false], - ] - ], - // filter by name pattern in the middle - [ - // one visible - [ - ['abcdefghi', true, false], - ['two', true, false], - ], - null, - 'def', - [ - ['abcdefghi', true, false], - ] - ] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('getAllTagsFilteredDataProvider')] - public function testGetAllTagsFiltered($testTags, $visibilityFilter, $nameSearch, $expectedResults): void { - foreach ($testTags as $testTag) { - $this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]); - } - - $testTagsById = []; - foreach ($expectedResults as $expectedTag) { - $tag = $this->tagManager->getTag($expectedTag[0], $expectedTag[1], $expectedTag[2]); - $testTagsById[$tag->getId()] = $tag; - } - - $tagList = $this->tagManager->getAllTags($visibilityFilter, $nameSearch); - - $this->assertCount(count($testTagsById), $tagList); - - foreach ($testTagsById as $testTagId => $testTag) { - $this->assertTrue(isset($tagList[$testTagId])); - $this->assertSameTag($tagList[$testTagId], $testTag); - } - } - - public static function oneTagMultipleFlagsProvider(): array { - return [ - ['one', false, false], - ['one', true, false], - ['one', false, true], - ['one', true, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('oneTagMultipleFlagsProvider')] - public function testCreateDuplicate($name, $userVisible, $userAssignable): void { - $this->expectException(TagAlreadyExistsException::class); - - try { - $this->tagManager->createTag($name, $userVisible, $userAssignable); - } catch (\Exception $e) { - $this->assertTrue(false, 'No exception thrown for the first create call'); - } - $this->tagManager->createTag($name, $userVisible, $userAssignable); - } - - public function testCreateDuplicateWithDifferentFlags(): void { - $this->expectException(TagAlreadyExistsException::class); - - // Create a tag with specific flags - $this->tagManager->createTag('duplicate', true, false); - // Try to create a tag with the same name but different flags - should fail - $this->tagManager->createTag('duplicate', false, true); - } - - public function testCreateOverlongName(): void { - $tag = $this->tagManager->createTag('Zona circundante do Palácio Nacional da Ajuda (Jardim das Damas, Salão de Física, Torre Sineira, Paço Velho e Jardim Botânico)', true, true); - $this->assertSame('Zona circundante do Palácio Nacional da Ajuda (Jardim das Damas', $tag->getName()); // 63 characters but 64 bytes due to "á" - } - - #[\PHPUnit\Framework\Attributes\DataProvider('oneTagMultipleFlagsProvider')] - public function testGetExistingTag($name, $userVisible, $userAssignable): void { - $tag1 = $this->tagManager->createTag($name, $userVisible, $userAssignable); - $tag2 = $this->tagManager->getTag($name, $userVisible, $userAssignable); - - $this->assertSameTag($tag1, $tag2); - } - - public function testGetExistingTagById(): void { - $tag1 = $this->tagManager->createTag('one', true, false); - $tag2 = $this->tagManager->createTag('two', false, true); - - $tagList = $this->tagManager->getTagsByIds([$tag1->getId(), $tag2->getId()]); - - $this->assertCount(2, $tagList); - - $this->assertSameTag($tag1, $tagList[$tag1->getId()]); - $this->assertSameTag($tag2, $tagList[$tag2->getId()]); - } - - public function testGetNonExistingTag(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagManager->getTag('nonexist', false, false); - } - - public function testGetNonExistingTagsById(): void { - $this->expectException(TagNotFoundException::class); - - $tag1 = $this->tagManager->createTag('one', true, false); - $this->tagManager->getTagsByIds([$tag1->getId(), 100, 101]); - } - - public function testGetInvalidTagIdFormat(): void { - $this->expectException(\InvalidArgumentException::class); - - $tag1 = $this->tagManager->createTag('one', true, false); - $this->tagManager->getTagsByIds([$tag1->getId() . 'suffix']); - } - - public static function updateTagProvider(): array { - return [ - [ - // update name - ['one', true, true], - ['two', true, true, '0082c9'] - ], - [ - // update one flag - ['one', false, true], - ['one', true, true, '0082c9'] - ], - [ - // update all flags - ['one', false, false], - ['one', true, true, null] - ], - [ - // update all - ['one', false, false], - ['two', true, true, '0082c9'] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('updateTagProvider')] - public function testUpdateTag($tagCreate, $tagUpdated): void { - $tag1 = $this->tagManager->createTag( - $tagCreate[0], - $tagCreate[1], - $tagCreate[2], - ); - $this->tagManager->updateTag( - $tag1->getId(), - $tagUpdated[0], - $tagUpdated[1], - $tagUpdated[2], - $tagUpdated[3], - ); - $tag2 = $this->tagManager->getTag( - $tagUpdated[0], - $tagUpdated[1], - $tagUpdated[2], - ); - - $this->assertEquals($tag2->getId(), $tag1->getId()); - $this->assertEquals($tag2->getName(), $tagUpdated[0]); - $this->assertEquals($tag2->isUserVisible(), $tagUpdated[1]); - $this->assertEquals($tag2->isUserAssignable(), $tagUpdated[2]); - $this->assertEquals($tag2->getColor(), $tagUpdated[3]); - - } - - public function testUpdateTagToExistingName(): void { - $this->expectException(TagAlreadyExistsException::class); - - // Create two different tags - $tag1 = $this->tagManager->createTag('first', true, true); - $tag2 = $this->tagManager->createTag('second', false, false); - - // Try to update tag2 to have the same name as tag1 - should fail - $this->tagManager->updateTag( - $tag2->getId(), - 'first', - false, - false, - null - ); - } - - public function testDeleteTags(): void { - $tag1 = $this->tagManager->createTag('one', true, false); - $tag2 = $this->tagManager->createTag('two', false, true); - - $this->tagManager->deleteTags([$tag1->getId(), $tag2->getId()]); - - $this->assertEmpty($this->tagManager->getAllTags()); - } - - public function testDeleteNonExistingTag(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagManager->deleteTags([100]); - } - - public function testDeleteTagRemovesRelations(): void { - $tag1 = $this->tagManager->createTag('one', true, false); - $tag2 = $this->tagManager->createTag('two', true, true); - - $tagMapper = new SystemTagObjectMapper($this->connection, $this->tagManager, $this->dispatcher); - - $tagMapper->assignTags(1, 'testtype', $tag1->getId()); - $tagMapper->assignTags(1, 'testtype', $tag2->getId()); - $tagMapper->assignTags(2, 'testtype', $tag1->getId()); - - $this->tagManager->deleteTags($tag1->getId()); - - $tagIdMapping = $tagMapper->getTagIdsForObjects( - [1, 2], - 'testtype' - ); - - $this->assertEquals([ - 1 => [$tag2->getId()], - 2 => [], - ], $tagIdMapping); - } - - public static function visibilityCheckProvider(): array { - return [ - [false, false, false, false], - [true, false, false, true], - [false, false, true, true], - [true, false, true, true], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('visibilityCheckProvider')] - public function testVisibilityCheck($userVisible, $userAssignable, $isAdmin, $expectedResult): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('test'); - $tag1 = $this->tagManager->createTag('one', $userVisible, $userAssignable); - - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->with('test') - ->willReturn($isAdmin); - - $this->assertEquals($expectedResult, $this->tagManager->canUserSeeTag($tag1, $user)); - } - - public static function assignabilityCheckProvider(): array { - return [ - // no groups - [false, false, false, false], - [true, false, false, false], - [true, true, false, true], - [false, true, false, false], - // admin rulez - [false, false, true, true], - [false, true, true, true], - [true, false, true, true], - [true, true, true, true], - // ignored groups - [false, false, false, false, ['group1'], ['group1']], - [true, true, false, true, ['group1'], ['group1']], - [true, true, false, true, ['group1'], ['anothergroup']], - [false, true, false, false, ['group1'], ['group1']], - // admin has precedence over groups - [false, false, true, true, ['group1'], ['anothergroup']], - [false, true, true, true, ['group1'], ['anothergroup']], - [true, false, true, true, ['group1'], ['anothergroup']], - [true, true, true, true, ['group1'], ['anothergroup']], - // groups only checked when visible and user non-assignable and non-admin - [true, false, false, false, ['group1'], ['anothergroup1']], - [true, false, false, true, ['group1'], ['group1']], - [true, false, false, true, ['group1', 'group2'], ['group2', 'group3']], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('assignabilityCheckProvider')] - public function testAssignabilityCheck($userVisible, $userAssignable, $isAdmin, $expectedResult, $userGroupIds = [], $tagGroupIds = []): void { - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('test'); - $tag1 = $this->tagManager->createTag('one', $userVisible, $userAssignable); - $this->tagManager->setTagGroups($tag1, $tagGroupIds); - - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->with('test') - ->willReturn($isAdmin); - $this->groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn($userGroupIds); - - $this->assertEquals($expectedResult, $this->tagManager->canUserAssignTag($tag1, $user)); - } - - public function testTagGroups(): void { - $tag1 = $this->tagManager->createTag('tag1', true, false); - $tag2 = $this->tagManager->createTag('tag2', true, false); - $this->tagManager->setTagGroups($tag1, ['group1', 'group2']); - $this->tagManager->setTagGroups($tag2, ['group2', 'group3']); - - $this->assertEquals(['group1', 'group2'], $this->tagManager->getTagGroups($tag1)); - $this->assertEquals(['group2', 'group3'], $this->tagManager->getTagGroups($tag2)); - - // change groups - $this->tagManager->setTagGroups($tag1, ['group3', 'group4']); - $this->tagManager->setTagGroups($tag2, []); - - $this->assertEquals(['group3', 'group4'], $this->tagManager->getTagGroups($tag1)); - $this->assertEquals([], $this->tagManager->getTagGroups($tag2)); - } - - /** - * empty groupIds should be ignored - */ - public function testEmptyTagGroup(): void { - $tag1 = $this->tagManager->createTag('tag1', true, false); - $this->tagManager->setTagGroups($tag1, ['']); - $this->assertEquals([], $this->tagManager->getTagGroups($tag1)); - } - - public static function allowedToCreateProvider(): array { - return [ - [true, null, true], - [true, null, false], - [false, true, true], - [false, true, false], - [false, false, false], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('allowedToCreateProvider')] - public function testAllowedToCreateTag(bool $isCli, ?bool $isAdmin, bool $isRestricted): void { - $oldCli = \OC::$CLI; - \OC::$CLI = $isCli; - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('test'); - $this->userSession->expects($this->any()) - ->method('getUser') - ->willReturn($isAdmin === null ? null : $user); - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->with('test') - ->willReturn($isAdmin); - $this->appConfig->expects($this->any()) - ->method('getValueBool') - ->with('systemtags', 'restrict_creation_to_admin') - ->willReturn($isRestricted); - - $name = uniqid('tag_', true); - $tag = $this->tagManager->createTag($name, true, true); - $this->assertEquals($tag->getName(), $name); - $this->tagManager->deleteTags($tag->getId()); - - \OC::$CLI = $oldCli; - } - - public static function disallowedToCreateProvider(): array { - return [ - [false], - [null], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('disallowedToCreateProvider')] - public function testDisallowedToCreateTag(?bool $isAdmin): void { - $oldCli = \OC::$CLI; - \OC::$CLI = false; - - $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->willReturn('test'); - $this->userSession->expects($this->any()) - ->method('getUser') - ->willReturn($isAdmin === null ? null : $user); - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->with('test') - ->willReturn($isAdmin); - $this->appConfig->expects($this->any()) - ->method('getValueBool') - ->with('systemtags', 'restrict_creation_to_admin') - ->willReturn(true); - - $this->expectException(\Exception::class); - $tag = $this->tagManager->createTag(uniqid('tag_', true), true, true); - - \OC::$CLI = $oldCli; - } - - /** - * @param ISystemTag $tag1 - * @param ISystemTag $tag2 - */ - private function assertSameTag($tag1, $tag2) { - $this->assertEquals($tag1->getId(), $tag2->getId()); - $this->assertEquals($tag1->getName(), $tag2->getName()); - $this->assertEquals($tag1->isUserVisible(), $tag2->isUserVisible()); - $this->assertEquals($tag1->isUserAssignable(), $tag2->isUserAssignable()); - } -} --- a/tests/lib/SystemTag/SystemTagObjectMapperTest.php +++ /dev/null @@ -1,459 +0,0 @@ -connection = Server::get(IDBConnection::class); - $this->pruneTagsTables(); - - $this->tagManager = $this->createMock(ISystemTagManager::class); - $this->dispatcher = $this->createMock(IEventDispatcher::class); - - $this->tagMapper = new SystemTagObjectMapper( - $this->connection, - $this->tagManager, - $this->dispatcher - ); - - $this->tag1 = new SystemTag(1, 'testtag1', false, false); - $this->tag2 = new SystemTag(2, 'testtag2', true, false); - $this->tag3 = new SystemTag(3, 'testtag3', false, false); - - $this->tagManager->expects($this->any()) - ->method('getTagsByIds') - ->willReturnCallback(function ($tagIds) { - $result = []; - if (in_array(1, $tagIds)) { - $result[1] = $this->tag1; - } - if (in_array(2, $tagIds)) { - $result[2] = $this->tag2; - } - if (in_array(3, $tagIds)) { - $result[3] = $this->tag3; - } - return $result; - }); - - $this->tagMapper->assignTags('1', 'testtype', $this->tag1->getId()); - $this->tagMapper->assignTags('1', 'testtype', $this->tag2->getId()); - $this->tagMapper->assignTags('2', 'testtype', $this->tag1->getId()); - $this->tagMapper->assignTags('3', 'anothertype', $this->tag1->getId()); - } - - #[\Override] - protected function tearDown(): void { - $this->pruneTagsTables(); - parent::tearDown(); - } - - protected function pruneTagsTables() { - $query = $this->connection->getQueryBuilder(); - $query->delete(SystemTagObjectMapper::RELATION_TABLE)->executeStatement(); - $query->delete(SystemTagManager::TAG_TABLE)->executeStatement(); - } - - public function testGetTagIdsForObjects(): void { - $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - ['1', '2', '3', '4'], - 'testtype' - ); - - $this->assertEquals([ - '1' => [$this->tag1->getId(), $this->tag2->getId()], - '2' => [$this->tag1->getId()], - '3' => [], - '4' => [], - ], $tagIdMapping); - } - - public function testGetTagIdsForNoObjects(): void { - $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - [], - 'testtype' - ); - - $this->assertEquals([], $tagIdMapping); - } - - public function testGetTagIdsForALotOfObjects(): void { - $ids = range(1, 10500); - $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - $ids, - 'testtype' - ); - - $this->assertEquals(10500, count($tagIdMapping)); - $this->assertEquals([$this->tag1->getId(), $this->tag2->getId()], $tagIdMapping[1]); - } - - public function testGetObjectsForTags(): void { - $objectIds = $this->tagMapper->getObjectIdsForTags( - [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], - 'testtype' - ); - sort($objectIds); - - $this->assertEquals([ - '1', - '2', - ], $objectIds); - } - - public function testGetObjectsForTagsLimit(): void { - $objectIds = $this->tagMapper->getObjectIdsForTags( - [$this->tag1->getId()], - 'testtype', - 1 - ); - - $this->assertEquals([ - 1, - ], $objectIds); - } - - public function testGetObjectsForTagsLimitWithMultipleTags(): void { - $this->expectException(\InvalidArgumentException::class); - - $this->tagMapper->getObjectIdsForTags( - [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], - 'testtype', - 1 - ); - } - - public function testGetObjectsForTagsLimitOffset(): void { - $objectIds = $this->tagMapper->getObjectIdsForTags( - [$this->tag1->getId()], - 'testtype', - 1, - '1' - ); - - $this->assertEquals([ - 2, - ], $objectIds); - } - - public function testGetObjectsForNonExistingTag(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagMapper->getObjectIdsForTags( - [100], - 'testtype' - ); - } - - public function testAssignUnassignTags(): void { - $event = null; - $this->dispatcher->expects($this->any())->method('dispatchTyped')->willReturnCallback(function (Event $e) use (&$event): void { - $event = $e; - }); - - $this->tagMapper->unassignTags('1', 'testtype', [$this->tag1->getId()]); - - $this->assertNotNull($event); - $this->assertEquals(TagUnassignedEvent::class, $event::class); - $this->assertEquals('testtype', $event->getObjectType()); - $this->assertCount(1, $event->getObjectIds()); - $this->assertEquals('1', current($event->getObjectIds())); - $this->assertCount(1, $event->getTags()); - $this->assertEquals($this->tag1->getId(), current($event->getTags())); - - $tagIdMapping = $this->tagMapper->getTagIdsForObjects('1', 'testtype'); - $this->assertEquals([ - 1 => [$this->tag2->getId()], - ], $tagIdMapping); - - $this->tagMapper->assignTags('1', 'testtype', [$this->tag1->getId()]); - - $this->assertNotNull($event); - $this->assertEquals(TagAssignedEvent::class, $event::class); - $this->assertEquals('testtype', $event->getObjectType()); - $this->assertCount(1, $event->getObjectIds()); - $this->assertEquals('1', current($event->getObjectIds())); - $this->assertCount(1, $event->getTags()); - $this->assertEquals($this->tag1->getId(), current($event->getTags())); - - $this->tagMapper->assignTags('1', 'testtype', $this->tag3->getId()); - - $this->assertNotNull($event); - $this->assertEquals(TagAssignedEvent::class, $event::class); - $this->assertEquals('testtype', $event->getObjectType()); - $this->assertCount(1, $event->getObjectIds()); - $this->assertEquals('1', current($event->getObjectIds())); - $this->assertCount(1, $event->getTags()); - $this->assertEquals($this->tag3->getId(), current($event->getTags())); - - $tagIdMapping = $this->tagMapper->getTagIdsForObjects('1', 'testtype'); - - $this->assertEquals([ - '1' => [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], - ], $tagIdMapping); - } - - public function testSetObjectIdsForTagDispatchesUnassignEventOncePerObject(): void { - $unassignedObjectIds = []; - $this->dispatcher->expects($this->any())->method('dispatch')->willReturnCallback( - function (string $eventName, Event $event) use (&$unassignedObjectIds): void { - if ($eventName === MapperEvent::EVENT_UNASSIGN) { - $unassignedObjectIds[] = $event->getObjectId(); - } - } - ); - - // tag1 is assigned to objects '1' and '2', keep only '2' - $this->tagMapper->setObjectIdsForTag((string)$this->tag1->getId(), 'testtype', ['2']); - - $this->assertEquals(['1'], $unassignedObjectIds); - - // same expectation when the new object list is empty - $unassignedObjectIds = []; - $this->tagMapper->setObjectIdsForTag((string)$this->tag1->getId(), 'testtype', []); - - $this->assertEquals(['2'], $unassignedObjectIds); - } - - public function testReAssignUnassignTags(): void { - // reassign tag1 - $this->tagMapper->assignTags('1', 'testtype', [$this->tag1->getId()]); - - // tag 3 was never assigned - $this->tagMapper->unassignTags('1', 'testtype', [$this->tag3->getId()]); - - $this->assertTrue(true, 'No error when reassigning/unassigning'); - } - - public function testAssignNonExistingTags(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagMapper->assignTags('1', 'testtype', [100]); - } - - public function testAssignNonExistingTagInArray(): void { - $caught = false; - try { - $this->tagMapper->assignTags('1', 'testtype', [100, $this->tag3->getId()]); - } catch (TagNotFoundException $e) { - $caught = true; - } - - $this->assertTrue($caught, 'Exception thrown'); - - $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - ['1'], - 'testtype' - ); - - $this->assertEquals([ - '1' => [$this->tag1->getId(), $this->tag2->getId()], - ], $tagIdMapping, 'None of the tags got assigned'); - } - - public function testUnassignNonExistingTags(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagMapper->unassignTags('1', 'testtype', [100]); - } - - public function testUnassignNonExistingTagsInArray(): void { - $caught = false; - try { - $this->tagMapper->unassignTags('1', 'testtype', [100, $this->tag1->getId()]); - } catch (TagNotFoundException $e) { - $caught = true; - } - - $this->assertTrue($caught, 'Exception thrown'); - - $tagIdMapping = $this->tagMapper->getTagIdsForObjects( - [1], - 'testtype' - ); - - $this->assertEquals([ - '1' => [$this->tag1->getId(), $this->tag2->getId()], - ], $tagIdMapping, 'None of the tags got unassigned'); - } - - public function testHaveTagAllMatches(): void { - $this->assertTrue( - $this->tagMapper->haveTag( - ['1'], - 'testtype', - $this->tag1->getId(), - true - ), - 'object 1 has the tag tag1' - ); - - $this->assertTrue( - $this->tagMapper->haveTag( - ['1', '2'], - 'testtype', - $this->tag1->getId(), - true - ), - 'object 1 and object 2 ALL have the tag tag1' - ); - - $this->assertFalse( - $this->tagMapper->haveTag( - ['1', '2'], - 'testtype', - $this->tag2->getId(), - true - ), - 'object 1 has tag2 but not object 2, so not ALL of them' - ); - - $this->assertFalse( - $this->tagMapper->haveTag( - ['2'], - 'testtype', - $this->tag2->getId(), - true - ), - 'object 2 does not have tag2' - ); - - $this->assertFalse( - $this->tagMapper->haveTag( - ['3'], - 'testtype', - $this->tag2->getId(), - true - ), - 'object 3 does not have tag1 due to different type' - ); - } - - public function testHaveTagAtLeastOneMatch(): void { - $this->assertTrue( - $this->tagMapper->haveTag( - ['1'], - 'testtype', - $this->tag1->getId(), - false - ), - 'object1 has the tag tag1' - ); - - $this->assertTrue( - $this->tagMapper->haveTag( - ['1', '2'], - 'testtype', - $this->tag1->getId(), - false - ), - 'object 1 and object 2 both the tag tag1' - ); - - $this->assertTrue( - $this->tagMapper->haveTag( - ['1', '2'], - 'testtype', - $this->tag2->getId(), - false - ), - 'at least object 1 has the tag tag2' - ); - - $this->assertFalse( - $this->tagMapper->haveTag( - ['2'], - 'testtype', - $this->tag2->getId(), - false - ), - 'object 2 does not have tag2' - ); - - $this->assertFalse( - $this->tagMapper->haveTag( - ['3'], - 'testtype', - $this->tag2->getId(), - false - ), - 'object 3 does not have tag1 due to different type' - ); - } - - public function testHaveTagNonExisting(): void { - $this->expectException(TagNotFoundException::class); - - $this->tagMapper->haveTag( - ['1'], - 'testtype', - 100 - ); - } -} --- a/tests/lib/TagsTest.php +++ /dev/null @@ -1,319 +0,0 @@ -clearBackends(); - Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy()); - $userId = $this->getUniqueID('user_'); - Server::get(IUserManager::class)->createUser($userId, 'pass'); - \OC_User::setUserId($userId); - $this->user = $this->createMock(IUser::class); - $this->user->method('getUID') - ->willReturn($userId); - $this->userManager = $this->createMock(IUserManager::class); - $this->userManager - ->expects($this->any()) - ->method('getExistingUser') - ->willReturn($this->user); - $this->userSession = $this->createMock(IUserSession::class); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->willReturn($this->user); - $userFolder = $this->createMock(Folder::class); - $node = $this->createMock(Node::class); - $this->rootFolder = $this->createMock(IRootFolder::class); - $this->rootFolder - ->method('getUserFolder') - ->willReturnCallback(fn () => $userFolder); - $userFolder - ->method('getFirstNodeById') - ->willReturnCallback(fn () => $node); - $node - ->method('getPath') - ->willReturnCallback(fn () => 'file.txt'); - - $this->objectType = $this->getUniqueID('type_'); - $this->tagMapper = Server::get(TagMapper::class); - $this->tagMgr = new TagManager( - $this->tagMapper, - $this->userSession, - $this->userManager, - Server::get(IDBConnection::class), - Server::get(LoggerInterface::class), - Server::get(IEventDispatcher::class), - $this->rootFolder - ); - } - - #[\Override] - protected function tearDown(): void { - $conn = Server::get(IDBConnection::class); - $conn->getQueryBuilder()->delete('vcategory_to_object')->executeStatement(); - $conn->getQueryBuilder()->delete('vcategory')->executeStatement(); - - parent::tearDown(); - } - - public function testTagManagerWithoutUserReturnsNull(): void { - $this->userSession = $this->createMock(IUserSession::class); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->willReturn(null); - $this->tagMgr = new TagManager( - $this->tagMapper, - $this->userSession, - $this->userManager, - Server::get(IDBConnection::class), - Server::get(LoggerInterface::class), - Server::get(IEventDispatcher::class), - $this->rootFolder - ); - $this->assertNull($this->tagMgr->load($this->objectType)); - } - - public function testInstantiateWithDefaults(): void { - $defaultTags = ['Friends', 'Family', 'Work', 'Other']; - - $tagger = $this->tagMgr->load($this->objectType, $defaultTags); - - $this->assertEquals(4, count($tagger->getTags())); - } - - public function testAddTags(): void { - $tags = ['Friends', 'Family', 'Work', 'Other']; - - $tagger = $this->tagMgr->load($this->objectType); - - foreach ($tags as $tag) { - $result = $tagger->add($tag); - $this->assertGreaterThan(0, $result, 'add() returned an ID <= 0'); - $this->assertTrue((bool)$result); - } - - $this->assertFalse($tagger->add('Family')); - $this->assertFalse($tagger->add('fAMILY')); - - $this->assertCount(4, $tagger->getTags(), 'Wrong number of added tags'); - } - - public function testAddMultiple(): void { - $tags = ['Friends', 'Family', 'Work', 'Other']; - - $tagger = $this->tagMgr->load($this->objectType); - - foreach ($tags as $tag) { - $this->assertFalse($tagger->hasTag($tag)); - } - - $result = $tagger->addMultiple($tags); - $this->assertTrue((bool)$result); - - foreach ($tags as $tag) { - $this->assertTrue($tagger->hasTag($tag)); - } - - $tagMaps = $tagger->getTags(); - $this->assertCount(4, $tagMaps, 'Not all tags added'); - foreach ($tagMaps as $tagMap) { - $this->assertEquals(null, $tagMap['id']); - } - - // As addMultiple has been called without $sync=true, the tags aren't - // saved to the database, so they're gone when we reload $tagger: - - $tagger = $this->tagMgr->load($this->objectType); - $this->assertEquals(0, count($tagger->getTags())); - - // Now, we call addMultiple() with $sync=true so the tags will be - // be saved to the database. - $result = $tagger->addMultiple($tags, true); - $this->assertTrue((bool)$result); - - $tagMaps = $tagger->getTags(); - foreach ($tagMaps as $tagMap) { - $this->assertNotEquals(null, $tagMap['id']); - } - - // Reload the tagger. - $tagger = $this->tagMgr->load($this->objectType); - - foreach ($tags as $tag) { - $this->assertTrue($tagger->hasTag($tag)); - } - - $this->assertCount(4, $tagger->getTags(), 'Not all previously saved tags found'); - } - - public function testIsEmpty(): void { - $tagger = $this->tagMgr->load($this->objectType); - - $this->assertEquals(0, count($tagger->getTags())); - $this->assertTrue($tagger->isEmpty()); - - $result = $tagger->add('Tag'); - $this->assertGreaterThan(0, $result, 'add() returned an ID <= 0'); - $this->assertNotEquals(false, $result, 'add() returned false'); - $this->assertFalse($tagger->isEmpty()); - } - - public function testGetTagsForObjects(): void { - $defaultTags = ['Friends', 'Family', 'Work', 'Other']; - $tagger = $this->tagMgr->load($this->objectType, $defaultTags); - - $tagger->tagAs(1, 'Friends'); - $tagger->tagAs(1, 'Other'); - $tagger->tagAs(2, 'Family'); - - $tags = $tagger->getTagsForObjects([1]); - $this->assertEquals(1, count($tags)); - $tags = current($tags); - sort($tags); - $this->assertSame(['Friends', 'Other'], $tags); - - $tags = $tagger->getTagsForObjects([1, 2]); - $this->assertEquals(2, count($tags)); - $tags1 = $tags[1]; - sort($tags1); - $this->assertSame(['Friends', 'Other'], $tags1); - $this->assertSame(['Family'], $tags[2]); - $this->assertEquals( - [], - $tagger->getTagsForObjects([4]) - ); - $this->assertEquals( - [], - $tagger->getTagsForObjects([4, 5]) - ); - } - - public function testGetTagsForObjectsMassiveResults(): void { - $defaultTags = ['tag1']; - $tagger = $this->tagMgr->load($this->objectType, $defaultTags); - $tagData = $tagger->getTags(); - $tagId = $tagData[0]['id']; - $tagType = $tagData[0]['type']; - - $conn = Server::get(IDBConnection::class); - - // insert lots of entries - $idsArray = []; - for ($i = 1; $i <= 1500; $i++) { - $qb = $conn->getQueryBuilder(); - $qb->insert('vcategory_to_object') - ->values([ - 'objid' => $qb->createNamedParameter($i, IQueryBuilder::PARAM_INT), - 'categoryid' => $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_INT), - 'type' => $qb->createNamedParameter($tagType), - ]) - ->executeStatement(); - $idsArray[] = $i; - } - - $tags = $tagger->getTagsForObjects($idsArray); - $this->assertEquals(1500, count($tags)); - } - - public function testDeleteTags(): void { - $defaultTags = ['Friends', 'Family', 'Work', 'Other']; - $tagger = $this->tagMgr->load($this->objectType, $defaultTags); - - $this->assertEquals(4, count($tagger->getTags())); - - $tagger->delete('family'); - $this->assertEquals(3, count($tagger->getTags())); - - $tagger->delete(['Friends', 'Work', 'Other']); - $this->assertEquals(0, count($tagger->getTags())); - } - - public function testRenameTag(): void { - $defaultTags = ['Friends', 'Family', 'Wrok', 'Other']; - $tagger = $this->tagMgr->load($this->objectType, $defaultTags); - - $this->assertTrue($tagger->rename('Wrok', 'Work')); - $this->assertTrue($tagger->hasTag('Work')); - $this->assertFalse($tagger->hasTag('Wrok')); - $this->assertFalse($tagger->rename('Wrok', 'Work')); // Rename non-existant tag. - $this->assertFalse($tagger->rename('Work', 'Family')); // Collide with existing tag. - } - - public function testTagAs(): void { - $objids = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - - $tagger = $this->tagMgr->load($this->objectType); - - foreach ($objids as $id) { - $this->assertTrue($tagger->tagAs($id, 'Family')); - } - - $this->assertEquals(1, count($tagger->getTags())); - $this->assertEquals(9, count($tagger->getIdsForTag('Family'))); - } - - #[\PHPUnit\Framework\Attributes\Depends('testTagAs')] - public function testUnTag(): void { - $objIds = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - - // Is this "legal"? - $this->testTagAs(); - $tagger = $this->tagMgr->load($this->objectType); - - foreach ($objIds as $id) { - $this->assertTrue(in_array($id, $tagger->getIdsForTag('Family'))); - $tagger->unTag($id, 'Family'); - $this->assertFalse(in_array($id, $tagger->getIdsForTag('Family'))); - } - - $this->assertEquals(1, count($tagger->getTags())); - $this->assertEquals(0, count($tagger->getIdsForTag('Family'))); - } - - public function testFavorite(): void { - $tagger = $this->tagMgr->load($this->objectType); - $this->assertTrue($tagger->addToFavorites(1)); - $this->assertEquals([1], $tagger->getFavorites()); - $this->assertTrue($tagger->removeFromFavorites(1)); - $this->assertEquals([], $tagger->getFavorites()); - } -} --- a/tests/lib/Talk/BrokerTest.php +++ /dev/null @@ -1,248 +0,0 @@ -coordinator = $this->createMock(Coordinator::class); - $this->container = $this->createMock(ContainerInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->broker = new Broker( - $this->coordinator, - $this->container, - $this->logger, - ); - } - - public function testHasNoBackendCalledTooEarly(): void { - $this->expectException(RuntimeException::class); - - $this->broker->hasBackend(); - } - - public function testHasNoBackend(): void { - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($this->createMock(RegistrationContext::class)); - - self::assertFalse( - $this->broker->hasBackend() - ); - } - - public function testHasFaultyBackend(): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $this->container->expects($this->once()) - ->method('get') - ->willThrowException(new QueryException()); - $this->logger->expects($this->once()) - ->method('error'); - - self::assertFalse( - $this->broker->hasBackend() - ); - } - - public function testHasBackend(): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $talkService = $this->createMock(ITalkBackend::class); - $this->container->expects($this->once()) - ->method('get') - ->with($fakeTalkServiceClass) - ->willReturn($talkService); - - self::assertTrue( - $this->broker->hasBackend() - ); - } - - public function testNewConversationOptions(): void { - $options = $this->broker->newConversationOptions(); - - $this->assertInstanceOf(IConversationOptions::class, $options); - } - - public function testCreateConversation(): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $talkService = $this->createMock(ITalkBackend::class); - $this->container->expects($this->once()) - ->method('get') - ->with($fakeTalkServiceClass) - ->willReturn($talkService); - $options = $this->createMock(IConversationOptions::class); - $talkService->expects($this->once()) - ->method('createConversation') - ->with('Watercooler', [], $options); - - $this->broker->createConversation( - 'Watercooler', - [], - $options - ); - } - - public function testIsEnabledForUserNoBackend(): void { - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($this->createMock(RegistrationContext::class)); - - self::assertFalse( - $this->broker->isEnabledForUser() - ); - } - - public static function dataIsEnabledForUser(): array { - return [ - [true], - [false], - ]; - } - - #[DataProvider('dataIsEnabledForUser')] - public function testIsEnabledForUser(bool $enabled): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $talkService = $this->createMock(ITalkBackend::class); - $this->container->expects($this->once()) - ->method('get') - ->with($fakeTalkServiceClass) - ->willReturn($talkService); - $talkService->expects($this->once()) - ->method('isEnabledForUser') - ->willReturn($enabled); - - self::assertSame( - $enabled, - $this->broker->isEnabledForUser() - ); - } - - public function testIsAllowedToCreateConversationsNoBackend(): void { - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($this->createMock(RegistrationContext::class)); - - self::assertFalse( - $this->broker->isAllowedToCreateConversations() - ); - } - - public function testIsAllowedToCreateConversationsBackendDisabled(): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $talkService = $this->createMock(ITalkBackend::class); - $this->container->expects($this->once()) - ->method('get') - ->with($fakeTalkServiceClass) - ->willReturn($talkService); - $talkService->expects($this->once()) - ->method('isEnabledForUser') - ->willReturn(false); - $talkService->expects($this->never()) - ->method('isAllowedToCreateConversations'); - - self::assertFalse( - $this->broker->isAllowedToCreateConversations() - ); - } - - public static function dataIsAllowedToCreateConversations(): array { - return [ - [true], - [false], - ]; - } - - #[DataProvider('dataIsAllowedToCreateConversations')] - public function testIsAllowedToCreateConversations(bool $allowed): void { - $fakeTalkServiceClass = '\\OCA\\Spreed\\TalkBackend'; - $registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator->expects($this->once()) - ->method('getRegistrationContext') - ->willReturn($registrationContext); - $registrationContext->expects($this->once()) - ->method('getTalkBackendRegistration') - ->willReturn(new ServiceRegistration('spreed', $fakeTalkServiceClass)); - $talkService = $this->createMock(ITalkBackend::class); - $this->container->expects($this->once()) - ->method('get') - ->with($fakeTalkServiceClass) - ->willReturn($talkService); - $talkService->expects($this->once()) - ->method('isEnabledForUser') - ->willReturn(true); - $talkService->expects($this->once()) - ->method('isAllowedToCreateConversations') - ->willReturn($allowed); - - self::assertSame( - $allowed, - $this->broker->isAllowedToCreateConversations() - ); - } -} --- a/tests/lib/Talk/ConversationOptionsTest.php +++ /dev/null @@ -1,21 +0,0 @@ -addToAssertionCount(1); - } -} --- a/tests/lib/TaskProcessing/SkipLockedSqlShapeTest.php +++ /dev/null @@ -1,90 +0,0 @@ -systemConfig = $this->createMock(SystemConfig::class); - $this->logger = $this->createMock(LoggerInterface::class); - } - - /** - * Build a QueryBuilder backed by a non-SQLite (MySQL 8) platform so the - * generated SQL exposes the locking clause the way it would in production. - */ - private function newMysqlQueryBuilder(): QueryBuilder { - $inner = $this->createMock(Connection::class); - $inner->method('getDatabasePlatform')->willReturn(new MySQL80Platform()); - - $adapter = $this->createMock(ConnectionAdapter::class); - $adapter->method('getInner')->willReturn($inner); - $adapter->method('getDatabaseProvider')->willReturn(IDBConnection::PLATFORM_MYSQL); - - return new QueryBuilder($adapter, $this->systemConfig, $this->logger); - } - - public function testClaimQueryContainsForUpdateSkipLocked(): void { - $qb = $this->newMysqlQueryBuilder(); - $qb->select('id', 'status', 'type', 'last_updated') - ->from('taskprocessing_tasks') - ->where($qb->expr()->eq('status', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) - ->orderBy('last_updated', 'ASC') - ->setMaxResults(1) - ->forUpdate(ConflictResolutionMode::SkipLocked); - - $sql = $qb->getSQL(); - - self::assertStringContainsString('FOR UPDATE', $sql); - self::assertStringContainsString('SKIP LOCKED', $sql); - } - - public function testOrdinaryForUpdateHasNoSkipLocked(): void { - // Sanity check: only the SkipLocked mode adds the SKIP LOCKED clause. - $qb = $this->newMysqlQueryBuilder(); - $qb->select('id') - ->from('taskprocessing_tasks') - ->setMaxResults(1) - ->forUpdate(ConflictResolutionMode::Ordinary); - - $sql = $qb->getSQL(); - - self::assertStringContainsString('FOR UPDATE', $sql); - self::assertStringNotContainsString('SKIP LOCKED', $sql); - } -} --- a/tests/lib/TaskProcessing/TaskProcessingTest.php +++ /dev/null @@ -1,2005 +0,0 @@ - new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio), - ]; - } - - #[\Override] - public function getOutputShape(): array { - return [ - 'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image), - ]; - } -} - -class AsyncProvider implements IProvider { - #[\Override] - public function getId(): string { - return 'test:sync:success'; - } - - #[\Override] - public function getName(): string { - return self::class; - } - - #[\Override] - public function getTaskTypeId(): string { - return AudioToImage::ID; - } - - #[\Override] - public function getExpectedRuntime(): int { - return 10; - } - - #[\Override] - public function getOptionalInputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getOptionalOutputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class SuccessfulSyncProvider implements IProvider, ISynchronousProvider { - public const ID = 'test:sync:success'; - - #[\Override] - public function getId(): string { - return self::ID; - } - - #[\Override] - public function getName(): string { - return self::class; - } - - #[\Override] - public function getTaskTypeId(): string { - return TextToText::ID; - } - - #[\Override] - public function getExpectedRuntime(): int { - return 10; - } - - #[\Override] - public function getOptionalInputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getOptionalOutputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function process(?string $userId, array $input, callable $reportProgress): array { - return ['output' => $input['input']]; - } - - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class FailingSyncProvider implements IProvider, ISynchronousProvider { - public const ERROR_MESSAGE = 'Failure'; - #[\Override] - public function getId(): string { - return 'test:sync:fail'; - } - - #[\Override] - public function getName(): string { - return self::class; - } - - #[\Override] - public function getTaskTypeId(): string { - return TextToText::ID; - } - - #[\Override] - public function getExpectedRuntime(): int { - return 10; - } - - #[\Override] - public function getOptionalInputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getOptionalOutputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function process(?string $userId, array $input, callable $reportProgress): array { - throw new ProcessingException(self::ERROR_MESSAGE); - } - - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class FailingSyncProviderWithUserFacingError implements IProvider, ISynchronousProvider { - public const ERROR_MESSAGE = 'Failure'; - public const USER_FACING_ERROR_MESSAGE = 'User-facing Failure'; - #[\Override] - public function getId(): string { - return 'test:sync:fail:user-facing'; - } - - #[\Override] - public function getName(): string { - return self::class; - } - - #[\Override] - public function getTaskTypeId(): string { - return TextToText::ID; - } - - #[\Override] - public function getExpectedRuntime(): int { - return 10; - } - - #[\Override] - public function getOptionalInputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getOptionalOutputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function process(?string $userId, array $input, callable $reportProgress): array { - throw new UserFacingProcessingException(self::ERROR_MESSAGE, userFacingMessage: self::USER_FACING_ERROR_MESSAGE); - } - - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class BrokenSyncProvider implements IProvider, ISynchronousProvider { - #[\Override] - public function getId(): string { - return 'test:sync:broken-output'; - } - - #[\Override] - public function getName(): string { - return self::class; - } - - #[\Override] - public function getTaskTypeId(): string { - return TextToText::ID; - } - - #[\Override] - public function getExpectedRuntime(): int { - return 10; - } - - #[\Override] - public function getOptionalInputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function getOptionalOutputShape(): array { - return [ - 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), - ]; - } - - #[\Override] - public function process(?string $userId, array $input, callable $reportProgress): array { - return []; - } - - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider { - public bool $ran = false; - - #[\Override] - public function getName(): string { - return 'TEST Vanilla LLM Provider'; - } - - #[\Override] - public function process(string $prompt): string { - $this->ran = true; - return $prompt . ' Summarize'; - } - - #[\Override] - public function getTaskType(): string { - return SummaryTaskType::class; - } -} - -class FailingTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider { - public bool $ran = false; - - #[\Override] - public function getName(): string { - return 'TEST Vanilla LLM Provider'; - } - - #[\Override] - public function process(string $prompt): string { - $this->ran = true; - throw new \Exception('ERROR'); - } - - #[\Override] - public function getTaskType(): string { - return SummaryTaskType::class; - } -} - -class SuccessfulTextToImageProvider implements \OCP\TextToImage\IProvider { - public bool $ran = false; - - #[\Override] - public function getId(): string { - return 'test:successful'; - } - - #[\Override] - public function getName(): string { - return 'TEST Provider'; - } - - #[\Override] - public function generate(string $prompt, array $resources): void { - $this->ran = true; - foreach ($resources as $resource) { - fwrite($resource, 'test'); - } - } - - #[\Override] - public function getExpectedRuntime(): int { - return 1; - } -} - -class FailingTextToImageProvider implements \OCP\TextToImage\IProvider { - public bool $ran = false; - - #[\Override] - public function getId(): string { - return 'test:failing'; - } - - #[\Override] - public function getName(): string { - return 'TEST Provider'; - } - - #[\Override] - public function generate(string $prompt, array $resources): void { - $this->ran = true; - throw new \RuntimeException('ERROR'); - } - - #[\Override] - public function getExpectedRuntime(): int { - return 1; - } -} - -class ExternalProvider implements IProvider { - public const ID = 'event:external:provider'; - public const TASK_TYPE_ID = 'event:external:tasktype'; - - #[\Override] - public function getId(): string { - return self::ID; - } - #[\Override] - public function getName(): string { - return 'External Provider via Event'; - } - #[\Override] - public function getTaskTypeId(): string { - return self::TASK_TYPE_ID; - } - #[\Override] - public function getExpectedRuntime(): int { - return 5; - } - #[\Override] - public function getOptionalInputShape(): array { - return []; - } - #[\Override] - public function getOptionalOutputShape(): array { - return []; - } - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class ExternalTriggerableProvider implements ITriggerableProvider { - public const ID = 'event:external:provider:triggerable'; - public const TASK_TYPE_ID = TextToText::ID; - - #[\Override] - public function getId(): string { - return self::ID; - } - #[\Override] - public function getName(): string { - return 'External Triggerable Provider via Event'; - } - - #[\Override] - public function getTaskTypeId(): string { - return self::TASK_TYPE_ID; - } - - #[\Override] - public function trigger(): void { - } - #[\Override] - public function getExpectedRuntime(): int { - return 5; - } - #[\Override] - public function getOptionalInputShape(): array { - return []; - } - #[\Override] - public function getOptionalOutputShape(): array { - return []; - } - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class ConflictingExternalProvider implements IProvider { - // Same ID as SuccessfulSyncProvider - public const ID = 'test:sync:success'; - public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type - - #[\Override] - public function getId(): string { - return self::ID; - } - #[\Override] - public function getName(): string { - return 'Conflicting External Provider'; - } - #[\Override] - public function getTaskTypeId(): string { - return self::TASK_TYPE_ID; - } - #[\Override] - public function getExpectedRuntime(): int { - return 50; - } - #[\Override] - public function getOptionalInputShape(): array { - return []; - } - #[\Override] - public function getOptionalOutputShape(): array { - return []; - } - #[\Override] - public function getInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalInputShapeDefaults(): array { - return []; - } - #[\Override] - public function getOutputShapeEnumValues(): array { - return []; - } - #[\Override] - public function getOptionalOutputShapeEnumValues(): array { - return []; - } -} - -class ExternalTaskType implements ITaskType { - public const ID = 'event:external:tasktype'; - - #[\Override] - public function getId(): string { - return self::ID; - } - #[\Override] - public function getName(): string { - return 'External Task Type via Event'; - } - #[\Override] - public function getDescription(): string { - return 'A task type added via event'; - } - #[\Override] - public function getInputShape(): array { - return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)]; - } - #[\Override] - public function getOutputShape(): array { - return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)]; - } -} - -class ConflictingExternalTaskType implements ITaskType { - // Same ID as built-in TextToText - public const ID = TextToText::ID; - - #[\Override] - public function getId(): string { - return self::ID; - } - #[\Override] - public function getName(): string { - return 'Conflicting External Task Type'; - } - #[\Override] - public function getDescription(): string { - return 'Overrides built-in TextToText'; - } - #[\Override] - public function getInputShape(): array { - return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)]; - } - #[\Override] - public function getOutputShape(): array { - return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)]; - } -} - -#[\PHPUnit\Framework\Attributes\Group('DB')] -class TaskProcessingTest extends \Test\TestCase { - private Coordinator&MockObject $coordinator; - private ContainerInterface&MockObject $serverContainer; - private IEventDispatcher&MockObject $eventDispatcher; - private IJobList&MockObject $jobList; - private IUserMountCache&MockObject $userMountCache; - private RegistrationContext&MockObject $registrationContext; - private IRemoteHostValidator&MockObject $remoteHostValidator; - - /** @var list hosts the mocked IRemoteHostValidator rejects */ - private array $invalidRemoteHosts = []; - /** Makes the mocked IRemoteHostValidator reject every host */ - private bool $rejectAllRemoteHosts = false; - - /** @var array */ - private array $providers; - private IAppConfig $appConfig; - private IConfig $config; - private IRootFolder $rootFolder; - private TaskMapper $taskMapper; - private IManager $manager; - - public const TEST_USER = 'testuser'; - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - $this->providers = [ - SuccessfulSyncProvider::class => new SuccessfulSyncProvider(), - FailingSyncProvider::class => new FailingSyncProvider(), - FailingSyncProviderWithUserFacingError::class => new FailingSyncProviderWithUserFacingError(), - BrokenSyncProvider::class => new BrokenSyncProvider(), - AsyncProvider::class => new AsyncProvider(), - AudioToImage::class => new AudioToImage(), - SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(), - FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(), - SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(), - FailingTextToImageProvider::class => new FailingTextToImageProvider(), - ExternalProvider::class => new ExternalProvider(), - ExternalTriggerableProvider::class => new ExternalTriggerableProvider(), - ConflictingExternalProvider::class => new ConflictingExternalProvider(), - ExternalTaskType::class => new ExternalTaskType(), - ConflictingExternalTaskType::class => new ConflictingExternalTaskType(), - ]; - - $userManager = Server::get(IUserManager::class); - if (!$userManager->userExists(self::TEST_USER)) { - $userManager->createUser(self::TEST_USER, 'test'); - } - - $this->serverContainer = $this->createMock(ContainerInterface::class); - $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) { - return $this->providers[$class]; - }); - - $this->registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator = $this->createMock(Coordinator::class); - $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext); - - $this->rootFolder = Server::get(IRootFolder::class); - $this->taskMapper = Server::get(TaskMapper::class); - - $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']); - $this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void { - }); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->configureEventDispatcherMock(); - - $text2imageManager = new \OC\TextToImage\Manager( - $this->serverContainer, - $this->coordinator, - Server::get(LoggerInterface::class), - $this->jobList, - Server::get(\OC\TextToImage\Db\TaskMapper::class), - Server::get(IConfig::class), - Server::get(IAppDataFactory::class), - ); - - $this->userMountCache = $this->createMock(IUserMountCache::class); - $this->invalidRemoteHosts = []; - $this->rejectAllRemoteHosts = false; - $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class); - $this->remoteHostValidator->expects($this->any())->method('isValid') - ->willReturnCallback(fn (string $host): bool => !$this->rejectAllRemoteHosts && !in_array($host, $this->invalidRemoteHosts, true)); - $this->config = Server::get(IConfig::class); - $this->appConfig = Server::get(IAppConfig::class); - $this->manager = new Manager( - $this->appConfig, - $this->coordinator, - $this->serverContainer, - Server::get(LoggerInterface::class), - $this->taskMapper, - $this->jobList, - $this->eventDispatcher, - Server::get(IAppDataFactory::class), - Server::get(IRootFolder::class), - $text2imageManager, - $this->userMountCache, - Server::get(IClientService::class), - Server::get(IAppManager::class), - $userManager, - Server::get(IUserSession::class), - Server::get(ICacheFactory::class), - Server::get(IFactory::class), - Server::get(ITimeFactory::class), - $this->remoteHostValidator, - ); - } - - private function getFile(string $name, string $content): File { - $folder = $this->rootFolder->getUserFolder(self::TEST_USER); - $file = $folder->newFile($name, $content); - return $file; - } - - public function testShouldNotHaveAnyProviders(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); - self::assertCount(0, $this->manager->getAvailableTaskTypes()); - self::assertCount(0, $this->manager->getAvailableTaskTypeIds()); - self::assertFalse($this->manager->hasProviders()); - self::expectException(PreConditionNotMetException::class); - $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null)); - } - - public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $taskProcessingTypeSettings = [ - TextToText::ID => false, - ]; - $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true); - self::assertCount(0, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypes(true)); - self::assertCount(0, $this->manager->getAvailableTaskTypeIds()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true)); - self::assertTrue($this->manager->hasProviders()); - self::expectException(PreConditionNotMetException::class); - $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null)); - } - - public function testProviderShouldBeRegisteredAndTaskFailValidation(): void { - $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', BrokenSyncProvider::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::expectException(ValidationException::class); - $this->manager->scheduleTask($task); - } - - public static function invalidWebhookDataProvider(): array { - return [ - 'uri without method' => ['https://example.com/hook', null], - 'method without uri' => [null, 'HTTP:POST'], - 'empty uri with method' => ['', 'HTTP:POST'], - 'uri with empty method' => ['https://example.com/hook', ''], - 'unknown method prefix' => ['https://example.com/hook', 'FTP:GET'], - 'unknown http verb' => ['https://example.com/hook', 'HTTP:PATCH'], - 'lowercase http verb' => ['https://example.com/hook', 'HTTP:post'], - 'unsupported uri scheme' => ['file:///etc/passwd', 'HTTP:GET'], - 'relative uri for http method' => ['/some/path', 'HTTP:POST'], - 'malformed uri' => ['https://', 'HTTP:POST'], - 'appapi method without exapp id' => ['/some/path', 'AppAPI:POST'], - 'appapi method with too many parts' => ['/some/path', 'AppAPI:my_app:POST:extra'], - 'appapi method with invalid exapp id' => ['/some/path', 'AppAPI:My App:POST'], - 'appapi method with unknown http verb' => ['/some/path', 'AppAPI:my_app:PATCH'], - 'absolute uri for appapi method' => ['https://example.com/hook', 'AppAPI:my_app:POST'], - 'uri too long' => ['https://example.com/' . str_repeat('a', 4000), 'HTTP:POST'], - 'method too long' => ['/some/path', 'AppAPI:' . str_repeat('a', 64) . ':POST'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('invalidWebhookDataProvider')] - public function testProviderShouldBeRegisteredAndWebhookFailValidation(?string $webhookUri, ?string $webhookMethod): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $task->setWebhookUri($webhookUri); - $task->setWebhookMethod($webhookMethod); - self::expectException(ValidationException::class); - $this->manager->scheduleTask($task); - } - - public static function validWebhookDataProvider(): array { - return [ - 'no webhook' => [null, null], - 'empty webhook' => ['', ''], - 'http get' => ['http://example.com/hook', 'HTTP:GET'], - 'https post' => ['https://example.com/hook?foo=bar', 'HTTP:POST'], - 'https put' => ['https://example.com/hook', 'HTTP:PUT'], - 'https delete' => ['https://example.com/hook', 'HTTP:DELETE'], - 'appapi post' => ['/some/path', 'AppAPI:my_app:POST'], - 'appapi get' => ['/', 'AppAPI:my-app2:GET'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('validWebhookDataProvider')] - public function testProviderShouldBeRegisteredAndWebhookPassValidation(?string $webhookUri, ?string $webhookMethod): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $task->setWebhookUri($webhookUri); - $task->setWebhookMethod($webhookMethod); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - // clean up so the scheduled task does not interfere with other tests - $this->manager->deleteTask($task); - } - - public static function localWebhookHostDataProvider(): array { - return [ - 'localhost' => ['http://localhost/hook', 'localhost'], - 'ipv4 loopback' => ['http://127.0.0.1:8080/hook', '127.0.0.1'], - 'ipv6 loopback' => ['http://[::1]/hook', '[::1]'], - 'private network' => ['https://192.168.1.1/hook', '192.168.1.1'], - 'local hostname' => ['https://server.local/hook', 'server.local'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('localWebhookHostDataProvider')] - public function testProviderShouldBeRegisteredAndLocalWebhookHostFailValidation(string $webhookUri, string $host): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $this->invalidRemoteHosts = [$host]; - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $task->setWebhookUri($webhookUri); - $task->setWebhookMethod('HTTP:POST'); - self::expectException(ValidationException::class); - $this->manager->scheduleTask($task); - } - - public function testProviderShouldBeRegisteredAndAppApiWebhookSkipsHostValidation(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - // AppAPI webhooks use an absolute path, so no remote host is involved - $this->rejectAllRemoteHosts = true; - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $task->setWebhookUri('/some/path'); - $task->setWebhookMethod('AppAPI:my_app:POST'); - $this->manager->scheduleTask($task); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - // clean up so the scheduled task does not interfere with other tests - $this->manager->deleteTask($task); - } - - public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ - new ServiceRegistration('test', AudioToImage::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', AsyncProvider::class) - ]); - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('uid'); - $mount = $this->createMock(ICachedMountInfo::class); - $mount->expects($this->any())->method('getUser')->willReturn($user); - $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]); - - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - - $audioId = $this->getFile('audioInput', 'Hello')->getId(); - $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - self::expectException(UnauthorizedException::class); - $this->manager->scheduleTask($task); - } - - public function testProviderShouldBeRegisteredAndFail(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', FailingSyncProvider::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); - self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage()); - } - - public function testProviderShouldBeRegisteredAndFailWithUserFacingMessage(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', FailingSyncProviderWithUserFacingError::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); - self::assertEquals(FailingSyncProviderWithUserFacingError::ERROR_MESSAGE, $task->getErrorMessage()); - self::assertEquals(FailingSyncProviderWithUserFacingError::USER_FACING_ERROR_MESSAGE, $task->getUserFacingErrorMessage()); - } - - public function testProviderShouldBeRegisteredAndFailOutputValidation(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', BrokenSyncProvider::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); - self::assertEquals('The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec', $task->getErrorMessage()); - } - - public function testProviderShouldBeRegisteredAndRun(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - $taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]]; - self::assertTrue(isset($taskTypeStruct['inputShape']['input'])); - self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType()); - self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey'])); - self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType()); - self::assertTrue(isset($taskTypeStruct['outputShape']['output'])); - self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType()); - self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey'])); - self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType()); - - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->manager->getTask($task->getId()); - self::assertEquals($task->getId(), $task2->getId()); - self::assertEquals(['input' => 'Hello'], $task2->getInput()); - self::assertNull($task2->getOutput()); - self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage()); - self::assertEquals(['output' => 'Hello'], $task->getOutput()); - self::assertEquals(1, $task->getProgress()); - } - - public function testTaskTypeExplicitlyEnabled(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - - $taskProcessingTypeSettings = [ - TextToText::ID => true, - ]; - $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true); - - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage()); - self::assertEquals(['output' => 'Hello'], $task->getOutput()); - self::assertEquals(1, $task->getProgress()); - } - - public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ - new ServiceRegistration('test', AudioToImage::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', AsyncProvider::class) - ]); - - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('testuser'); - $mount = $this->createMock(ICachedMountInfo::class); - $mount->expects($this->any())->method('getUser')->willReturn($user); - $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]); - - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - - self::assertTrue($this->manager->hasProviders()); - $audioId = $this->getFile('audioInput', 'Hello')->getId(); - $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser'); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->manager->getTask($task->getId()); - self::assertEquals($task->getId(), $task2->getId()); - self::assertEquals(['audio' => $audioId], $task2->getInput()); - self::assertNull($task2->getOutput()); - self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $this->manager->setTaskProgress($task2->getId(), 0.1); - $input = $this->manager->prepareInputData($task2); - self::assertTrue(isset($input['audio'])); - self::assertInstanceOf(File::class, $input['audio']); - self::assertEquals($audioId, $input['audio']->getId()); - - $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); - self::assertEquals(1, $task->getProgress()); - self::assertTrue(isset($task->getOutput()['spectrogram'])); - $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); - self::assertNotNull($node); - self::assertInstanceOf(File::class, $node); - self::assertEquals('World', $node->getContent()); - } - - public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileShapedData(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ - new ServiceRegistration('test', AudioToImage::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', AsyncProvider::class) - ]); - - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('testuser'); - $mount = $this->createMock(ICachedMountInfo::class); - $mount->expects($this->any())->method('getUser')->willReturn($user); - $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]); - - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - - self::assertTrue($this->manager->hasProviders()); - $audioId = $this->getFile('audioInput', 'Hello')->getId(); - $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser'); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->manager->getTask($task->getId()); - self::assertEquals($task->getId(), $task2->getId()); - self::assertEquals(['audio' => $audioId], $task2->getInput()); - self::assertNull($task2->getOutput()); - self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $this->manager->setTaskProgress($task2->getId(), 0.1); - $input = $this->manager->prepareInputData($task2); - self::assertTrue(isset($input['audio'])); - self::assertInstanceOf(File::class, $input['audio']); - self::assertEquals($audioId, $input['audio']->getId()); - - // Provider returns the raw file contents wrapped in a FileShaped object, including a file extension - $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => new FileShaped(EShapeType::Image, 'World', 'png')]); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); - self::assertEquals(1, $task->getProgress()); - self::assertTrue(isset($task->getOutput()['spectrogram'])); - $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); - self::assertNotNull($node); - self::assertInstanceOf(File::class, $node); - self::assertEquals('World', $node->getContent()); - // The extension carried by the FileShaped object is applied to the stored file name - self::assertStringEndsWith('.png', $node->getName()); - } - - public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void { - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ - new ServiceRegistration('test', AudioToImage::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', AsyncProvider::class) - ]); - $user = $this->createMock(IUser::class); - $user->expects($this->any())->method('getUID')->willReturn('testuser'); - $mount = $this->createMock(ICachedMountInfo::class); - $mount->expects($this->any())->method('getUser')->willReturn($user); - $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - - self::assertTrue($this->manager->hasProviders()); - $audioId = $this->getFile('audioInput', 'Hello')->getId(); - $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser'); - self::assertNull($task->getId()); - self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); - $this->manager->scheduleTask($task); - self::assertNotNull($task->getId()); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->manager->getTask($task->getId()); - self::assertEquals($task->getId(), $task2->getId()); - self::assertEquals(['audio' => $audioId], $task2->getInput()); - self::assertNull($task2->getOutput()); - self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $this->manager->setTaskProgress($task2->getId(), 0.1); - $input = $this->manager->prepareInputData($task2); - self::assertTrue(isset($input['audio'])); - self::assertInstanceOf(File::class, $input['audio']); - self::assertEquals($audioId, $input['audio']->getId()); - - $outputFileId = $this->getFile('audioOutput', 'World')->getId(); - - $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); - self::assertEquals(1, $task->getProgress()); - self::assertTrue(isset($task->getOutput()['spectrogram'])); - $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']); - self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']); - self::assertInstanceOf(File::class, $node); - self::assertEquals('World', $node->getContent()); - } - - public function testNonexistentTask(): void { - $this->expectException(NotFoundException::class); - $this->manager->getTask(2147483646); - } - - /** - * Insert a task without going through a provider, to control its timestamps and status. - */ - private function insertTask(int $status, ?int $scheduledAt, ?int $startedAt): DbTask { - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $task->setStatus($status); - $task->setScheduledAt($scheduledAt); - $task->setStartedAt($startedAt); - /** @var DbTask $entity */ - $entity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); - return $entity; - } - - public function testCountTasks(): void { - // Far in the future, so that tasks of other tests are outside of the window - $now = time() + 365 * 24 * 3600; - $window = $now - 7200; - $totalBefore = $this->manager->countTasks(); - $entities = []; - - try { - // Scheduled within the window, picked up after 1 minute - $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 3600, $now - 3540); - // Scheduled within the window, picked up after 10 minutes - $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 3600, $now - 3000); - // Scheduled within the window, picked up after 10 minutes, but failed - $entities[] = $this->insertTask(Task::STATUS_FAILED, $now - 3600, $now - 3000); - // Scheduled within the window, never picked up - $entities[] = $this->insertTask(Task::STATUS_CANCELLED, $now - 3600, null); - // Scheduled before the window, picked up after 10 minutes - $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 90000, $now - 89400); - - self::assertEquals($totalBefore + 5, $this->manager->countTasks()); - self::assertEquals(4, $this->manager->countTasks(scheduleAfter: $window)); - self::assertEquals(1, $this->manager->countTasks(status: Task::STATUS_FAILED, scheduleAfter: $window)); - // Tasks that were never picked up are not counted as slow - self::assertEquals(2, $this->manager->countTasks(scheduleAfter: $window, minPickupDelay: 60 * 4)); - self::assertEquals(1, $this->manager->countTasks(status: Task::STATUS_SUCCESSFUL, scheduleAfter: $window, minPickupDelay: 60 * 4)); - self::assertEquals(0, $this->manager->countTasks(scheduleAfter: $window, minPickupDelay: 60 * 20)); - self::assertEquals(4, $this->manager->countTasks(taskTypeIds: [TextToText::ID], scheduleAfter: $window)); - self::assertEquals(0, $this->manager->countTasks(taskTypeIds: [TextToImage::ID], scheduleAfter: $window)); - } finally { - foreach ($entities as $entity) { - $this->taskMapper->delete($entity); - } - } - } - - public function testOldTasksShouldBeCleanedUp(): void { - $currentTime = new \DateTime('now'); - $timeFactory = $this->createMock(ITimeFactory::class); - $timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime); - $timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp()); - - $this->taskMapper = new TaskMapper( - Server::get(IDBConnection::class), - $timeFactory, - ); - - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - self::assertCount(1, $this->manager->getAvailableTaskTypes()); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - - $currentTime = $currentTime->add(new \DateInterval('P1Y')); - // run background job - $bgJob = new RemoveOldTasksBackgroundJob( - $timeFactory, - $this->manager, - $this->taskMapper, - Server::get(LoggerInterface::class), - Server::get(IAppDataFactory::class), - ); - $bgJob->setArgument([]); - $bgJob->start($this->jobList); - - $this->expectException(NotFoundException::class); - $this->manager->getTask($task->getId()); - } - - public function testShouldTransparentlyHandleTextProcessingProviders(): void { - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - ]); - $taskTypes = $this->manager->getAvailableTaskTypes(); - self::assertCount(1, $taskTypes); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue(isset($taskTypes[TextToTextSummary::ID])); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); - self::assertIsArray($task->getOutput()); - self::assertTrue(isset($task->getOutput()['output'])); - self::assertEquals('Hello Summarize', $task->getOutput()['output']); - self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran); - } - - public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void { - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ - new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - ]); - $taskTypes = $this->manager->getAvailableTaskTypes(); - self::assertCount(1, $taskTypes); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue(isset($taskTypes[TextToTextSummary::ID])); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); - self::assertTrue($task->getOutput() === null); - self::assertEquals('ERROR', $task->getErrorMessage()); - self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran); - } - - public function testShouldTransparentlyHandleText2ImageProviders(): void { - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulTextToImageProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - ]); - $taskTypes = $this->manager->getAvailableTaskTypes(); - self::assertCount(1, $taskTypes); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue(isset($taskTypes[TextToImage::ID])); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null); - $this->manager->scheduleTask($task); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus()); - self::assertIsArray($task->getOutput()); - self::assertTrue(isset($task->getOutput()['images'])); - self::assertIsArray($task->getOutput()['images']); - self::assertCount(3, $task->getOutput()['images']); - self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran); - $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); - self::assertNotNull($node); - self::assertInstanceOf(File::class, $node); - self::assertEquals('test', $node->getContent()); - } - - public function testShouldTransparentlyHandleFailingText2ImageProviders(): void { - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([ - new ServiceRegistration('test', FailingTextToImageProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - ]); - $taskTypes = $this->manager->getAvailableTaskTypes(); - self::assertCount(1, $taskTypes); - self::assertCount(1, $this->manager->getAvailableTaskTypeIds()); - self::assertTrue(isset($taskTypes[TextToImage::ID])); - self::assertTrue($this->manager->hasProviders()); - $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null); - $this->manager->scheduleTask($task); - - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - $backgroundJob = new SynchronousBackgroundJob( - Server::get(ITimeFactory::class), - $this->manager, - $this->jobList, - Server::get(LoggerInterface::class), - ); - $backgroundJob->start($this->jobList); - - $task = $this->manager->getTask($task->getId()); - self::assertEquals(Task::STATUS_FAILED, $task->getStatus()); - self::assertTrue($task->getOutput() === null); - self::assertEquals('ERROR', $task->getErrorMessage()); - self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran); - } - - public function testMergeProvidersLocalAndEvent() { - // Arrange: Local provider registered, DIFFERENT external provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalProvider = new ExternalProvider(); // ID = 'event:external:provider' - $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); - $this->manager = $this->createManagerInstance(); - - // Act - $providers = $this->manager->getProviders(); - - // Assert: Both providers should be present - self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers); - self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]); - self::assertArrayHasKey(ExternalProvider::ID, $providers); - self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]); - self::assertCount(2, $providers); - } - - public function testGetProvidersIncludesExternalViaEvent() { - // Arrange: No local providers, one external provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalProvider = new ExternalProvider(); - $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); - $this->manager = $this->createManagerInstance(); // Create manager with configured mocks - - // Act - $providers = $this->manager->getProviders(); // Returns ID-indexed array - - // Assert - self::assertArrayHasKey(ExternalProvider::ID, $providers); - self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]); - self::assertCount(1, $providers); - self::assertTrue($this->manager->hasProviders()); - } - - public function testGetAvailableTaskTypesIncludesExternalViaEvent() { - // Arrange: No local types/providers, one external type and provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalProvider = new ExternalProvider(); // Provides ExternalTaskType - $externalTaskType = new ExternalTaskType(); - $this->configureEventDispatcherMock( - providersToAdd: [$externalProvider], - taskTypesToAdd: [$externalTaskType] - ); - $this->manager = $this->createManagerInstance(); - - // Act - $availableTypes = $this->manager->getAvailableTaskTypes(); - - // Assert - self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes); - self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds()); - self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type'); - self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']); - // Check if shapes match the external type/provider - self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']); - self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']); - self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider - } - - public function testLocalProviderWinsConflictWithEvent() { - // Arrange: Local provider registered, conflicting external provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success' - $this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]); - $this->manager = $this->createManagerInstance(); - - // Act - $providers = $this->manager->getProviders(); - - // Assert: Only the local provider should be present for the conflicting ID - self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers); - self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]); - self::assertCount(1, $providers); // Ensure no extra provider was added - } - - public function testTriggerableProviderWithNoOtherRunningTasks() { - // Arrange: Local provider registered, conflicting external provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']); - $externalProvider->expects($this->once())->method('trigger'); - $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); - $this->manager = $this->createManagerInstance(); - - // Act - $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null); - $this->manager->scheduleTask($task); - } - - public function testTriggerableProviderWithOtherRunningTasks() { - // Arrange: Local provider registered, conflicting external provider via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']); - $externalProvider->expects($this->once())->method('trigger'); - $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]); - $this->manager = $this->createManagerInstance(); - - $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null); - $this->manager->scheduleTask($task); - $this->manager->lockTask($task); - - // Act - $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null); - $this->manager->scheduleTask($task); - } - - public function testMergeTaskTypesLocalAndEvent() { - // Arrange: Local type registered, DIFFERENT external type via event - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', AsyncProvider::class) - ]); - $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([ - new ServiceRegistration('test', AudioToImage::class) - ]); - $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]); - $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]); - - $externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype' - $externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype' - $this->configureEventDispatcherMock( - providersToAdd: [$externalProvider], - taskTypesToAdd: [$externalTaskType] - ); - $this->manager = $this->createManagerInstance(); - - // Act - $availableTypes = $this->manager->getAvailableTaskTypes(); - $availableTypeIds = $this->manager->getAvailableTaskTypeIds(); - - // Assert: Both task types should be available - self::assertContains(AudioToImage::ID, $availableTypeIds); - self::assertArrayHasKey(AudioToImage::ID, $availableTypes); - self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']); - - self::assertContains(ExternalTaskType::ID, $availableTypeIds); - self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes); - self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']); - - self::assertCount(2, $availableTypes); - } - - private function createManagerInstance(): Manager { - // Clear potentially cached config values if needed - $this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences'); - - // Re-create Text2ImageManager if its state matters or mocks change - $text2imageManager = new \OC\TextToImage\Manager( - $this->serverContainer, - $this->coordinator, - Server::get(LoggerInterface::class), - $this->jobList, - Server::get(\OC\TextToImage\Db\TaskMapper::class), - $this->config, // Use the shared config mock - Server::get(IAppDataFactory::class), - ); - - return new Manager( - $this->appConfig, - $this->coordinator, - $this->serverContainer, - Server::get(LoggerInterface::class), - $this->taskMapper, - $this->jobList, - $this->eventDispatcher, // Use the potentially reconfigured mock - Server::get(IAppDataFactory::class), - $this->rootFolder, - $text2imageManager, - $this->userMountCache, - Server::get(IClientService::class), - Server::get(IAppManager::class), - Server::get(IUserManager::class), - Server::get(IUserSession::class), - Server::get(ICacheFactory::class), - Server::get(IFactory::class), - Server::get(ITimeFactory::class), - $this->remoteHostValidator, - ); - } - - private function configureEventDispatcherMock( - array $providersToAdd = [], - array $taskTypesToAdd = [], - ?int $expectedCalls = null, - ): void { - $dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls); - - $this->eventDispatcher->expects($dispatchExpectation) - ->method('dispatchTyped') - ->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void { - if ($event instanceof GetTaskProcessingProvidersEvent) { - foreach ($providersToAdd as $providerInstance) { - $event->addProvider($providerInstance); - } - foreach ($taskTypesToAdd as $taskTypeInstance) { - $event->addTaskType($taskTypeInstance); - } - } - }); - } - - /** - * Register a single synchronous provider for TextToText so tasks can be scheduled. - * - * The integration suite shares one database across tests and does not truncate - * between them, so we clear the tasks table first to make the oldest-scheduled - * ordering of the claim deterministic for these focused tests. - */ - private function registerTextToTextProvider(): void { - $db = Server::get(IDBConnection::class); - $db->getQueryBuilder()->delete('taskprocessing_tasks')->executeStatement(); - - $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true); - $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([ - new ServiceRegistration('test', SuccessfulSyncProvider::class) - ]); - self::assertTrue($this->manager->hasProviders()); - } - - public function testClaimReturnsNullWhenNoScheduledTask(): void { - $this->registerTextToTextProvider(); - - // No task scheduled => nothing to claim. - self::assertNull($this->manager->claimNextScheduledTask([TextToText::ID])); - self::assertNull($this->manager->claimNextScheduledTask()); - } - - public function testClaimReturnsTaskAndSetsItRunning(): void { - $this->registerTextToTextProvider(); - - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); - $scheduledId = $task->getId(); - - $claimed = $this->manager->claimNextScheduledTask([TextToText::ID]); - self::assertNotNull($claimed); - self::assertEquals($scheduledId, $claimed->getId()); - // The returned task object reports RUNNING ... - self::assertEquals(Task::STATUS_RUNNING, $claimed->getStatus()); - // ... and the change is persisted in the database. - $persisted = $this->manager->getTask($scheduledId); - self::assertEquals(Task::STATUS_RUNNING, $persisted->getStatus()); - } - - public function testClaimNeverReturnsTheSameTaskTwice(): void { - // No-duplicate invariant. We cannot run two truly concurrent DB transactions - // inside one PHPUnit process, but the structural guarantee is the same: once a - // task is claimed it is RUNNING (no longer SCHEDULED), so a second claim can - // never return it again. Under real concurrency, FOR UPDATE SKIP LOCKED enforces - // the same property by skipping rows another transaction has locked; that path - // is additionally validated live on nc-ai. - $this->registerTextToTextProvider(); - - $taskA = new Task(TextToText::ID, ['input' => 'A'], 'test', null); - $this->manager->scheduleTask($taskA); - $taskB = new Task(TextToText::ID, ['input' => 'B'], 'test', null); - $this->manager->scheduleTask($taskB); - - $firstClaim = $this->manager->claimNextScheduledTask([TextToText::ID]); - $secondClaim = $this->manager->claimNextScheduledTask([TextToText::ID]); - - self::assertNotNull($firstClaim); - self::assertNotNull($secondClaim); - // Two distinct tasks were handed out, never the same one twice. - self::assertNotEquals($firstClaim->getId(), $secondClaim->getId()); - self::assertEqualsCanonicalizing( - [$taskA->getId(), $taskB->getId()], - [$firstClaim->getId(), $secondClaim->getId()], - ); - - // Both are now RUNNING and the queue is drained. - self::assertEquals(Task::STATUS_RUNNING, $this->manager->getTask($taskA->getId())->getStatus()); - self::assertEquals(Task::STATUS_RUNNING, $this->manager->getTask($taskB->getId())->getStatus()); - self::assertNull($this->manager->claimNextScheduledTask([TextToText::ID])); - } - - public function testClaimNeverMarksTaskFailed(): void { - $this->registerTextToTextProvider(); - - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - $id = $task->getId(); - - $claimed = $this->manager->claimNextScheduledTask([TextToText::ID]); - self::assertNotNull($claimed); - - // Claiming only ever transitions SCHEDULED -> RUNNING, never to FAILED/CANCELLED. - self::assertNotEquals(Task::STATUS_FAILED, $claimed->getStatus()); - self::assertNotEquals(Task::STATUS_CANCELLED, $claimed->getStatus()); - $persisted = $this->manager->getTask($id); - self::assertNotEquals(Task::STATUS_FAILED, $persisted->getStatus()); - self::assertNotEquals(Task::STATUS_CANCELLED, $persisted->getStatus()); - self::assertEquals(Task::STATUS_RUNNING, $persisted->getStatus()); - self::assertNull($persisted->getErrorMessage()); - } - - public function testClaimRespectsTaskTypeFilter(): void { - $this->registerTextToTextProvider(); - - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - - // A type that does not match the only scheduled task must not be claimed. - self::assertNull($this->manager->claimNextScheduledTask(['some:other:tasktype'])); - // The task is still SCHEDULED and claimable without a filter. - self::assertEquals(Task::STATUS_SCHEDULED, $this->manager->getTask($task->getId())->getStatus()); - $claimed = $this->manager->claimNextScheduledTask(); - self::assertNotNull($claimed); - self::assertEquals($task->getId(), $claimed->getId()); - } - - public function testClaimRecordsStartedAt(): void { - $this->registerTextToTextProvider(); - - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - // A scheduled task has not started yet. - self::assertNull($this->manager->getTask($task->getId())->getStartedAt()); - - $before = time(); - $claimed = $this->manager->claimNextScheduledTask([TextToText::ID]); - $after = time(); - - self::assertNotNull($claimed); - // started_at is recorded at claim time on the returned task ... - self::assertNotNull($claimed->getStartedAt()); - self::assertGreaterThanOrEqual($before, $claimed->getStartedAt()); - self::assertLessThanOrEqual($after, $claimed->getStartedAt()); - // ... and persisted in the database (since the worker receives the task already - // RUNNING, the later setTaskStatus SCHEDULED -> RUNNING edge is skipped and would - // otherwise never write started_at). - $persisted = $this->manager->getTask($task->getId()); - self::assertNotNull($persisted->getStartedAt()); - self::assertGreaterThanOrEqual($before, $persisted->getStartedAt()); - self::assertLessThanOrEqual($after, $persisted->getStartedAt()); - } - - public function testLockTaskDoesNotResurrectFinishedTask(): void { - // Regression guard for the lockTask claim path (used by the SQLite fallback and the - // external-provider API claim). lockTask must only ever transition SCHEDULED -> RUNNING. - // If another worker finished a task (SUCCESSFUL/FAILED) between the SELECT and this - // UPDATE, lockTask must NOT flip it back to RUNNING -- otherwise a completed task is - // resurrected and processed twice. (The previous `status != RUNNING` guard let a - // SUCCESSFUL/FAILED row be re-locked.) - $this->registerTextToTextProvider(); - - $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); - $this->manager->scheduleTask($task); - $id = $task->getId(); - - // Simulate another worker having already finished the task. - $entity = $this->taskMapper->find($id); - $entity->setStatus(Task::STATUS_SUCCESSFUL); - $this->taskMapper->update($entity); - - // Attempting to claim the (now SUCCESSFUL) task must be a no-op. - $affected = $this->taskMapper->lockTask($this->taskMapper->find($id)); - - self::assertSame(0, $affected, 'lockTask must not claim a task that is no longer SCHEDULED'); - self::assertEquals(Task::STATUS_SUCCESSFUL, $this->manager->getTask($id)->getStatus()); - } -} --- a/tests/lib/Teams/TeamFolderTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertSame(42, $folder->getId()); - $this->assertSame('Engineering', $folder->getMountPoint()); - $this->assertNull($folder->getQuota()); - $this->assertSame(['id' => 42, 'quota' => null, 'mountPoint' => 'Engineering'], $folder->jsonSerialize()); - } - - public function testSerializesFolderQuota(): void { - $folder = new TeamFolder(42, 'Engineering', 1024); - - $this->assertSame(1024, $folder->getQuota()); - $this->assertSame(['id' => 42, 'quota' => 1024, 'mountPoint' => 'Engineering'], $folder->jsonSerialize()); - } -} --- a/tests/lib/Teams/TeamManagerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -createTeamManager(); - - $this->assertNull($teamManager->getTeamFolderProvider()); - } - - public function testGetTeamFolderProviderReturnsNullWithoutFolderProvider(): void { - $teamManager = $this->createTeamManager(true); - $this->setProviders($teamManager, [ - 'other' => $this->createMock(ITeamResourceProvider::class), - ]); - - $this->assertNull($teamManager->getTeamFolderProvider()); - } - - public function testGetTeamFolderProviderReturnsRegisteredFolderProvider(): void { - $teamManager = $this->createTeamManager(true); - $folderProvider = $this->createMock(ITeamFolderProvider::class); - $this->setProviders($teamManager, [ - 'other' => $this->createMock(ITeamResourceProvider::class), - 'folder' => $folderProvider, - ]); - - $this->assertSame($folderProvider, $teamManager->getTeamFolderProvider()); - } - - private function createTeamManager(bool $hasTeamSupport = false): TeamManager { - return new class($this->createMock(Coordinator::class), $this->createMock(IURLGenerator::class), null, $hasTeamSupport, ) extends TeamManager { - public function __construct( - Coordinator $bootContext, - IURLGenerator $urlGenerator, - null $circlesManager, - private bool $hasTeamSupport, - ) { - parent::__construct($bootContext, $urlGenerator, $circlesManager); - } - - #[\Override] - public function hasTeamSupport(): bool { - return $this->hasTeamSupport; - } - }; - } - - /** - * @param array $providers - */ - private function setProviders(TeamManager $teamManager, array $providers): void { - (new \ReflectionProperty(TeamManager::class, 'providers'))->setValue($teamManager, $providers); - } -} --- a/tests/lib/TempManagerTest.php +++ /dev/null @@ -1,189 +0,0 @@ -baseDir = $this->getManager()->getTempBaseDir() . $this->getUniqueID('/oc_tmp_test'); - if (!is_dir($this->baseDir)) { - mkdir($this->baseDir); - } - } - - #[\Override] - protected function tearDown(): void { - if ($this->baseDir !== null) { - Files::rmdirr($this->baseDir); - } - $this->baseDir = null; - parent::tearDown(); - } - - protected function getManager(?LoggerInterface $logger = null, ?IConfig $config = null): TempManager { - if (!$logger) { - $logger = $this->createMock(LoggerInterface::class); - } - if (!$config) { - $config = $this->createMock(IConfig::class); - $config->method('getSystemValue') - ->with('tempdirectory', null) - ->willReturn('/dev/shm'); - } - $iniGetWrapper = $this->createMock(IniGetWrapper::class); - $manager = new TempManager($logger, $config, $iniGetWrapper); - if ($this->baseDir) { - $manager->overrideTempBaseDir($this->baseDir); - } - return $manager; - } - - public function testGetFile(): void { - $manager = $this->getManager(); - $file = $manager->getTemporaryFile('txt'); - $this->assertStringEndsWith('.txt', $file); - $this->assertTrue(is_file($file)); - $this->assertTrue(is_writable($file)); - - file_put_contents($file, 'bar'); - $this->assertEquals('bar', file_get_contents($file)); - } - - public function testGetFolder(): void { - $manager = $this->getManager(); - $folder = $manager->getTemporaryFolder(); - $this->assertStringEndsWith('/', $folder); - $this->assertTrue(is_dir($folder)); - $this->assertTrue(is_writable($folder)); - - file_put_contents($folder . 'foo.txt', 'bar'); - $this->assertEquals('bar', file_get_contents($folder . 'foo.txt')); - } - - public function testCleanFiles(): void { - $manager = $this->getManager(); - $file1 = $manager->getTemporaryFile('txt'); - $file2 = $manager->getTemporaryFile('txt'); - $this->assertTrue(file_exists($file1)); - $this->assertTrue(file_exists($file2)); - - $manager->clean(); - - $this->assertFalse(file_exists($file1)); - $this->assertFalse(file_exists($file2)); - } - - public function testCleanFolder(): void { - $manager = $this->getManager(); - $folder1 = $manager->getTemporaryFolder(); - $folder2 = $manager->getTemporaryFolder(); - touch($folder1 . 'foo.txt'); - touch($folder1 . 'bar.txt'); - $this->assertTrue(file_exists($folder1)); - $this->assertTrue(file_exists($folder2)); - $this->assertTrue(file_exists($folder1 . 'foo.txt')); - $this->assertTrue(file_exists($folder1 . 'bar.txt')); - - $manager->clean(); - - $this->assertFalse(file_exists($folder1)); - $this->assertFalse(file_exists($folder2)); - $this->assertFalse(file_exists($folder1 . 'foo.txt')); - $this->assertFalse(file_exists($folder1 . 'bar.txt')); - } - - public function testCleanOld(): void { - $manager = $this->getManager(); - $oldFile = $manager->getTemporaryFile('txt'); - $newFile = $manager->getTemporaryFile('txt'); - $folder = $manager->getTemporaryFolder(); - $nonOcFile = $this->baseDir . '/foo.txt'; - file_put_contents($nonOcFile, 'bar'); - - $past = time() - 2 * 3600; - touch($oldFile, $past); - touch($folder, $past); - touch($nonOcFile, $past); - - $manager2 = $this->getManager(); - $manager2->cleanOld(); - $this->assertFalse(file_exists($oldFile)); - $this->assertFalse(file_exists($folder)); - $this->assertTrue(file_exists($nonOcFile)); - $this->assertTrue(file_exists($newFile)); - } - - public function testLogCantCreateFile(): void { - $logger = $this->createMock(LoggerInterface::class); - $manager = $this->getManager($logger); - chmod($this->baseDir, 0500); - $logger->expects($this->once()) - ->method('warning') - ->with($this->stringContains('Can not create a temporary file in directory')); - $this->assertFalse($manager->getTemporaryFile('txt')); - } - - public function testLogCantCreateFolder(): void { - $logger = $this->createMock(LoggerInterface::class); - $manager = $this->getManager($logger); - chmod($this->baseDir, 0500); - $logger->expects($this->once()) - ->method('warning') - ->with($this->stringContains('Can not create a temporary folder in directory')); - $this->assertFalse($manager->getTemporaryFolder()); - } - - public function testGenerateTemporaryPathWithPostfix(): void { - $logger = $this->createMock(LoggerInterface::class); - $tmpManager = self::invokePrivate( - $this->getManager($logger), - 'generateTemporaryPath', - ['postfix'] - ); - - $this->assertStringEndsWith('.postfix', $tmpManager); - } - - public function testGenerateTemporaryPathTraversal(): void { - $logger = $this->createMock(LoggerInterface::class); - $tmpManager = self::invokePrivate( - $this->getManager($logger), - 'generateTemporaryPath', - ['../Traversal\\../FileName'] - ); - - $this->assertStringEndsNotWith('./Traversal\\../FileName', $tmpManager); - $this->assertStringEndsWith('.Traversal..FileName', $tmpManager); - } - - public function testGetTempBaseDirFromConfig(): void { - $dir = $this->getManager()->getTemporaryFolder(); - - $config = $this->createMock(IConfig::class); - $config->expects($this->once()) - ->method('getSystemValue') - ->with('tempdirectory', null) - ->willReturn($dir); - - $this->baseDir = null; // prevent override - $tmpManager = $this->getManager(null, $config); - - $this->assertEquals($dir, $tmpManager->getTempBaseDir()); - } -} --- a/tests/lib/Template/CSSResourceLocatorTest.php +++ /dev/null @@ -1,123 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->appData = $this->createMock(AppData::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->config = $this->createMock(IConfig::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->themingDefaults = $this->createMock(ThemingDefaults::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->appConfig = $this->createMock(AppConfig::class); - } - - private function cssResourceLocator() { - /** @var Factory|\PHPUnit\Framework\MockObject\MockObject $factory */ - $factory = $this->createMock(Factory::class); - $factory->method('get')->with('css')->willReturn($this->appData); - return new CSSResourceLocator( - $this->logger, - $this->createMock(IConfig::class), - Server::get(IAppManager::class), - 'theme', - ['core' => 'map'], - ['3rd' => 'party'], - ); - } - - private function rrmdir($directory) { - $files = array_diff(scandir($directory), ['.','..']); - foreach ($files as $file) { - if (is_dir($directory . '/' . $file)) { - $this->rrmdir($directory . '/' . $file); - } else { - unlink($directory . '/' . $file); - } - } - return rmdir($directory); - } - - private function randomString(): string { - return sha1(random_bytes(10)); - } - - public function testFindWithAppPathSymlink(): void { - // First create new apps path, and a symlink to it - $apps_dirname = $this->randomString(); - $new_apps_path = sys_get_temp_dir() . '/' . $apps_dirname; - $new_apps_path_symlink = $new_apps_path . '_link'; - mkdir($new_apps_path); - symlink($apps_dirname, $new_apps_path_symlink); - - // Create an app within that path - mkdir($new_apps_path . '/' . 'test_css_app'); - - // Use the symlink as the app path - \OC::$APPSROOTS[] = [ - 'path' => $new_apps_path_symlink, - 'url' => '/css-apps-test', - 'writable' => false, - ]; - - $locator = $this->cssResourceLocator(); - $locator->find(['test_css_app/test-file']); - - $resources = $locator->getResources(); - $this->assertCount(1, $resources); - $resource = $resources[0]; - $this->assertCount(3, $resource); - $root = $resource[0]; - $webRoot = $resource[1]; - $file = $resource[2]; - - $expectedRoot = $new_apps_path . '/test_css_app'; - $expectedWebRoot = \OC::$WEBROOT . '/css-apps-test/test_css_app'; - $expectedFile = 'test-file.css'; - - $this->assertEquals($expectedRoot, $root, - 'Ensure the app path symlink is resolved into the real path'); - $this->assertEquals($expectedWebRoot, $webRoot); - $this->assertEquals($expectedFile, $file); - - array_pop(\OC::$APPSROOTS); - unlink($new_apps_path_symlink); - $this->rrmdir($new_apps_path); - } -} --- a/tests/lib/Template/JSCombinerTest.php +++ /dev/null @@ -1,535 +0,0 @@ -appData = $this->createMock(IAppData::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->config = $this->createMock(IConfig::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->depsCache = $this->createMock(ICache::class); - $this->cacheFactory->expects($this->atLeastOnce()) - ->method('createDistributed') - ->willReturn($this->depsCache); - $this->logger = $this->createMock(LoggerInterface::class); - $this->jsCombiner = new JSCombiner( - $this->appData, - $this->urlGenerator, - $this->cacheFactory, - $this->config, - $this->logger - ); - } - - public function testProcessDebugMode(): void { - $this->config - ->expects($this->once()) - ->method('getSystemValueBool') - ->with('debug') - ->willReturn(true); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertFalse($actual); - } - - public function testProcessNotInstalled(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['debug', false], - ['installed', false] - ]); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertFalse($actual); - } - - public function testProcessUncachedFileNoAppDataFolder(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['debug', false], - ['installed', true], - ]); - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willThrowException(new NotFoundException()); - $this->appData->expects($this->once())->method('newFolder')->with('awesomeapp')->willReturn($folder); - $file = $this->createMock(ISimpleFile::class); - $gzfile = $this->createMock(ISimpleFile::class); - - $fileDeps = $this->createMock(ISimpleFile::class); - - $folder->method('getFile') - ->willReturnCallback(function ($path) use ($file, $gzfile) { - if ($path === 'combine.js') { - return $file; - } elseif ($path === 'combine.js.deps') { - throw new NotFoundException(); - } elseif ($path === 'combine.js.gzip') { - return $gzfile; - } - $this->fail(); - }); - $folder->expects($this->once()) - ->method('newFile') - ->with('combine.js.deps') - ->willReturn($fileDeps); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertTrue($actual); - } - - public function testProcessUncachedFile(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['debug', false], - ['installed', true], - ]); - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willReturn($folder); - $file = $this->createMock(ISimpleFile::class); - $fileDeps = $this->createMock(ISimpleFile::class); - $gzfile = $this->createMock(ISimpleFile::class); - - $folder->method('getFile') - ->willReturnCallback(function ($path) use ($file, $gzfile) { - if ($path === 'combine.js') { - return $file; - } elseif ($path === 'combine.js.deps') { - throw new NotFoundException(); - } elseif ($path === 'combine.js.gzip') { - return $gzfile; - } - $this->fail(); - }); - $folder->expects($this->once()) - ->method('newFile') - ->with('combine.js.deps') - ->willReturn($fileDeps); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertTrue($actual); - } - - public function testProcessCachedFile(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['debug', false], - ['installed', true], - ]); - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willReturn($folder); - $file = $this->createMock(ISimpleFile::class); - - $fileDeps = $this->createMock(ISimpleFile::class); - - $fileDeps->expects($this->once())->method('getContent')->willReturn('{}'); - - $folder->method('fileExists') - ->with('combine.js') - ->willReturn(true); - - $folder->method('getFile') - ->willReturnCallback(function ($path) use ($file, $fileDeps) { - if ($path === 'combine.js') { - return $file; - } - - if ($path === 'combine.js.deps') { - return $fileDeps; - } - - $this->fail(); - }); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertTrue($actual); - } - - public function testProcessCachedFileMemcache(): void { - $this->config - ->expects($this->exactly(2)) - ->method('getSystemValueBool') - ->willReturnMap([ - ['debug', false], - ['installed', true], - ]); - $folder = $this->createMock(ISimpleFolder::class); - $this->appData->expects($this->once()) - ->method('getFolder') - ->with('awesomeapp') - ->willReturn($folder); - $folder->method('getName') - ->willReturn('awesomeapp'); - $folder->method('fileExists') - ->with('combine.js') - ->willReturn(true); - - $file = $this->createMock(ISimpleFile::class); - - $this->depsCache->method('get') - ->with('awesomeapp-combine.js.deps') - ->willReturn('{}'); - - $folder->method('getFile') - ->willReturnCallback(function ($path) use ($file) { - if ($path === 'combine.js') { - return $file; - } - $this->fail(); - }); - - $actual = $this->jsCombiner->process(__DIR__, '/data/combine.json', 'awesomeapp'); - $this->assertTrue($actual); - } - - public function testIsCachedNoDepsFile(): void { - $fileName = 'combine.json'; - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - - $folder->method('getFile') - ->willReturnCallback(function ($path) use ($file) { - if ($path === 'combine.js') { - return $file; - } - if ($path === 'combine.js.deps') { - throw new NotFoundException(); - } - $this->fail(); - }); - - $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]); - $this->assertFalse($actual); - } - - public function testIsCachedWithNotExistingFile(): void { - $fileName = 'combine.json'; - $folder = $this->createMock(ISimpleFolder::class); - $folder->method('fileExists') - ->with('combine.js') - ->willReturn(true); - $file = $this->createMock(ISimpleFile::class); - $folder->method('getFile') - ->with('combine.js.deps') - ->willReturn($file); - $file->expects($this->once()) - ->method('getContent') - ->willReturn(json_encode(['/etc/certainlynotexisting/file/ihope' => 10000])); - - $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]); - $this->assertFalse($actual); - } - - public function testIsCachedWithOlderMtime(): void { - $fileName = 'combine.json'; - $folder = $this->createMock(ISimpleFolder::class); - $folder->method('fileExists') - ->with('combine.js') - ->willReturn(true); - $file = $this->createMock(ISimpleFile::class); - $folder->method('getFile') - ->with('combine.js.deps') - ->willReturn($file); - $file->expects($this->once()) - ->method('getContent') - ->willReturn(json_encode([__FILE__ => 1234])); - - $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]); - $this->assertFalse($actual); - } - - public function testIsCachedWithoutContent(): void { - $fileName = 'combine.json'; - $folder = $this->createMock(ISimpleFolder::class); - $folder->method('fileExists') - ->with('combine.js') - ->willReturn(true); - $file = $this->createMock(ISimpleFile::class); - $folder->method('getFile') - ->with('combine.js.deps') - ->willReturn($file); - $file->expects($this->once()) - ->method('getContent') - ->willReturn(''); - $this->logger->expects($this->once()) - ->method('info') - ->with('JSCombiner: deps file empty: combine.js.deps'); - $actual = self::invokePrivate($this->jsCombiner, 'isCached', [$fileName, $folder]); - $this->assertFalse($actual); - } - - public function testCacheNoFile(): void { - $fileName = 'combine.js'; - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $depsFile = $this->createMock(ISimpleFile::class); - $gzFile = $this->createMock(ISimpleFile::class); - - $path = __DIR__ . '/data/'; - - $folder->method('getFile')->willThrowException(new NotFoundException()); - - $folder->method('newFile')->willReturnCallback( - function ($filename) use ($file, $depsFile, $gzFile) { - if ($filename === 'combine.js') { - return $file; - } elseif ($filename === 'combine.js.deps') { - return $depsFile; - } elseif ($filename === 'combine.js.gzip') { - return $gzFile; - } - $this->fail(); - } - ); - - $file->expects($this->once())->method('putContent'); - $depsFile->expects($this->once())->method('putContent'); - $gzFile->expects($this->once())->method('putContent'); - - $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); - $this->assertTrue($actual); - } - - public function testCache(): void { - $fileName = 'combine.js'; - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $depsFile = $this->createMock(ISimpleFile::class); - $gzFile = $this->createMock(ISimpleFile::class); - - $path = __DIR__ . '/data/'; - - $folder->method('getFile')->willReturnCallback( - function ($filename) use ($file, $depsFile, $gzFile) { - if ($filename === 'combine.js') { - return $file; - } elseif ($filename === 'combine.js.deps') { - return $depsFile; - } elseif ($filename === 'combine.js.gzip') { - return $gzFile; - } - $this->fail(); - } - ); - - $file->expects($this->once())->method('putContent'); - $depsFile->expects($this->once())->method('putContent'); - $gzFile->expects($this->once())->method('putContent'); - - $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); - $this->assertTrue($actual); - } - - public function testCacheNotPermittedException(): void { - $fileName = 'combine.js'; - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $depsFile = $this->createMock(ISimpleFile::class); - $gzFile = $this->createMock(ISimpleFile::class); - - $path = __DIR__ . '/data/'; - - $folder->expects($this->exactly(3)) - ->method('getFile') - ->willReturnMap([ - [$fileName, $file], - [$fileName . '.deps', $depsFile], - [$fileName . '.gzip', $gzFile] - ]); - - $file->expects($this->once()) - ->method('putContent') - ->with('var a = \'hello\'; - - -var b = \'world\'; - - -'); - $depsFile - ->expects($this->once()) - ->method('putContent') - ->with($this->callback( - function ($content) { - $deps = json_decode($content, true); - return array_key_exists(__DIR__ . '/data//1.js', $deps) - && array_key_exists(__DIR__ . '/data//2.js', $deps); - })) - ->willThrowException(new NotPermittedException()); - - $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); - $this->assertFalse($actual); - } - - public function testCacheSuccess(): void { - $fileName = 'combine.js'; - - $folder = $this->createMock(ISimpleFolder::class); - $file = $this->createMock(ISimpleFile::class); - $depsFile = $this->createMock(ISimpleFile::class); - $gzFile = $this->createMock(ISimpleFile::class); - - $path = __DIR__ . '/data/'; - - $folder->method('getFile')->willReturnCallback( - function ($filename) use ($file, $depsFile, $gzFile) { - if ($filename === 'combine.js') { - return $file; - } elseif ($filename === 'combine.js.deps') { - return $depsFile; - } elseif ($filename === 'combine.js.gzip') { - return $gzFile; - } - $this->fail(); - } - ); - - $file->expects($this->once()) - ->method('putContent') - ->with('var a = \'hello\'; - - -var b = \'world\'; - - -'); - $depsFile->expects($this->once())->method('putContent')->with($this->callback( - function ($content) { - $deps = json_decode($content, true); - return array_key_exists(__DIR__ . '/data//1.js', $deps) - && array_key_exists(__DIR__ . '/data//2.js', $deps); - })); - $gzFile->expects($this->once())->method('putContent')->with($this->callback( - function ($content) { - return gzdecode($content) === 'var a = \'hello\'; - - -var b = \'world\'; - - -'; - } - )); - - $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); - $this->assertTrue($actual); - } - - public static function dataGetCachedSCSS(): array { - return [ - ['awesomeapp', 'core/js/foo.json', '/js/core/foo.js'], - ['files', 'apps/files/js/foo.json', '/js/files/foo.js'] - ]; - } - - /** - * @param $appName - * @param $fileName - * @param $result - */ - #[\PHPUnit\Framework\Attributes\DataProvider('dataGetCachedSCSS')] - public function testGetCachedSCSS($appName, $fileName, $result): void { - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.Js.getJs', [ - 'fileName' => 'foo.js', - 'appName' => $appName - ]) - ->willReturn(\OC::$WEBROOT . $result); - - $actual = $this->jsCombiner->getCachedJS($appName, $fileName); - $this->assertEquals(substr($result, 1), $actual); - } - - public function testGetContent(): void { - // Create temporary file with some content - $tmpFile = Server::get(ITempManager::class)->getTemporaryFile('JSCombinerTest'); - $pathInfo = pathinfo($tmpFile); - file_put_contents($tmpFile, json_encode(['/foo/bar/test', $pathInfo['dirname'] . '/js/mytest.js'])); - $tmpFilePathArray = explode('/', $pathInfo['basename']); - array_pop($tmpFilePathArray); - - $expected = [ - '//foo/bar/test', - '/' . implode('/', $tmpFilePathArray) . $pathInfo['dirname'] . '/js/mytest.js', - ]; - $this->assertEquals($expected, $this->jsCombiner->getContent($pathInfo['dirname'], $pathInfo['basename'])); - } - - public function testGetContentInvalidJson(): void { - // Create temporary file with some content - $tmpFile = Server::get(ITempManager::class)->getTemporaryFile('JSCombinerTest'); - $pathInfo = pathinfo($tmpFile); - file_put_contents($tmpFile, 'CertainlyNotJson'); - $expected = []; - $this->assertEquals($expected, $this->jsCombiner->getContent($pathInfo['dirname'], $pathInfo['basename'])); - } - - public function testResetCache(): void { - $file = $this->createMock(ISimpleFile::class); - $file->expects($this->once()) - ->method('delete'); - - $folder = $this->createMock(ISimpleFolder::class); - $folder->expects($this->once()) - ->method('getDirectoryListing') - ->willReturn([$file]); - - $cache = $this->createMock(ICache::class); - $this->cacheFactory->expects($this->once()) - ->method('createDistributed') - ->willReturn($cache); - $cache->expects($this->never()) - ->method('clear'); - $this->appData->expects($this->once()) - ->method('getDirectoryListing') - ->willReturn([$folder]); - - $this->jsCombiner->resetCache(); - } -} --- a/tests/lib/Template/JSResourceLocatorTest.php +++ /dev/null @@ -1,182 +0,0 @@ -appData = $this->createMock(IAppData::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->config = $this->createMock(IConfig::class); - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->appManager = $this->createMock(IAppManager::class); - } - - private function jsResourceLocator(): JSResourceLocator { - $jsCombiner = new JSCombiner( - $this->appData, - $this->urlGenerator, - $this->cacheFactory, - $this->config, - $this->logger - ); - return new JSResourceLocator( - $this->logger, - $this->config, - $jsCombiner, - $this->appManager, - ); - } - - private function rrmdir($directory) { - $files = array_diff(scandir($directory), ['.','..']); - foreach ($files as $file) { - if (is_dir($directory . '/' . $file)) { - $this->rrmdir($directory . '/' . $file); - } else { - unlink($directory . '/' . $file); - } - } - return rmdir($directory); - } - - private function randomString(): string { - return sha1(random_bytes(10)); - } - - public function testFindWithAppPathSymlink(): void { - $appName = 'test-js-app'; - - // First create new apps path, and a symlink to it - $apps_dirname = $this->randomString(); - $new_apps_path = sys_get_temp_dir() . '/' . $apps_dirname; - $new_apps_path_symlink = $new_apps_path . '_link'; - $this->assertTrue(( - mkdir($new_apps_path) && symlink($apps_dirname, $new_apps_path_symlink) - ), 'Setup of apps path failed'); - - // Create an app within that path - $this->assertTrue(( - mkdir($new_apps_path . '/' . $appName) && touch($new_apps_path . '/' . $appName . '/' . 'test-file.js') - ), 'Setup of app within the new apps path failed'); - - // Use the symlink as the app path - $this->appManager->expects($this->once()) - ->method('getAppPath') - ->with($appName) - ->willReturn("$new_apps_path_symlink/$appName"); - $this->appManager->expects($this->once()) - ->method('getAppWebPath') - ->with($appName) - ->willReturn("/js-apps-test/$appName"); - - // Run the tests - $locator = $this->jsResourceLocator(); - $locator->find(["$appName/test-file"]); - - $resources = $locator->getResources(); - $this->assertCount(1, $resources); - $resource = $resources[0]; - $this->assertCount(3, $resource); - $root = $resource[0]; - $webRoot = $resource[1]; - $file = $resource[2]; - - $expectedRoot = $new_apps_path; - $expectedWebRoot = \OC::$WEBROOT . '/js-apps-test'; - $expectedFile = $appName . '/test-file.js'; - - $this->assertEquals($expectedRoot, $root, - 'Ensure the app path symlink is resolved into the real path'); - $this->assertEquals($expectedWebRoot, $webRoot); - $this->assertEquals($expectedFile, $file); - - unlink($new_apps_path_symlink); - $this->rrmdir($new_apps_path); - } - - public function testNotExistingTranslationHandledSilent(): void { - $this->appManager->expects($this->once()) - ->method('getAppPath') - ->with('core') - ->willThrowException(new AppPathNotFoundException()); - $this->appManager->expects($this->atMost(1)) - ->method('getAppWebPath') - ->with('core') - ->willThrowException(new AppPathNotFoundException()); - // Assert logger is not called - $this->logger->expects($this->never()) - ->method('error'); - - // Run the tests - $locator = $this->jsResourceLocator(); - $locator->find(['core/l10n/en.js']); - - $resources = $locator->getResources(); - $this->assertCount(0, $resources); - } - - public function testFindModuleJSWithFallback(): void { - // First create new apps path, and a symlink to it - $apps_dirname = $this->randomString(); - $new_apps_path = sys_get_temp_dir() . '/' . $apps_dirname; - mkdir($new_apps_path); - - // Create an app within that path - mkdir("$new_apps_path/test-js-app"); - touch("$new_apps_path/test-js-app/module.mjs"); - touch("$new_apps_path/test-js-app/both.mjs"); - touch("$new_apps_path/test-js-app/both.js"); - touch("$new_apps_path/test-js-app/plain.js"); - - // Use the app path - $this->appManager->expects($this->any()) - ->method('getAppPath') - ->with('test-js-app') - ->willReturn("$new_apps_path/test-js-app"); - - $locator = $this->jsResourceLocator(); - $locator->find(['test-js-app/module', 'test-js-app/both', 'test-js-app/plain']); - - $resources = $locator->getResources(); - $this->assertCount(3, $resources); - - $expectedWebRoot = \OC::$WEBROOT . '/js-apps-test/test-js-app'; - $expectedFiles = ['module.mjs', 'both.mjs', 'plain.js']; - - for ($idx = 0; $idx++; $idx < 3) { - $this->assertEquals($expectedWebRoot, $resources[$idx][1]); - $this->assertEquals($expectedFiles[$idx], $resources[$idx][2]); - } - - $this->rrmdir($new_apps_path); - } -} --- a/tests/lib/Template/ResourceLocatorTest.php +++ /dev/null @@ -1,84 +0,0 @@ -logger = $this->createMock(LoggerInterface::class); - $this->config = $this->createMock(IConfig::class); - } - - public function getResourceLocator(string $theme): ResourceLocator&MockObject { - $this->config - ->expects($this->any()) - ->method('getSystemValueString') - ->with('theme', '') - ->willReturn($theme); - return $this->getMockBuilder(ResourceLocator::class) - ->onlyMethods(['doFind', 'doFindTheme']) - ->setConstructorArgs( - [$this->logger, $this->config], - '', true, true, true, [] - ) - ->getMock(); - } - - public function testFind(): void { - $locator = $this->getResourceLocator('theme'); - $locator->expects($this->once()) - ->method('doFind') - ->with('foo'); - $locator->expects($this->once()) - ->method('doFindTheme') - ->with('foo'); - $locator->find(['foo']); - } - - public function testFindNotFound(): void { - $locator = $this->getResourceLocator('theme', - ['core' => 'map'], ['3rd' => 'party'], ['foo' => 'bar']); - $locator->expects($this->once()) - ->method('doFind') - ->with('foo') - ->willThrowException(new ResourceNotFoundException('foo', 'map')); - $locator->expects($this->once()) - ->method('doFindTheme') - ->with('foo') - ->willThrowException(new ResourceNotFoundException('foo', 'map')); - $this->logger->expects($this->exactly(2)) - ->method('debug') - ->with($this->stringContains('map/foo')); - $locator->find(['foo']); - } - - public function testAppendIfExist(): void { - $locator = $this->getResourceLocator('theme'); - $method = new \ReflectionMethod($locator, 'appendIfExist'); - - $method->invoke($locator, __DIR__, basename(__FILE__), 'webroot'); - $resource1 = [__DIR__, 'webroot', basename(__FILE__)]; - $this->assertEquals([$resource1], $locator->getResources()); - - $method->invoke($locator, __DIR__, 'does-not-exist'); - $this->assertEquals([$resource1], $locator->getResources()); - } -} --- a/tests/lib/Template/data/1.js +++ /dev/null @@ -1 +0,0 @@ -var a = 'hello'; --- a/tests/lib/Template/data/1.js.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/lib/Template/data/2.js +++ /dev/null @@ -1 +0,0 @@ -var b = 'world'; --- a/tests/lib/Template/data/2.js.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/lib/Template/data/combine.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "1.js", - "2.js" -] --- a/tests/lib/Template/data/combine.json.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors -SPDX-License-Identifier: AGPL-3.0-or-later --- a/tests/lib/TemplateFunctionsTest.php +++ /dev/null @@ -1,299 +0,0 @@ -expectOutputString('<img onload="alert(1)" />'); - p(''); - } - - public function testPJavaScriptWithScriptTags(): void { - $this->expectOutputString('<script>alert('Hacked!');</script>'); - p(""); - } - - public function testPNormalString(): void { - $string = 'This is a good string without HTML.'; - $this->expectOutputString($string); - p($string); - } - - public function testPrintUnescaped(): void { - $htmlString = ""; - $this->expectOutputString($htmlString); - print_unescaped($htmlString); - } - - public function testPrintUnescapedNormalString(): void { - $string = 'This is a good string!'; - $this->expectOutputString($string); - print_unescaped($string); - } - - public function testEmitScriptTagWithContent(): void { - $this->expectOutputRegex('/ for it!', - ]; - $goodArray = [ - 'While it is unusual to pass an array', - 'this function actually <blink>supports</blink> it.', - 'And therefore there needs to be a <script>alert("Unit"+'test')</script> for it!', - ]; - $result = Util::sanitizeHTML($badArray); - $this->assertEquals($goodArray, $result); - - $badString = ''; - $result = Util::sanitizeHTML($badString); - $this->assertEquals('<img onload="alert(1)" />', $result); - - $badString = ""; - $result = Util::sanitizeHTML($badString); - $this->assertEquals('<script>alert('Hacked!');</script>', $result); - - $goodString = 'This is a good string without HTML.'; - $result = Util::sanitizeHTML($goodString); - $this->assertEquals('This is a good string without HTML.', $result); - } - - public function testEncodePath(): void { - $component = '/§#@test%&^ä/-child'; - $result = Util::encodePath($component); - $this->assertEquals('/%C2%A7%23%40test%25%26%5E%C3%A4/-child', $result); - } - - public function testIsNonUTF8Locale(): void { - // OC_Util::isNonUTF8Locale() assumes escapeshellcmd('§') returns '' with non-UTF-8 locale. - $locale = setlocale(LC_CTYPE, 0); - setlocale(LC_CTYPE, 'C'); - $this->assertEquals('', escapeshellcmd('§')); - $this->assertEquals('\'\'', escapeshellarg('§')); - setlocale(LC_CTYPE, 'C.UTF-8'); - $this->assertEquals('§', escapeshellcmd('§')); - $this->assertEquals('\'§\'', escapeshellarg('§')); - setlocale(LC_CTYPE, $locale); - } - - public function testFileInfoLoaded(): void { - $expected = function_exists('finfo_open'); - $this->assertEquals($expected, \OC_Util::fileInfoLoaded()); - } - - /** - * Host is "localhost" this is a valid for emails, - * but not for default strict email verification that requires a top level domain. - * So we check that with strict email verification we fallback to the default - */ - public function testGetDefaultEmailAddressStrict(): void { - $email = Util::getDefaultEmailAddress('no-reply'); - $this->assertEquals('no-reply@localhost.localdomain', $email); - } - - /** - * If no strict email check is enabled "localhost" should validate as a valid email domain - */ - public function testGetDefaultEmailAddress(): void { - $config = Server::get(IConfig::class); - $config->setAppValue('core', 'enforce_strict_email_check', 'no'); - $email = Util::getDefaultEmailAddress('no-reply'); - $this->assertEquals('no-reply@localhost', $email); - $config->deleteAppValue('core', 'enforce_strict_email_check'); - } - - public function testGetDefaultEmailAddressFromConfig(): void { - $config = Server::get(IConfig::class); - $config->setSystemValue('mail_domain', 'example.com'); - $email = Util::getDefaultEmailAddress('no-reply'); - $this->assertEquals('no-reply@example.com', $email); - $config->deleteSystemValue('mail_domain'); - } - - public function testGetConfiguredEmailAddressFromConfig(): void { - $config = Server::get(IConfig::class); - $config->setSystemValue('mail_domain', 'example.com'); - $config->setSystemValue('mail_from_address', 'owncloud'); - $email = Util::getDefaultEmailAddress('no-reply'); - $this->assertEquals('owncloud@example.com', $email); - $config->deleteSystemValue('mail_domain'); - $config->deleteSystemValue('mail_from_address'); - } - - public function testGetInstanceIdGeneratesValidId(): void { - Server::get(IConfig::class)->deleteSystemValue('instanceid'); - $instanceId = OC_Util::getInstanceId(); - $this->assertStringStartsWith('oc', $instanceId); - $matchesRegex = preg_match('/^[a-z0-9]+$/', $instanceId); - $this->assertSame(1, $matchesRegex); - } - - /** - * Test needUpgrade() when the core version is increased - */ - public function testNeedUpgradeCore(): void { - $config = Server::get(IConfig::class); - $oldConfigVersion = $config->getSystemValue('version', '0.0.0'); - $oldSessionVersion = Server::get(ISession::class)->get('OC_Version'); - - $this->assertFalse(Util::needUpgrade()); - - $config->setSystemValue('version', '7.0.0.0'); - Server::get(ISession::class)->set('OC_Version', [7, 0, 0, 1]); - self::invokePrivate(new Util, 'needUpgradeCache', [null]); - - $this->assertTrue(Util::needUpgrade()); - - $config->setSystemValue('version', $oldConfigVersion); - Server::get(ISession::class)->set('OC_Version', $oldSessionVersion); - self::invokePrivate(new Util, 'needUpgradeCache', [null]); - - $this->assertFalse(Util::needUpgrade()); - } - - public function testCheckDataDirectoryValidity(): void { - $dataDir = Server::get(ITempManager::class)->getTemporaryFolder(); - touch($dataDir . '/.ncdata'); - $errors = \OC_Util::checkDataDirectoryValidity($dataDir); - $this->assertEmpty($errors); - Files::rmdirr($dataDir); - - $dataDir = Server::get(ITempManager::class)->getTemporaryFolder(); - // no touch - $errors = \OC_Util::checkDataDirectoryValidity($dataDir); - $this->assertNotEmpty($errors); - Files::rmdirr($dataDir); - - $errors = \OC_Util::checkDataDirectoryValidity('relative/path'); - $this->assertNotEmpty($errors); - } - - #[\Override] - protected function setUp(): void { - parent::setUp(); - - \OC_Util::$styles = []; - self::invokePrivate(Util::class, 'scripts', [[]]); - self::invokePrivate(Util::class, 'scriptDeps', [[]]); - } - #[\Override] - protected function tearDown(): void { - parent::tearDown(); - - \OC_Util::$styles = []; - self::invokePrivate(Util::class, 'scripts', [[]]); - self::invokePrivate(Util::class, 'scriptDeps', [[]]); - } - - public function testAddScript(): void { - Util::addScript('first', 'myFirstJSFile'); - Util::addScript('core', 'myFancyJSFile1'); - Util::addScript('files', 'myFancyJSFile2', 'core'); - Util::addScript('myApp5', 'myApp5JSFile', 'myApp2'); - Util::addScript('myApp', 'myFancyJSFile3'); - Util::addScript('core', 'myFancyJSFile4'); - // after itself - Util::addScript('core', 'myFancyJSFile5', 'core'); - // add duplicate - Util::addScript('core', 'myFancyJSFile1'); - // dependency chain - Util::addScript('myApp4', 'myApp4JSFile', 'myApp3'); - Util::addScript('myApp3', 'myApp3JSFile', 'myApp2'); - Util::addScript('myApp2', 'myApp2JSFile', 'myApp'); - Util::addScript('core', 'common'); - Util::addScript('core', 'main'); - - $scripts = Util::getScripts(); - - // Core should appear first - $this->assertEquals( - 0, - array_search('core/js/common', $scripts, true) - ); - $this->assertEquals( - 1, - array_search('core/js/main', $scripts, true) - ); - $this->assertEquals( - 2, - array_search('core/js/myFancyJSFile1', $scripts, true) - ); - $this->assertEquals( - 3, - array_search('core/js/myFancyJSFile4', $scripts, true) - ); - - // Dependencies should appear before their children - $this->assertLessThan( - array_search('files/js/myFancyJSFile2', $scripts, true), - array_search('core/js/myFancyJSFile3', $scripts, true) - ); - $this->assertLessThan( - array_search('myApp2/js/myApp2JSFile', $scripts, true), - array_search('myApp/js/myFancyJSFile3', $scripts, true) - ); - $this->assertLessThan( - array_search('myApp3/js/myApp3JSFile', $scripts, true), - array_search('myApp2/js/myApp2JSFile', $scripts, true) - ); - $this->assertLessThan( - array_search('myApp4/js/myApp4JSFile', $scripts, true), - array_search('myApp3/js/myApp3JSFile', $scripts, true) - ); - $this->assertLessThan( - array_search('myApp5/js/myApp5JSFile', $scripts, true), - array_search('myApp2/js/myApp2JSFile', $scripts, true) - ); - - // No duplicates - $this->assertEquals( - $scripts, - array_unique($scripts) - ); - - // All scripts still there - $scripts = [ - 'core/js/common', - 'core/js/main', - 'core/js/myFancyJSFile1', - 'core/js/myFancyJSFile4', - 'core/js/myFancyJSFile5', - 'first/l10n/en', - 'first/js/myFirstJSFile', - 'files/l10n/en', - 'files/js/myFancyJSFile2', - 'myApp/l10n/en', - 'myApp/js/myFancyJSFile3', - 'myApp2/l10n/en', - 'myApp2/js/myApp2JSFile', - 'myApp5/l10n/en', - 'myApp5/js/myApp5JSFile', - 'myApp3/l10n/en', - 'myApp3/js/myApp3JSFile', - 'myApp4/l10n/en', - 'myApp4/js/myApp4JSFile', - ]; - foreach ($scripts as $script) { - $this->assertContains($script, $scripts); - } - } - - public function testAddScriptCircularDependency(): void { - Util::addScript('circular', 'file1', 'dependency'); - Util::addScript('dependency', 'file2', 'circular'); - - $scripts = Util::getScripts(); - $this->assertContains('circular/js/file1', $scripts); - $this->assertContains('dependency/js/file2', $scripts); - } - - public function testAddTranslations(): void { - Util::addTranslations('appId', 'de'); - - $this->assertEquals([ - 'appId/l10n/de' - ], Util::getScripts()); - $this->assertEquals([], \OC_Util::$styles); - } - - public function testAddStyle(): void { - Util::addStyle('core', 'myFancyCSSFile1'); - Util::addStyle('myApp', 'myFancyCSSFile2'); - Util::addStyle('core', 'myFancyCSSFile0', true); - Util::addStyle('core', 'myFancyCSSFile10', true); - // add duplicate - Util::addStyle('core', 'myFancyCSSFile1'); - - $this->assertEquals([], Util::getScripts()); - $this->assertEquals([ - 'core/css/myFancyCSSFile10', - 'core/css/myFancyCSSFile0', - 'core/css/myFancyCSSFile1', - 'myApp/css/myFancyCSSFile2', - ], \OC_Util::$styles); - } - - public function testAddVendorStyle(): void { - \OC_Util::addVendorStyle('core', 'myFancyCSSFile1'); - \OC_Util::addVendorStyle('myApp', 'myFancyCSSFile2'); - \OC_Util::addVendorStyle('core', 'myFancyCSSFile0', true); - \OC_Util::addVendorStyle('core', 'myFancyCSSFile10', true); - // add duplicate - \OC_Util::addVendorStyle('core', 'myFancyCSSFile1'); - - $this->assertEquals([], Util::getScripts()); - $this->assertEquals([ - 'core/vendor/myFancyCSSFile10', - 'core/vendor/myFancyCSSFile0', - 'core/vendor/myFancyCSSFile1', - 'myApp/vendor/myFancyCSSFile2', - ], \OC_Util::$styles); - } - - public function testShortenMultibyteString(): void { - $this->assertEquals('Short nuff', Util::shortenMultibyteString('Short nuff', 255)); - $this->assertEquals('ABC', Util::shortenMultibyteString('ABCDEF', 3)); - // each of the characters is 12 bytes - $this->assertEquals('🙈', Util::shortenMultibyteString('🙈🙊🙉', 16, 2)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('humanFileSizeProvider')] - public function testHumanFileSize($expected, $input): void { - $result = Util::humanFileSize($input); - $this->assertEquals($expected, $result); - } - - public static function humanFileSizeProvider(): array { - return [ - ['0 B', 0], - ['1 KB', 1024], - ['9.5 MB', 10000000], - ['1.3 GB', 1395864371], - ['465.7 GB', 500000000000], - ['454.7 TB', 500000000000000], - ['444.1 PB', 500000000000000000], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providesComputerFileSize')] - public function testComputerFileSize($expected, $input): void { - $result = Util::computerFileSize($input); - $this->assertEquals($expected, $result); - } - - public static function providesComputerFileSize(): array { - return [ - [0.0, '0 B'], - [1024.0, '1 KB'], - [1395864371.0, '1.3 GB'], - [9961472.0, '9.5 MB'], - [500041567437.0, '465.7 GB'], - [false, '12 GB etfrhzui'] - ]; - } - - public function testMb_array_change_key_case(): void { - $arrayStart = [ - 'Foo' => 'bar', - 'Bar' => 'foo', - ]; - $arrayResult = [ - 'foo' => 'bar', - 'bar' => 'foo', - ]; - $result = Util::mb_array_change_key_case($arrayStart); - $expected = $arrayResult; - $this->assertEquals($result, $expected); - - $arrayStart = [ - 'foo' => 'bar', - 'bar' => 'foo', - ]; - $arrayResult = [ - 'FOO' => 'bar', - 'BAR' => 'foo', - ]; - $result = Util::mb_array_change_key_case($arrayStart, MB_CASE_UPPER); - $expected = $arrayResult; - $this->assertEquals($result, $expected); - } - - public static function sanitizeProvider(): array { - return [ - // Basic spaces and line controls - ['Hello World', 'Hello World'], - [' Hello World ', 'Hello World'], - ["Hello\t World \nAgain", 'Hello World Again'], - ["Hello\rWorld", 'HelloWorld'], - ["Hello\r\nWorld", 'HelloWorld'], - ["Hello\u{200B}World", 'HelloWorld'], // zero-width space removed - ["Hello\t\n\r World", 'Hello World'], - - // Unicode, emoji, and CJK - ['テスト 😃 💬', 'テスト 😃 💬'], - ['中文測試 ✅', '中文測試 ✅'], - ['Русский текст 😁', 'Русский текст 😁'], - ['Café crème ☕', 'Café crème ☕'], - - // Punctuation and filename-like - ['Hello-World_123.', 'Hello-World_123.'], - ['File.name, with commas', 'File.name, with commas'], - ['Smile — dash', 'Smile — dash'], - ['Invalid:/\\?%*|<>name', 'Invalid:/\\?%*|<>name'], // kept as is - ['test@example.com', 'test@example.com'], - - // Control and invisible chars - ["Bad\0Name", 'BadName'], - ["Hello\u{0007}World", 'HelloWorld'], - ["Line\r\nbreaks", 'Linebreaks'], - ["\x1F Hidden control", 'Hidden control'], - - // Whitespace and normalization - [" Multiple spaces\t and \nnewlines ", 'Multiple spaces and newlines'], - ["No-break\u{00A0}space", 'No-break space'], // NBSP normalized - ["Zero\u{2003}width\u{2009}spaces", 'Zero width spaces'], // various spaces - - // Complex mixes - ['テスト 💬.png', 'テスト 💬.png'], - [' Mix 😎 emojis 🎉 and 123 numbers ', 'Mix 😎 emojis 🎉 and 123 numbers'], - ["Hello \u{200B}\n World", 'Hello World'], - ['Path ../etc/passwd', 'Path ../etc/passwd'], - ['Symbols! @ # % ^ & * ( )', 'Symbols! @ # % ^ & * ( )'], - ['Special chars