diff --git a/build/.remarkrc b/.config/.remarkrc similarity index 100% rename from build/.remarkrc rename to .config/.remarkrc diff --git a/build/.yamllint b/.config/.yamllint similarity index 100% rename from build/.yamllint rename to .config/.yamllint diff --git a/.config/hadolint.yml b/.config/hadolint.yml new file mode 100644 index 00000000..541ddd31 --- /dev/null +++ b/.config/hadolint.yml @@ -0,0 +1,30 @@ +--- +# For all available rules see: https://github.com/hadolint/hadolint#rules +ignored: + - DL3008 # We do not want to pin versions in apt get install. + - DL3018 # We do not want to pin versions in apk add + +# For full details see https://github.com/hadolint/hadolint#configure +# +# The following keys are available: +# +# failure-threshold: string # name of threshold level (error | warning | info | style | ignore | none) +# format: string # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy) +# label-schema: # See https://github.com/hadolint/hadolint#linting-labels for details +# author: string # Your name +# contact: string # email address +# created: timestamp # rfc3339 datetime +# version: string # semver +# documentation: string # url +# git-revision: string # hash +# license: string # spdx +# no-color: boolean # true | false +# no-fail: boolean # true | false +# override: +# error: [string] # list of rules +# warning: [string] # list of rules +# info: [string] # list of rules +# style: [string] # list of rules +# strict-labels: boolean # true | false +# disable-ignore-pragma: boolean # true | false +# trustedRegistries: string | [string] # registry or list of registries diff --git a/build/phpcs.xml.dist b/.config/phpcs.xml.dist similarity index 98% rename from build/phpcs.xml.dist rename to .config/phpcs.xml.dist index fc1286ec..a6147742 100644 --- a/build/phpcs.xml.dist +++ b/.config/phpcs.xml.dist @@ -15,7 +15,7 @@ . - */vendor/*|*/build/* + */vendor/*|*/.config/* diff --git a/.github/workflows/dependancy-security-check.yml b/.github/workflows/dependancy-security-check.yml deleted file mode 100644 index 285f62b6..00000000 --- a/.github/workflows/dependancy-security-check.yml +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: Security check - -on: - - push - - pull_request - # Allow manually triggering the workflow. - - workflow_dispatch - -# Cancels all previous workflow runs for the same branch that have not yet completed. -concurrency: - # The concurrency group contains the workflow name and the branch name. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - security-check: - runs-on: ubuntu-latest - name: "Security check" - - strategy: - matrix: - php: ['8.2'] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - - # Install dependencies and handle caching in one go. - # @link https://github.com/marketplace/actions/install-composer-dependencies - - name: Install Composer dependencies - uses: "ramsey/composer-install@v2" - with: - working-directory: "solid" - - - name: Download security checker - # yamllint disable-line rule:line-length - run: wget -P . https://github.com/fabpot/local-php-security-checker/releases/download/v2.0.4/local-php-security-checker_2.0.4_linux_amd64 - - - name: Make security checker executable - run: chmod +x ./local-php-security-checker_2.0.4_linux_amd64 - - - name: Check against insecure dependencies - run: ./local-php-security-checker_2.0.4_linux_amd64 --path=solid/composer.lock diff --git a/.github/workflows/dockerfile.yml b/.github/workflows/dockerfile.yml new file mode 100644 index 00000000..0fdbc00a --- /dev/null +++ b/.github/workflows/dockerfile.yml @@ -0,0 +1,56 @@ +--- +name: Dockerfile Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '.config/hadolint.yml' + - '.dockerignore' + - '.github/workflows/dockerfile.yml' + - 'Dockerfile' + # Docker project specific, Dockerfile "COPY" and "ADD" entries. + - 'solid/' + - 'init-live.sh' + - 'init.sh' + - 'site.conf' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '.config/hadolint.yml' + - '.dockerignore' + - '.github/workflows/dockerfile.yml' + - 'Dockerfile' + # Docker project specific, Dockerfile "COPY" and "ADD" entries. + - 'solid/' + - 'init-live.sh' + - 'init.sh' + - 'site.conf' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 03.quality.docker.lint.yml + lint-dockerfile: + name: Dockerfile Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/hadolint + with: + args: >- + hadolint + --config .config/hadolint.yml + Dockerfile diff --git a/.github/workflows/json.yml b/.github/workflows/json.yml new file mode 100644 index 00000000..7e83269e --- /dev/null +++ b/.github/workflows/json.yml @@ -0,0 +1,46 @@ +--- +name: JSON Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.json' + - '.github/workflows/json.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.json' + - '.github/workflows/json.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.json.lint-syntax.yml + lint-json-syntax: + name: JSON Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/jsonlint + with: + args: >- + find . + -not -path '*/.git/*' + -not -path '*/node_modules/*' + -not -path '*/vendor/*' + -name '*.json' + -type f + -exec jsonlint --quiet {} ; diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml deleted file mode 100644 index cbca0f41..00000000 --- a/.github/workflows/linting.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Linting jobs - -on: - - push - - pull_request - -jobs: - lint-json: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: "docker://pipelinecomponents/jsonlint:latest" - with: - args: "find . -not -path './.git/*' -name '*.json' -type f" - - lint-php: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: pipeline-components/php-linter@master - - lint-markdown: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: pipeline-components/remark-lint@master - with: - options: --rc-path=build/.remarkrc --ignore-pattern='*/vendor/*' - - lint-yaml: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: pipeline-components/yamllint@master - with: - options: --config-file=build/.yamllint diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 00000000..581b9c7e --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,42 @@ +--- +name: Markdown Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.md' + - '.github/workflows/markdown.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.md' + - '.github/workflows/markdown.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.quality.markdown.lint-syntax.yml + lint-markdown-syntax: + name: Markdown Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/remark-lint + with: + args: >- + remark + --rc-path=.config/.remarkrc + --ignore-pattern='*/vendor/*' diff --git a/.github/workflows/php-version-sniff.yml b/.github/workflows/php-version-sniff.yml deleted file mode 100644 index 30cfd373..00000000 --- a/.github/workflows/php-version-sniff.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: PHP Version Compatibility - -on: - - push - - pull_request - # Allow manually triggering the workflow. - - workflow_dispatch - -# Cancels all previous workflow runs for the same branch that have not yet completed. -concurrency: - # The concurrency group contains the workflow name and the branch name. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - php-codesniffer: - strategy: - matrix: - php: [ '8.1' ] - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - uses: pipeline-components/php-codesniffer@master - with: - options: >- - -s - --ignore='*vendor/*' - --standard=PHPCompatibility - --extensions=php - --runtime-set testVersion ${{ matrix.php }} diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 00000000..12e0c29d --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,151 @@ +--- +name: PHP Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - '.config/phpunit.xml.dist' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + branches: [ main ] + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - '.config/phpunit.xml.dist' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.php.lint-syntax.yml + lint-php-syntax: + name: PHP Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-linter + with: + args: >- + parallel-lint + --exclude .git + --exclude vendor + --no-progress + . + # 01.quality.php.validate.dependencies-file.yml + validate-dependencies-file: + name: Validate dependencies file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + composer validate + --check-lock + --no-plugins + --no-scripts + --strict + working-directory: "solid" + # 02.test.php.test-unit.yml + php-unittest: + name: PHP Unit Tests + needs: + - lint-php-syntax + - validate-dependencies-file + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + ini-values: error_reporting=E_ALL, display_errors=On + php-version: ${{ matrix.php }} + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + with: + working-directory: "solid" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: bin/phpunit --configuration .config/phpunit.xml.dist + # 03.quality.php.scan.dependencies-vulnerabilities.yml + scan-dependencies-vulnerabilities: + name: Scan Dependencies Vulnerabilities + needs: + - validate-dependencies-file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + composer audit + --abandoned=report + --locked + --no-dev + --no-plugins + --no-scripts + working-directory: "solid" + # 03.quality.php.lint-quality.yml + php-lint-quality: + needs: + - lint-php-syntax + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-codesniffer + with: + args: >- + phpcs + -s + --extensions=php + --ignore='*vendor/*' + --standard=.config/phpcs.xml.dist + . + # 03.quality.php.lint-version-compatibility.yml + php-check-version-compatibility: + name: PHP Version Compatibility + needs: + - lint-php-syntax + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.0' # from 2020-11 to 2022-11 (2023-11) + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-codesniffer + with: + args: >- + phpcs + -s + --extensions=php + --ignore='*vendor/*' + --runtime-set testVersion ${{ matrix.php }} + --standard=PHPCompatibility + . diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml deleted file mode 100644 index 885c567a..00000000 --- a/.github/workflows/quality-checks.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Quality Assurance jobs - -on: - - push - - pull_request - -jobs: - composer-validate: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: "docker://composer" - with: - args: composer validate --strict --working-dir=solid/ - - php-codesniffer: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: pipeline-components/php-codesniffer@master - with: - options: --standard=build/phpcs.xml.dist diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml new file mode 100644 index 00000000..d3026535 --- /dev/null +++ b/.github/workflows/shell.yml @@ -0,0 +1,55 @@ +--- +name: Shell Script Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.bash' + - '**.sh' + - '.github/workflows/shell.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.bash' + - '**.sh' + - '.github/workflows/shell.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.shell.lint-syntax.yml + lint-shell-syntax: + name: Shell Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + find . + -name '*.sh' + -not -path '*/.git/*' + -type f + -print0 + | xargs -0 -P"$(nproc)" -I{} bash -n "{}" + # 03.quality.shell.lint.yml + lint-shell-quality: + name: Shell Quality Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/shellcheck + with: + # yamllint disable-line rule:line-length + args: /bin/sh -c "find . -not -path '*/.git/*' -name '*.sh' -type f -print0 | xargs -0 -r -n1 shellcheck" diff --git a/.github/workflows/shell.yml.bak b/.github/workflows/shell.yml.bak new file mode 100644 index 00000000..44da2a0e --- /dev/null +++ b/.github/workflows/shell.yml.bak @@ -0,0 +1,55 @@ +--- +name: Shell Script Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.bash' + - '**.sh' + - '.github/workflows/shell.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.bash' + - '**.sh' + - '.github/workflows/shell.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.shell.lint-syntax.yml + lint-shell-syntax: + name: Shell Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + find . + -name '*.sh' + -not -path '*/.git/*' + -type f + -print0 + | xargs -0 -P"$(nproc)" -I{} bash -n "{}" + # 03.quality.shell.lint.yml + lint-shell-quality: + name: Shell Quality Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/shellcheck:latest + with: + # yamllint disable-line rule:line-length + args: /bin/sh -c "find . -not -path '*/.git/*' -name '*.sh' -type f -print0 | xargs -0 -r -n1 shellcheck" diff --git a/.github/workflows/ci.yml b/.github/workflows/solid-tests-suites.yml similarity index 93% rename from .github/workflows/ci.yml rename to .github/workflows/solid-tests-suites.yml index 4aa46902..77c9303e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/solid-tests-suites.yml @@ -34,9 +34,7 @@ jobs: # Version 24 comes with PHP 8.0, which is no longer supported; # Latest is not tested here, as that could cause failures unrelated to project changes nextcloud_version: - - 28 - - 29 - - 30 + - 31 steps: - name: Create docker tag from git reference @@ -46,15 +44,15 @@ jobs: | tr --complement --squeeze-repeats '[:alnum:]._-' '_')" \ >> "${GITHUB_ENV}" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache-solid-nextcloud-docker with: path: cache/solid-nextcloud key: solid-nextcloud-docker-${{ matrix.nextcloud_version }}-${{ github.sha }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: docker/login-action@v2 + - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -82,16 +80,14 @@ jobs: fail-fast: false matrix: nextcloud_version: - - 28 - - 29 - - 30 + - 31 test: - 'solidtestsuite/solid-crud-tests:v7.0.5' - 'solidtestsuite/web-access-control-tests:v7.1.0' - 'solidtestsuite/webid-provider-tests:v2.1.1' # Prevent EOL or non-stable versions of Nextcloud to fail the test-suite - continue-on-error: ${{ contains(fromJson('[28,29,30]'), matrix.nextcloud_version) == false }} + continue-on-error: ${{ contains(fromJson('[31]'), matrix.nextcloud_version) == false }} steps: - name: Create docker tag from git reference @@ -101,15 +97,15 @@ jobs: | tr --complement --squeeze-repeats '[:alnum:]._-' '_')" \ >> "${GITHUB_ENV}" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache-solid-nextcloud-docker with: path: cache/solid-nextcloud key: solid-nextcloud-docker-${{ matrix.nextcloud_version }}-${{ github.sha }} - - uses: docker/login-action@v2 + - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/xml.yml b/.github/workflows/xml.yml new file mode 100644 index 00000000..0c30f8f5 --- /dev/null +++ b/.github/workflows/xml.yml @@ -0,0 +1,42 @@ +--- +name: XML Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.xml' + - '**.xml.dist' + - '.github/workflows/xml.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.xml' + - '**.xml.dist' + - '.github/workflows/xml.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.xml.lint-syntax.yml + lint-xml: + name: XML Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/xmllint + with: + # yamllint disable-line rule:line-length + args: /bin/sh -c "find . -iname '*.xml' -type f -exec xmllint --noout {} \+" diff --git a/.github/workflows/xml.yml.bak b/.github/workflows/xml.yml.bak new file mode 100644 index 00000000..0c30f8f5 --- /dev/null +++ b/.github/workflows/xml.yml.bak @@ -0,0 +1,42 @@ +--- +name: XML Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.xml' + - '**.xml.dist' + - '.github/workflows/xml.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.xml' + - '**.xml.dist' + - '.github/workflows/xml.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.xml.lint-syntax.yml + lint-xml: + name: XML Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/xmllint + with: + # yamllint disable-line rule:line-length + args: /bin/sh -c "find . -iname '*.xml' -type f -exec xmllint --noout {} \+" diff --git a/.github/workflows/yaml.yml b/.github/workflows/yaml.yml new file mode 100644 index 00000000..ad8fb9d3 --- /dev/null +++ b/.github/workflows/yaml.yml @@ -0,0 +1,42 @@ +--- +name: YAML Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.yml' + - '**.yaml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.yml' + - '**.yaml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.yaml.lint.yml + lint-yaml: + name: YAML Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/yamllint + with: + args: >- + yamllint + --config-file=.config/.yamllint + . diff --git a/Dockerfile b/Dockerfile index 80ff378e..9324cbad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG NEXTCLOUD_VERSION FROM nextcloud:${NEXTCLOUD_VERSION} -RUN apt-get update && apt-get install -yq \ +RUN apt-get update && apt-get install --no-install-recommends -yq \ git \ sudo \ vim \ diff --git a/env-vars-server.list b/env-vars-server.list index 28239ba7..1f38d6fe 100644 --- a/env-vars-server.list +++ b/env-vars-server.list @@ -1,6 +1,6 @@ SERVER_ROOT=https://server -STORAGE_ROOT=https://server/apps/solid/@alice/storage/ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +STORAGE_ROOT=https://server/apps/solid/~alice/storage/ +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible USERNAME=alice PASSWORD=alice123 diff --git a/env-vars-testers.list b/env-vars-testers.list index 366167e0..a8a79563 100644 --- a/env-vars-testers.list +++ b/env-vars-testers.list @@ -1,11 +1,11 @@ -WEBID_ALICE=https://server/apps/solid/@alice/profile/card#me +WEBID_ALICE=https://server/apps/solid/~alice/profile/card#me OIDC_ISSUER_ALICE=https://server -STORAGE_ROOT_ALICE=https://server/apps/solid/@alice/storage/ -WEBID_BOB=https://thirdparty/apps/solid/@alice/profile/card#me +STORAGE_ROOT_ALICE=https://server/apps/solid/~alice/storage/ +WEBID_BOB=https://thirdparty/apps/solid/~alice/profile/card#me OIDC_ISSUER_BOB=https://thirdparty STORAGE_ROOT_BOB=https://thirdparty/ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me SERVER_ROOT_ESCAPED=https:\/\/server SERVER_ROOT=https://server -STORAGE_ROOT=https://server/apps/solid/@alice/storage/ +STORAGE_ROOT=https://server/apps/solid/~alice/storage/ SKIP_CONC=1 diff --git a/env-vars-thirdparty.list b/env-vars-thirdparty.list index 9a2c8416..1c889484 100644 --- a/env-vars-thirdparty.list +++ b/env-vars-thirdparty.list @@ -1,5 +1,5 @@ SERVER_ROOT=https://thirdparty -ALICE_WEBID=https://thirdparty/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://thirdparty/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible USERNAME=alice PASSWORD=alice123 diff --git a/env.list b/env.list index 1256e61d..cef5c00e 100644 --- a/env.list +++ b/env.list @@ -1,2 +1,2 @@ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible diff --git a/run-solid-test-suite.sh b/run-solid-test-suite.sh index 4416b4cb..9f35e320 100755 --- a/run-solid-test-suite.sh +++ b/run-solid-test-suite.sh @@ -2,7 +2,7 @@ set -e -# Note that .github/workflows/ci.yml does not use this, this function is just for manual runs of this script. +# Note that .github/workflows/solid-tests-suites.yml does not use this, this function is just for manual runs of this script. # You can pick different values for the NEXTCLOUD_VERSION build arg, as required: function setup { docker build -t pubsub-server https://github.com/pdsinterop/php-solid-pubsub-server.git#main diff --git a/site.conf b/site.conf index d7789bd9..2f1272a1 100644 --- a/site.conf +++ b/site.conf @@ -1,4 +1,8 @@ + # To use User SubDomains, make sure to add a "catch-all", for instance: + # ServerName nextcloud.local + # ServerAlias *.nextcloud.local + DocumentRoot /var/www/html ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined diff --git a/solid/appinfo/info.xml b/solid/appinfo/info.xml index 16cc1db6..86c0ea28 100644 --- a/solid/appinfo/info.xml +++ b/solid/appinfo/info.xml @@ -11,14 +11,14 @@ It supports the webid-oidc-dpop-pkce login flow to connect to a Solid App with y When you do this, the Solid App can store data in your Nextcloud account through the Solid protocol. ]]> - 0.9.1 + 0.10.0 agpl Auke van Slooten Solid integration https://github.com/pdsinterop/solid-nextcloud/issues - + OCA\Solid\Settings\SolidAdmin diff --git a/solid/appinfo/routes.php b/solid/appinfo/routes.php index 085cf09a..5475341a 100644 --- a/solid/appinfo/routes.php +++ b/solid/appinfo/routes.php @@ -7,58 +7,93 @@ * The controller class has to be registered in the application.php file since * it's instantiated in there */ + +use OC\AllConfig; +use OC\AppConfig; +use OCA\Solid\AppInfo\Application; +use OCP\IConfig; +use OCP\IRequest; + +$routes = [ + ['name' => 'page#approval', 'url' => '/sharing/{clientId}', 'verb' => 'GET'], + ['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'DELETE'], + ['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'POST'], + + ['name' => 'page#handleApproval', 'url' => '/sharing/{clientId}', 'verb' => 'POST'], + ['name' => 'page#customscheme', 'url' => '/customscheme', 'verb' => 'GET'], + + ['name' => 'server#cors', 'url' => '/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], + ['name' => 'server#authorize', 'url' => '/authorize', 'verb' => 'GET'], + ['name' => 'server#jwks', 'url' => '/jwks', 'verb' => 'GET'], + ['name' => 'server#session', 'url' => '/session', 'verb' => 'GET'], + ['name' => 'server#logout', 'url' => '/logout', 'verb' => 'GET'], + ['name' => 'server#token', 'url' => '/token', 'verb' => 'POST'], + ['name' => 'server#userinfo', 'url' => '/userinfo', 'verb' => 'GET'], + ['name' => 'server#register', 'url' => '/register', 'verb' => 'POST'], + ['name' => 'server#registeredClient', 'url' => '/register/{clientId}', 'verb' => 'GET'], + + ['name' => 'solidWebhook#listWebhooks', 'url' => '/webhook/list', 'verb' => 'GET'], + ['name' => 'solidWebhook#register', 'url' => '/webhook/register', 'verb' => 'POST'], + ['name' => 'solidWebhook#unregister', 'url' => '/webhook/unregister', 'verb' => 'POST'], + + ['name' => 'solidWebsocket#register', 'url' => '/websocket/register', 'verb' => 'POST'], + + ['name' => 'app#appLauncher', 'url' => '/', 'verb' => 'GET'], +]; + +$userIdRoutes = [ + ['name' => 'page#profile', 'url' => '/~{userId}/', 'verb' => 'GET'], + + ['name' => 'profile#handleGet', 'url' => '/~{userId}/profile{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handlePut', 'url' => '/~{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handlePatch', 'url' => '/~{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handleHead', 'url' => '/~{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'storage#handleGet', 'url' => '/~{userId}/storage{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePost', 'url' => '/~{userId}/storage{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePut', 'url' => '/~{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handleDelete', 'url' => '/~{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePatch', 'url' => '/~{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handleHead', 'url' => '/~{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'calendar#handleGet', 'url' => '/~{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePost', 'url' => '/~{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePut', 'url' => '/~{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handleDelete', 'url' => '/~{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePatch', 'url' => '/~{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handleHead', 'url' => '/~{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'contacts#handleGet', 'url' => '/~{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePost', 'url' => '/~{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePut', 'url' => '/~{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handleDelete', 'url' => '/~{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePatch', 'url' => '/~{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handleHead', 'url' => '/~{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], +]; + +// @TODO: All routes NOT generated by the UrlGenerator ANYWHERE in the code need to be checked! + +if (Application::$userSubDomainsEnabled) { + $userIdRoutes = array_map(function ($route) { + if ($route['name'] === 'page#profile') { + // The profile route should be `/me` instead of `/~{userId}/` + $route['url'] = '/me'; + } else { + // When UserSubDomains are enabled, all routes that start with + // `/~{userId}/` should just be `/`, as the userId is present + // in the subdomain. + $route['url'] = preg_replace('#^/~{userId}/#', '/', $route['url']); + } + + // The required userId is set to the userId from the subdomain + $host = OC::$server->get(IRequest::class)->getServerHost(); + $userId = explode('.', $host)[0]; + $route['defaults'] = ['userId' => $userId]; + + return $route; + }, $userIdRoutes); +} + return [ - 'routes' => [ - ['name' => 'page#profile', 'url' => '/@{userId}/', 'verb' => 'GET'], - ['name' => 'page#approval', 'url' => '/sharing/{clientId}', 'verb' => 'GET'], - ['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'DELETE'], - ['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'POST'], - - ['name' => 'page#handleApproval', 'url' => '/sharing/{clientId}', 'verb' => 'POST'], - ['name' => 'page#customscheme', 'url' => '/customscheme', 'verb' => 'GET'], - - ['name' => 'server#cors', 'url' => '/{path}', 'verb' => 'OPTIONS', 'requirements' => array('path' => '.+') ], - ['name' => 'server#authorize', 'url' => '/authorize', 'verb' => 'GET'], - ['name' => 'server#jwks', 'url' => '/jwks', 'verb' => 'GET'], - ['name' => 'server#session', 'url' => '/session', 'verb' => 'GET'], - ['name' => 'server#logout', 'url' => '/logout', 'verb' => 'GET'], - ['name' => 'server#token', 'url' => '/token', 'verb' => 'POST'], - ['name' => 'server#userinfo', 'url' => '/userinfo', 'verb' => 'GET'], - ['name' => 'server#register', 'url' => '/register', 'verb' => 'POST'], - ['name' => 'server#registeredClient', 'url' => '/register/{clientId}', 'verb' => 'GET'], - - ['name' => 'profile#handleGet', 'url' => '/@{userId}/profile{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')], - ['name' => 'profile#handlePut', 'url' => '/@{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')], - ['name' => 'profile#handlePatch', 'url' => '/@{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')], - ['name' => 'profile#handleHead', 'url' => '/@{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')], - - ['name' => 'storage#handleGet', 'url' => '/@{userId}/storage{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')], - ['name' => 'storage#handlePost', 'url' => '/@{userId}/storage{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')], - ['name' => 'storage#handlePut', 'url' => '/@{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')], - ['name' => 'storage#handleDelete', 'url' => '/@{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')], - ['name' => 'storage#handlePatch', 'url' => '/@{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')], - ['name' => 'storage#handleHead', 'url' => '/@{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')], - - ['name' => 'calendar#handleGet', 'url' => '/@{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')], - ['name' => 'calendar#handlePost', 'url' => '/@{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')], - ['name' => 'calendar#handlePut', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')], - ['name' => 'calendar#handleDelete', 'url' => '/@{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')], - ['name' => 'calendar#handlePatch', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')], - ['name' => 'calendar#handleHead', 'url' => '/@{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')], - - ['name' => 'contacts#handleGet', 'url' => '/@{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')], - ['name' => 'contacts#handlePost', 'url' => '/@{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')], - ['name' => 'contacts#handlePut', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')], - ['name' => 'contacts#handleDelete', 'url' => '/@{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')], - ['name' => 'contacts#handlePatch', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')], - ['name' => 'contacts#handleHead', 'url' => '/@{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')], - - ['name' => 'solidWebhook#listWebhooks', 'url' => '/webhook/list', 'verb' => 'GET'], - ['name' => 'solidWebhook#register', 'url' => '/webhook/register', 'verb' => 'POST'], - ['name' => 'solidWebhook#unregister', 'url' => '/webhook/unregister', 'verb' => 'POST'], - - ['name' => 'solidWebsocket#register', 'url' => '/websocket/register', 'verb' => 'POST'], - - ['name' => 'app#appLauncher', 'url' => '/', 'verb' => 'GET'], - ] + 'routes' => array_merge($routes, $userIdRoutes), ]; diff --git a/solid/composer.json b/solid/composer.json index 61dbb3b6..e66e4b33 100644 --- a/solid/composer.json +++ b/solid/composer.json @@ -21,7 +21,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.3", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", @@ -30,9 +30,9 @@ "laminas/laminas-diactoros": "^2.8", "lcobucci/jwt": "^4.1", "pdsinterop/flysystem-nextcloud": "^0.2", - "pdsinterop/flysystem-rdf": "^0.5", - "pdsinterop/solid-auth": "v0.11.0", - "pdsinterop/solid-crud": "^0.7.3", + "pdsinterop/flysystem-rdf": "^0.6", + "pdsinterop/solid-auth": "^0.12.2", + "pdsinterop/solid-crud": "^0.8", "psr/log": "^1.1" }, "require-dev": { @@ -45,9 +45,9 @@ "type": "package", "package": { "name": "nextcloud/server", - "version": "27.0.0", + "version": "31.0.0", "dist": { - "url": "https://github.com/nextcloud/server/archive/refs/tags/v27.0.0.zip", + "url": "https://github.com/nextcloud/server/archive/refs/tags/v31.0.0.zip", "type": "zip" }, "source": { diff --git a/solid/composer.lock b/solid/composer.lock index 40fd3303..a7a30c6d 100644 --- a/solid/composer.lock +++ b/solid/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1843d50801f15c12e9fb50345b3bfb3b", + "content-hash": "fa12882f728cefb8c05628ee8e66d9ee", "packages": [ { "name": "arc/base", @@ -670,20 +670,20 @@ }, { "name": "league/event", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/thephpleague/event.git", - "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" + "reference": "062ebb450efbe9a09bc2478e89b7c933875b0935" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", - "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "url": "https://api.github.com/repos/thephpleague/event/zipball/062ebb450efbe9a09bc2478e89b7c933875b0935", + "reference": "062ebb450efbe9a09bc2478e89b7c933875b0935", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { "henrikbjorn/phpspec-code-coverage": "~1.0.1", @@ -718,9 +718,9 @@ ], "support": { "issues": "https://github.com/thephpleague/event/issues", - "source": "https://github.com/thephpleague/event/tree/master" + "source": "https://github.com/thephpleague/event/tree/2.3.0" }, - "time": "2018-11-26T11:52:41+00:00" + "time": "2025-03-14T19:51:10+00:00" }, { "name": "league/flysystem", @@ -1455,24 +1455,24 @@ }, { "name": "pdsinterop/flysystem-rdf", - "version": "v0.5.0", + "version": "v0.6.0", "source": { "type": "git", "url": "https://github.com/pdsinterop/flysystem-rdf.git", - "reference": "2a0b105f66c16b664bcd56f30d76f464b18be065" + "reference": "cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/flysystem-rdf/zipball/2a0b105f66c16b664bcd56f30d76f464b18be065", - "reference": "2a0b105f66c16b664bcd56f30d76f464b18be065", + "url": "https://api.github.com/repos/pdsinterop/flysystem-rdf/zipball/cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d", + "reference": "cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d", "shasum": "" }, "require": { - "easyrdf/easyrdf": "^1.1.1", "ext-mbstring": "*", "league/flysystem": "^1.0", "ml/json-ld": "^1.2", - "php": "^8.0" + "php": "^8.0", + "sweetrdf/easyrdf": "^1.1" }, "require-dev": { "phpunit/phpunit": "^8|^9" @@ -1490,22 +1490,22 @@ "description": "Flysystem plugin to transform RDF data between various serialization formats.", "support": { "issues": "https://github.com/pdsinterop/flysystem-rdf/issues", - "source": "https://github.com/pdsinterop/flysystem-rdf/tree/v0.5.0" + "source": "https://github.com/pdsinterop/flysystem-rdf/tree/v0.6.0" }, - "time": "2022-08-22T14:36:29+00:00" + "time": "2025-05-16T08:57:11+00:00" }, { "name": "pdsinterop/solid-auth", - "version": "v0.11.0", + "version": "v0.12.2", "source": { "type": "git", "url": "https://github.com/pdsinterop/php-solid-auth.git", - "reference": "0c5f65b0a9340fe9d50bef9d0e279db54610ffac" + "reference": "1d1160ee0f7ca71d3e34151aea94232e1cfa49ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/0c5f65b0a9340fe9d50bef9d0e279db54610ffac", - "reference": "0c5f65b0a9340fe9d50bef9d0e279db54610ffac", + "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/1d1160ee0f7ca71d3e34151aea94232e1cfa49ff", + "reference": "1d1160ee0f7ca71d3e34151aea94232e1cfa49ff", "shasum": "" }, "require": { @@ -1514,7 +1514,7 @@ "ext-openssl": "*", "laminas/laminas-diactoros": "^2.8", "lcobucci/jwt": "^4.1", - "league/oauth2-server": "^8.3.5", + "league/oauth2-server": "^8.5.5", "php": "^8.0", "web-token/jwt-core": "^2.2" }, @@ -1539,22 +1539,22 @@ "description": "OAuth2, OpenID and OIDC for Solid Server implementations.", "support": { "issues": "https://github.com/pdsinterop/php-solid-auth/issues", - "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.11.0" + "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.12.2" }, - "time": "2025-02-14T12:57:21+00:00" + "time": "2025-05-28T14:53:41+00:00" }, { "name": "pdsinterop/solid-crud", - "version": "v0.7.3", + "version": "v0.8.0", "source": { "type": "git", "url": "https://github.com/pdsinterop/php-solid-crud.git", - "reference": "c5369ef7b46d3d77a7686c3f4531e818e1797e27" + "reference": "ca1421770b17c69cc5989ce6864e86405030a50c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/php-solid-crud/zipball/c5369ef7b46d3d77a7686c3f4531e818e1797e27", - "reference": "c5369ef7b46d3d77a7686c3f4531e818e1797e27", + "url": "https://api.github.com/repos/pdsinterop/php-solid-crud/zipball/ca1421770b17c69cc5989ce6864e86405030a50c", + "reference": "ca1421770b17c69cc5989ce6864e86405030a50c", "shasum": "" }, "require": { @@ -1562,7 +1562,7 @@ "laminas/laminas-diactoros": "^2.14", "league/flysystem": "^1.0", "mjrider/flysystem-factory": "^0.7", - "pdsinterop/flysystem-rdf": "^0.5", + "pdsinterop/flysystem-rdf": "^0.6", "php": "^8.0", "pietercolpaert/hardf": "^0.3", "psr/http-factory": "^1.0", @@ -1586,9 +1586,9 @@ "description": "Solid HTTPS REST API specification compliant implementation for handling Resource CRUD", "support": { "issues": "https://github.com/pdsinterop/php-solid-crud/issues", - "source": "https://github.com/pdsinterop/php-solid-crud/tree/v0.7.3" + "source": "https://github.com/pdsinterop/php-solid-crud/tree/v0.8.0" }, - "time": "2024-01-17T10:48:57+00:00" + "time": "2025-05-16T09:04:57+00:00" }, { "name": "phrity/net-uri", @@ -1647,24 +1647,25 @@ }, { "name": "phrity/util-errorhandler", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", - "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e" + "reference": "61813189e4525fde4aecad3df849829d526d6f76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e", - "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/61813189e4525fde4aecad3df849829d526d6f76", + "reference": "61813189e4525fde4aecad3df849829d526d6f76", "shasum": "" }, "require": { - "php": "^7.4 | ^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.0", - "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 | ^11.0 | ^12.0", "squizlabs/php_codesniffer": "^3.5" }, "type": "library", @@ -1692,9 +1693,9 @@ ], "support": { "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues", - "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1" + "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.2.0" }, - "time": "2024-09-12T06:49:16+00:00" + "time": "2025-05-26T18:26:51+00:00" }, { "name": "pietercolpaert/hardf", @@ -2128,6 +2129,82 @@ ], "time": "2020-11-03T09:10:25+00:00" }, + { + "name": "sweetrdf/easyrdf", + "version": "1.7", + "source": { + "type": "git", + "url": "https://github.com/sweetrdf/easyrdf.git", + "reference": "6952b79bd1818817f20d0c64de54c7ecd5a24947" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sweetrdf/easyrdf/zipball/6952b79bd1818817f20d0c64de54c7ecd5a24947", + "reference": "6952b79bd1818817f20d0c64de54c7ecd5a24947", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-xmlreader": "*", + "lib-libxml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "ml/json-ld": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "semsol/arc2": "^2.4", + "zendframework/zend-http": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "EasyRdf\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nicholas Humfrey", + "email": "njh@aelius.com", + "homepage": "http://www.aelius.com/njh/", + "role": "Developer" + }, + { + "name": "Alexey Zakhlestin", + "email": "indeyets@gmail.com", + "homepage": "http://indeyets.ru/", + "role": "Developer" + }, + { + "name": "Konrad Abicht", + "email": "hi@inspirito.de", + "homepage": "http://inspirito.de/", + "role": "Maintainer, Developer" + } + ], + "description": "EasyRdf is a PHP library designed to make it easy to consume and produce RDF.", + "keywords": [ + "Linked Data", + "RDF", + "Semantic Web", + "Turtle", + "rdfa", + "sparql" + ], + "support": { + "issues": "https://github.com/sweetrdf/easyrdf/issues", + "source": "https://github.com/sweetrdf/easyrdf/tree/1.7" + }, + "time": "2022-09-19T07:53:57+00:00" + }, { "name": "textalk/websocket", "version": "1.6.3", @@ -2260,16 +2337,16 @@ "packages-dev": [ { "name": "doctrine/dbal", - "version": "4.2.2", + "version": "4.2.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec" + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", - "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", "shasum": "" }, "require": { @@ -2346,7 +2423,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.2.2" + "source": "https://github.com/doctrine/dbal/tree/4.2.3" }, "funding": [ { @@ -2362,30 +2439,33 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:40:56+00:00" + "time": "2025-03-07T18:29:05+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -2405,9 +2485,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/instantiator", @@ -2481,16 +2561,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -2529,7 +2609,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -2537,11 +2617,11 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nextcloud/server", - "version": "27.0.0", + "version": "31.0.0", "source": { "type": "git", "url": "https://github.com/nextcloud/server.git", @@ -2549,7 +2629,7 @@ }, "dist": { "type": "zip", - "url": "https://github.com/nextcloud/server/archive/refs/tags/v27.0.0.zip" + "url": "https://github.com/nextcloud/server/archive/refs/tags/v31.0.0.zip" }, "type": "library", "autoload": { @@ -2563,16 +2643,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -2615,9 +2695,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "phar-io/manifest", @@ -3058,16 +3138,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.22", + "version": "9.6.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", "shasum": "" }, "require": { @@ -3078,7 +3158,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3141,7 +3221,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" }, "funding": [ { @@ -3152,12 +3232,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-12-05T13:48:26+00:00" + "time": "2025-05-02T06:40:34+00:00" }, { "name": "sebastian/cli-parser", @@ -4179,7 +4267,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.1", + "php": "^8.3", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", diff --git a/solid/css/settings-admin.css b/solid/css/settings-admin.css index 1facf521..9ca74f0a 100644 --- a/solid/css/settings-admin.css +++ b/solid/css/settings-admin.css @@ -1,4 +1,4 @@ -#solid-admin label { +#solid-admin label.narrow { width: 160px; vertical-align: top; display: block; @@ -8,4 +8,8 @@ height: 240px; font-size: 12px; font-family: monospace; +} +#solid-admin input.textaligned { + height: 1rem; + min-height: unset; } \ No newline at end of file diff --git a/solid/js/settings-admin.js b/solid/js/settings-admin.js index 8f63de10..9aac545f 100644 --- a/solid/js/settings-admin.js +++ b/solid/js/settings-admin.js @@ -1,4 +1,8 @@ -$(document).ready(function() { +$(document).ready(function () { + $('#solid-enable-user-subdomains').change(function (el) { + OCP.AppConfig.setValue('solid', 'userSubDomainsEnabled', this.checked ? true : false) + }) + $('#solid-private-key').change(function(el) { OCP.AppConfig.setValue('solid', 'privateKey', this.value); }); diff --git a/solid/lib/AppInfo/Application.php b/solid/lib/AppInfo/Application.php index 5436450b..3080fb03 100644 --- a/solid/lib/AppInfo/Application.php +++ b/solid/lib/AppInfo/Application.php @@ -4,31 +4,22 @@ namespace OCA\Solid\AppInfo; -use OC\AppFramework\Utility\TimeFactory; -use OC\Authentication\Events\AppPasswordCreatedEvent; -use OC\Authentication\Token\IProvider; -use OC\Server; +use OC; +use OC\AppConfig; -use OCA\Solid\Service\UserService; use OCA\Solid\Service\SolidWebhookService; use OCA\Solid\Db\SolidWebhookMapper; -use OCA\Solid\WellKnown\OpenIdConfigurationHandler; -use OCA\Solid\WellKnown\SolidHandler; use OCA\Solid\Middleware\SolidCorsMiddleware; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; -use OCP\AppFramework\IAppContainer; -use OCP\Defaults; -use OCP\IServerContainer; -use OCP\Settings\IManager; -use OCP\Util; use OCP\IDBConnection; class Application extends App implements IBootstrap { public const APP_ID = 'solid'; + public static $userSubDomainsEnabled; /** * @param array $urlParams @@ -76,5 +67,6 @@ public function register(IRegistrationContext $context): void { } public function boot(IBootContext $context): void { + self::$userSubDomainsEnabled = OC::$server->get(AppConfig::class)->getValueBool(self::APP_ID, 'userSubDomainsEnabled'); } } diff --git a/solid/lib/BaseServerConfig.php b/solid/lib/BaseServerConfig.php index 2b9de07c..7c391893 100644 --- a/solid/lib/BaseServerConfig.php +++ b/solid/lib/BaseServerConfig.php @@ -1,14 +1,18 @@ config = $config; } @@ -91,7 +95,7 @@ public function getClients() { $clients[] = [ "clientId" => $matches[1], "clientName" => $clientRegistration['client_name'], - "clientBlocked" => $clientRegistration['blocked'] + "clientBlocked" => $clientRegistration['blocked'] ?? false, ]; } } @@ -152,6 +156,7 @@ public function removeClientConfig($clientId) { unset($scopes[$clientId]); $this->config->setAppValue('solid', 'clientScopes', $scopes); } + public function saveClientRegistration($origin, $clientData) { $originHash = md5($origin); $existingRegistration = $this->getClientRegistration($originHash); @@ -182,4 +187,57 @@ public function getClientRegistration($clientId) { $data = $this->config->getAppValue('solid', "client-" . $clientId, "{}"); return json_decode($data, true); } + + public function getUserSubDomainsEnabled() { + $value = $this->config->getAppValue('solid', 'userSubDomainsEnabled', false); + + return $this->castToBool($value); + } + + public function setUserSubDomainsEnabled($enabled) { + $value = $this->castToBool($enabled); + + $this->config->setAppValue('solid', 'userSubDomainsEnabled', $value); + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + private function castToBool(string $mixedValue): bool + { + $type = gettype($mixedValue); + + if ($type === 'boolean' || $type === 'NULL' || $type === 'integer') { + $value = (bool) $mixedValue; + } else { + if ($type === 'string') { + $mixedValue = strtolower($mixedValue); + if ($mixedValue === 'true' || $mixedValue === '1') { + $value = true; + } elseif ($mixedValue === 'false' || $mixedValue === '0' || $mixedValue === '') { + $value = false; + } else { + $error = [ + 'invalid' => 'value', + 'for' => 'userSubDomainsEnabled', + 'received' => $mixedValue, + 'expected' => implode(',', ['true', 'false', '1', '0']) + ]; + } + } else { + $error = [ + 'invalid' => 'type', + 'for' => 'userSubDomainsEnabled', + 'received' => $type, + 'expected' => implode(',', ['boolean', 'NULL', 'integer', 'string']) + ]; + } + } + + if (isset($error)) { + $errorMessage = vsprintf(self::ERROR_INVALID_ARGUMENT, $error); + throw new InvalidArgumentException($errorMessage); + } + + return $value; + } } diff --git a/solid/lib/Controller/AppController.php b/solid/lib/Controller/AppController.php index 94addc28..db34f5bd 100644 --- a/solid/lib/Controller/AppController.php +++ b/solid/lib/Controller/AppController.php @@ -2,35 +2,38 @@ namespace OCA\Solid\Controller; use OCA\Solid\ServerConfig; -use OCP\IRequest; -use OCP\IUserManager; -use OCP\Contacts\IManager; -use OCP\IURLGenerator; -use OCP\IConfig; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Http\DataResponse; + use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Contacts\IManager; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserManager; class AppController extends Controller { + use GetStorageUrlTrait; + + protected ServerConfig $config; + protected IURLGenerator $urlGenerator; + private $userId; private $userManager; - private $urlGenerator; - private $config; - public function __construct($AppName, IRequest $request, IConfig $config, IUserManager $userManager, IManager $contactsManager, IURLGenerator $urlGenerator, $userId){ + public function __construct($AppName, IRequest $request, IConfig $config, IUserManager $userManager, IManager $contactsManager, IURLGenerator $urlGenerator, $userId) { parent::__construct($AppName, $request); $this->userId = $userId; $this->userManager = $userManager; $this->contactsManager = $contactsManager; $this->request = $request; $this->urlGenerator = $urlGenerator; - $this->config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->config = new ServerConfig($config, $urlGenerator, $userManager); } - private function getUserApps($userId) { + private function getUserApps($userId) { $userApps = []; if ($this->userManager->userExists($userId)) { $allowedClients = $this->config->getAllowedClients($userId); @@ -46,7 +49,7 @@ private function getAppsList() { $path = __DIR__ . "/../solid-app-list.json"; $appsListJson = file_get_contents($path); $appsList = json_decode($appsListJson, true); - + $userApps = $this->getUserApps($this->userId); foreach ($appsList as $key => $app) { @@ -64,11 +67,7 @@ private function getAppsList() { private function getProfilePage() { return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $this->userId, "path" => "/card"))) . "#me"; } - private function getStorageUrl($userId) { - $storageUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleHead", array("userId" => $userId, "path" => "foo"))); - $storageUrl = preg_replace('/foo$/', '', $storageUrl); - return $storageUrl; - } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/solid/lib/Controller/CalendarController.php b/solid/lib/Controller/CalendarController.php index a9e773bb..9030dabb 100644 --- a/solid/lib/Controller/CalendarController.php +++ b/solid/lib/Controller/CalendarController.php @@ -188,15 +188,26 @@ public function handlePost($userId, $path) { public function handlePut() { // $userId, $path) { // FIXME: Adding the correct variables in the function name will make nextcloud // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - + // because we got here, the request uri should look like: - // /index.php/apps/solid/@{userId}/storage{path} - $pathInfo = explode("@", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^calendar/", "", $path); - + // - if we have user subdomains enabled: + // /index.php/apps/solid/calendar{path} + // and otherwise: + // index.php/apps/solid/~{userId}/calendar{path} + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("calendar/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^calendar/", "", $path); + } + return $this->handleRequest($userId, $path); } /** diff --git a/solid/lib/Controller/ContactsController.php b/solid/lib/Controller/ContactsController.php index 1fc3fcec..0363add2 100644 --- a/solid/lib/Controller/ContactsController.php +++ b/solid/lib/Controller/ContactsController.php @@ -189,15 +189,26 @@ public function handlePost($userId, $path) { public function handlePut() { // $userId, $path) { // FIXME: Adding the correct variables in the function name will make nextcloud // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - + // because we got here, the request uri should look like: - // /index.php/apps/solid/@{userId}/storage{path} - $pathInfo = explode("@", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^contacts/", "", $path); - + // - if we have user subdomains enabled: + // /index.php/apps/solid/contacts{path} + // and otherwise: + // index.php/apps/solid/~{userId}/contacts{path} + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("contacts/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^contacts/", "", $path); + } + return $this->handleRequest($userId, $path); } /** diff --git a/solid/lib/Controller/GetStorageUrlTrait.php b/solid/lib/Controller/GetStorageUrlTrait.php new file mode 100644 index 00000000..5be0f308 --- /dev/null +++ b/solid/lib/Controller/GetStorageUrlTrait.php @@ -0,0 +1,90 @@ +config = $config; + } + + final public function setUrlGenerator(IURLGenerator $urlGenerator): void + { + $this->urlGenerator = $urlGenerator; + } + + ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + protected ServerConfig $config; + protected IURLGenerator $urlGenerator; + + /////////////////////////////// PROTECTED API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + /** + * @FIXME: Add check for bob.nextcloud.local/solid/alice to throw 404 + * @TODO: Use route without `~alice` in /apps/solid/~alice/profile/card#me when user-domains are enabled + */ + public function getStorageUrl($userId) { + $routeUrl = $this->urlGenerator->linkToRoute( + 'solid.storage.handleHead', + ['userId' => $userId, 'path' => 'foo'] + ); + + $storageUrl = $this->urlGenerator->getAbsoluteURL($routeUrl); + + $storageUrl = preg_replace('/foo$/', '', $storageUrl); + + if ($this->config->getUserSubDomainsEnabled()) { + $url = parse_url($storageUrl); + + if (strpos($url['host'], $userId . '.') !== false) { + $url['host'] = str_replace($userId . '.', '', $url['host']); + } + + $url['host'] = $userId . '.' . $url['host']; // $storageUrl = $userId . '.' . $storageUrl; + $storageUrl = $this->buildUrl($url); + } + + return $storageUrl; + } + + public function validateUrl(RequestInterface $request): bool { + $isValid = false; + + $host = $request->getUri()->getHost(); + $path = $request->getUri()->getPath(); + $pathParts = explode('/', $path); + + $pathUsers = array_filter($pathParts, static function ($value) { + return str_starts_with($value, '~'); + }); + + if (count($pathUsers) === 1) { + $pathUser = reset($pathUsers); + $subDomainUser = explode('.', $host)[0]; + + $isValid = $pathUser === '~' . $subDomainUser; + } + + return $isValid; + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + private function buildUrl(array $parts) { + // @FIXME: Replace with existing more robust URL builder + return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') . + (isset($parts['host']) ? "//{$parts['host']}" : '') . + (isset($parts['port']) ? ":{$parts['port']}" : '') . + (isset($parts['path']) ? "{$parts['path']}" : '') . + (isset($parts['query']) ? "?{$parts['query']}" : '') . + (isset($parts['fragment']) ? "#{$parts['fragment']}" : ''); + } +} diff --git a/solid/lib/Controller/ProfileController.php b/solid/lib/Controller/ProfileController.php index 578e6239..0f7cfec8 100644 --- a/solid/lib/Controller/ProfileController.php +++ b/solid/lib/Controller/ProfileController.php @@ -4,6 +4,7 @@ use OCA\Solid\DpopFactoryTrait; use OCA\Solid\PlainResponse; use OCA\Solid\Notifications\SolidNotifications; +use OCA\Solid\ServerConfig; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -21,13 +22,14 @@ class ProfileController extends Controller { use DpopFactoryTrait; + use GetStorageUrlTrait; - /* @var IURLGenerator */ - private $urlGenerator; + protected ServerConfig $config; + protected IURLGenerator $urlGenerator; /* @var ISession */ private $session; - + public function __construct( $AppName, IRequest $request, @@ -82,7 +84,7 @@ private function getFileSystem($userId) { $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); $filesystem->addPlugin($plugin); @@ -102,7 +104,7 @@ private function generateDefaultAcl($userId) { acl:accessTo <./>; acl:default <./>; acl:mode acl:Read. - + # The owner has full access to every resource in their pod. # Other agents have no access rights, # unless specifically authorized in other .acl resources. @@ -131,11 +133,6 @@ private function getProfileUrl($userId) { $profileUrl = preg_replace('/foo$/', '', $profileUrl); return $profileUrl; } - private function getStorageUrl($userId) { - $storageUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleHead", array("userId" => $userId, "path" => "foo"))); - $storageUrl = preg_replace('/foo$/', '/', $storageUrl); - return $storageUrl; - } /** * @PublicPage @@ -150,11 +147,11 @@ public function handleRequest($userId, $path) { $this->filesystem = $this->getFileSystem($userId); - $this->resourceServer = new ResourceServer($this->filesystem, $this->response); + $this->resourceServer = new ResourceServer($this->filesystem, $this->response); $this->WAC = new WAC($this->filesystem); $request = $this->rawRequest; - $baseUrl = $this->getProfileUrl($userId); + $baseUrl = $this->getProfileUrl($userId); $this->resourceServer->setBaseUrl($baseUrl); $this->WAC->setBaseUrl($baseUrl); $notifications = new SolidNotifications(); @@ -179,20 +176,21 @@ public function handleRequest($userId, $path) { return $this->respond($response); } - $response = $this->resourceServer->respondToRequest($request); + $response = $this->resourceServer->respondToRequest($request); $response = $this->WAC->addWACHeaders($request, $response, $webId); return $this->respond($response); } - + /** * @PublicPage * @NoAdminRequired * @NoCSRFRequired */ - public function handleGet($userId, $path) { + public function handleGet($userId, $path) { + //TODO: check that the $userId matches the userDomain, if enabled. return $this->handleRequest($userId, $path); } - + /** * @PublicPage * @NoAdminRequired @@ -209,15 +207,25 @@ public function handlePost($userId, $path) { public function handlePut() { // $userId, $path) { // FIXME: Adding the correct variables in the function name will make nextcloud // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - + // because we got here, the request uri should look like: - // /index.php/apps/solid/@{userId}/storage{path} - $pathInfo = explode("@", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^profile/", "", $path); - + // - if we have user subdomains enabled: + // /index.php/apps/solid/profile{path} + // and otherwise: + // index.php/apps/solid/~{userId}/profile{path} + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("profile/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^profile/", "", $path); + } + return $this->handleRequest($userId, $path); } /** @@ -287,6 +295,7 @@ private function getUserProfile($userId) { } } } + //TODO: privateTypeIndex and publisTypeIndex need to user getStorageURL if ($user !== null) { $profile = array( 'id' => $userId, @@ -297,7 +306,7 @@ private function getUserProfile($userId) { 'preferences' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/preferences.ttl"))), 'privateTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/privateTypeIndex.ttl"))), 'publicTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/publicTypeIndex.ttl"))), - 'storage' => $this->getStorageUrl($userId), + 'storage' => $this->getStorageUrl($userId) . "/", 'issuer' => $this->urlGenerator->getBaseURL() ); return $profile; @@ -322,9 +331,9 @@ private function generateTurtleProfile($userId) { @prefix inbox: <>. @prefix sp: . @prefix ser: <>. - + pro:card a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me. - + :me a schem:Person, foaf:Person; ldp:inbox inbox:; diff --git a/solid/lib/Controller/SolidWebhookController.php b/solid/lib/Controller/SolidWebhookController.php index 6a88a81e..5846097d 100644 --- a/solid/lib/Controller/SolidWebhookController.php +++ b/solid/lib/Controller/SolidWebhookController.php @@ -3,42 +3,35 @@ namespace OCA\Solid\Controller; use Closure; -use OCA\Solid\AppInfo\Application; -use OCA\Solid\Service\SolidWebhookService; -use OCA\Solid\ServerConfig; -use OCA\Solid\PlainResponse; -use OCA\Solid\Notifications\SolidNotifications; + use OCA\Solid\DpopFactoryTrait; +use OCA\Solid\PlainResponse; +use OCA\Solid\ServerConfig; +use OCA\Solid\Service\SolidWebhookService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\Files\IRootFolder; +use OCP\IConfig; +use OCP\IDBConnection; use OCP\IRequest; -use OCP\IUserManager; -use OCP\IURLGenerator; use OCP\ISession; -use OCP\IDBConnection; -use OCP\IConfig; -use OCP\Files\IRootFolder; -use OCP\Files\IHomeStorage; -use OCP\Files\SimpleFS\ISimpleRoot; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\Response; -use OCP\AppFramework\Http\JSONResponse; -use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\IURLGenerator; +use OCP\IUserManager; -use Pdsinterop\Solid\Resources\Server as ResourceServer; -use Pdsinterop\Solid\Auth\Utils\DPop as DPop; use Pdsinterop\Solid\Auth\WAC as WAC; class SolidWebhookController extends Controller { use DpopFactoryTrait; + use GetStorageUrlTrait; - /* @var IURLGenerator */ - private $urlGenerator; + protected ServerConfig $config; + protected IURLGenerator $urlGenerator; /* @var ISession */ private $session; - + /** @var SolidWebhookService */ private $webhookService; @@ -139,18 +132,13 @@ private function getFileSystem() { $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); $filesystem->addPlugin($plugin); return $filesystem; } - private function getStorageUrl($userId) { - $storageUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleHead", array("userId" => $userId, "path" => "foo"))); - $storageUrl = preg_replace('/foo$/', '', $storageUrl); - return $storageUrl; - } private function getAppBaseUrl() { $appBaseUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.app.appLauncher")); return $appBaseUrl; @@ -162,20 +150,20 @@ private function initializeStorage($userId) { } private function parseTopic($topic) { - // topic = https://nextcloud.server/solid/@alice/storage/foo/bar + // topic = https://nextcloud.server/solid/~alice/storage/foo/bar $appBaseUrl = $this->getAppBaseUrl(); // https://nextcloud.server/solid/ - $internalUrl = str_replace($appBaseUrl, '', $topic); // @alice/storage/foo/bar + $internalUrl = str_replace($appBaseUrl, '', $topic); // ~alice/storage/foo/bar $pathicles = explode("/", $internalUrl); - $userId = $pathicles[0]; // @alice - $userId = preg_replace("/^@/", "", $userId); // alice - $storageUrl = $this->getStorageUrl($userId); // https://nextcloud.server/solid/@alice/storage/ + $userId = $pathicles[0]; // ~alice + $userId = preg_replace("/^~/", "", $userId); // alice + $storageUrl = $this->getStorageUrl($userId); // https://nextcloud.server/solid/~alice/storage/ $storagePath = str_replace($storageUrl, '/', $topic); // /foo/bar return array( "userId" => $userId, "path" => $storagePath ); } - + private function createGetRequest($topic) { $serverParams = []; $fileParams = []; @@ -192,15 +180,15 @@ private function createGetRequest($topic) { $headers ); } - + private function checkReadAccess($topic) { - // split out $topic into $userId and $path https://nextcloud.server/solid/@alice/storage/foo/bar + // split out $topic into $userId and $path https://nextcloud.server/solid/~alice/storage/foo/bar // - userId in this case is the pod owner (not the one doing the request). (alice) // - path is the path within the storage pod (/foo/bar) $target = $this->parseTopic($topic); $userId = $target["userId"]; $path = $target["path"]; - + $this->initializeStorage($userId); $this->WAC = new WAC($this->filesystem); diff --git a/solid/lib/Controller/StorageController.php b/solid/lib/Controller/StorageController.php index c5a66735..4a834d3b 100644 --- a/solid/lib/Controller/StorageController.php +++ b/solid/lib/Controller/StorageController.php @@ -1,13 +1,15 @@ addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); $filesystem->addPlugin($plugin); @@ -90,11 +91,7 @@ private function getFileSystem() { private function getUserProfile($userId) { return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; } - private function getStorageUrl($userId) { - $storageUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleHead", array("userId" => $userId, "path" => "foo"))); - $storageUrl = preg_replace('/foo$/', '', $storageUrl); - return $storageUrl; - } + private function generateDefaultAcl($userId) { $defaultAcl = <<< EOF # Root ACL resource for the user account @@ -303,7 +300,7 @@ public function handleRequest($userId, $path) { $this->WAC = new WAC($this->filesystem); $request = $this->rawRequest; - $baseUrl = $this->getStorageUrl($userId); + $baseUrl = $this->getStorageUrl($userId); $this->resourceServer->setBaseUrl($baseUrl); $this->WAC->setBaseUrl($baseUrl); @@ -351,20 +348,20 @@ public function handleRequest($userId, $path) { ->withStatus(403, "Access denied"); return $this->respond($response); } - $response = $this->resourceServer->respondToRequest($request); + $response = $this->resourceServer->respondToRequest($request); $response = $this->WAC->addWACHeaders($request, $response, $webId); return $this->respond($response); } - + /** * @PublicPage * @NoAdminRequired * @NoCSRFRequired */ - public function handleGet($userId, $path) { + public function handleGet($userId, $path) { return $this->handleRequest($userId, $path); } - + /** * @PublicPage * @NoAdminRequired @@ -381,15 +378,25 @@ public function handlePost($userId, $path) { public function handlePut() { // $userId, $path) { // FIXME: Adding the correct variables in the function name will make nextcloud // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - + // because we got here, the request uri should look like: - // /index.php/apps/solid/@{userId}/storage{path} - $pathInfo = explode("@", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^storage/", "", $path); - + // - if we have user subdomains enabled: + // /index.php/apps/solid/storage{path} + // and otherwise: + // index.php/apps/solid/~{userId}/storage{path} + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("storage/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^storage/", "", $path); + } + return $this->handleRequest($userId, $path); } /** @@ -434,7 +441,7 @@ private function respond($response) { // $result->addHeader('Access-Control-Allow-Credentials', 'true'); // $result->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // $result->addHeader('Access-Control-Allow-Origin', $origin); - + $policy = new EmptyContentSecurityPolicy(); $policy->addAllowedStyleDomain("*"); $policy->addAllowedStyleDomain("data:"); diff --git a/solid/lib/Sections/SolidAdmin.php b/solid/lib/Sections/SolidAdmin.php index 59a5c9b1..1e66b429 100644 --- a/solid/lib/Sections/SolidAdmin.php +++ b/solid/lib/Sections/SolidAdmin.php @@ -29,4 +29,5 @@ public function getName(): string { public function getPriority(): int { return 98; } + } diff --git a/solid/lib/ServerConfig.php b/solid/lib/ServerConfig.php index 8313b1f8..9874cb6c 100644 --- a/solid/lib/ServerConfig.php +++ b/solid/lib/ServerConfig.php @@ -10,7 +10,6 @@ * @package OCA\Solid */ class ServerConfig extends BaseServerConfig { - private IConfig $config; private IUrlGenerator $urlGenerator; private IUserManager $userManager; @@ -23,6 +22,7 @@ public function __construct(IConfig $config, IUrlGenerator $urlGenerator, IUserM $this->config = $config; $this->userManager = $userManager; $this->urlGenerator = $urlGenerator; + parent::__construct($config); } diff --git a/solid/lib/Settings/SolidAdmin.php b/solid/lib/Settings/SolidAdmin.php index 2fc684f8..a4d4e73d 100644 --- a/solid/lib/Settings/SolidAdmin.php +++ b/solid/lib/Settings/SolidAdmin.php @@ -25,9 +25,10 @@ public function getForm() { $allClients = $this->serverConfig->getClients(); $parameters = [ - 'privateKey' => $this->serverConfig->getPrivateKey(), - 'encryptionKey' => $this->serverConfig->getEncryptionKey(), - 'clients' => $allClients + 'clients' => $allClients, + 'encryptionKey' => $this->serverConfig->getEncryptionKey(), + 'privateKey' => $this->serverConfig->getPrivateKey(), + 'userSubDomainsEnabled' => $this->serverConfig->getUserSubDomainsEnabled(), ]; return new TemplateResponse('solid', 'admin', $parameters, ''); diff --git a/solid/templates/admin.php b/solid/templates/admin.php index c414498c..616f7edb 100644 --- a/solid/templates/admin.php +++ b/solid/templates/admin.php @@ -1,19 +1,37 @@
-

t('Solid OpenID Connect Settings')); ?>

+

t('Solid Server Settings')); ?>

+

+ +

t('Solid OpenID Connect Settings')); ?>

+

+ -

\ No newline at end of file diff --git a/solid/tests/Unit/BaseServerConfigTest.php b/solid/tests/Unit/BaseServerConfigTest.php new file mode 100644 index 00000000..aeb70bad --- /dev/null +++ b/solid/tests/Unit/BaseServerConfigTest.php @@ -0,0 +1,105 @@ +expectException(TypeError::class); + $this->expectExceptionMessage('Too few arguments to function'); + + new BaseServerConfig(); + } + + /** + * @testdox BaseServerConfig should be instantiated when given a valid Configuration + * @covers ::__construct + */ + public function testConstructorWithValidConfig() + { + $configMock = $this->createMock(IConfig::class); + + $baseServerConfig = new BaseServerConfig($configMock); + + $this->assertInstanceOf(BaseServerConfig::class, $baseServerConfig); + } + + /** + * @testdox BaseServerConfig should return a boolean when asked whether UserSubDomains are Enabled + * @covers ::getUserSubDomainsEnabled + * @dataProvider provideBooleans + */ + public function testGetUserSubDomainsEnabled($expected) + { + $configMock = $this->createMock(IConfig::class); + $configMock->method('getAppValue')->willReturn($expected); + + $baseServerConfig = new BaseServerConfig($configMock); + $actual = $baseServerConfig->getUserSubDomainsEnabled(); + + $this->assertEquals($expected, $actual); + } + + /** + * @testdox BaseServerConfig should get value from AppConfig when asked whether UserSubDomains are Enabled + * @covers ::getUserSubDomainsEnabled + */ + public function testGetUserSubDomainsEnabledFromAppConfig() + { + $configMock = $this->createMock(IConfig::class); + $configMock->expects($this->atLeast(1)) + ->method('getAppValue') + ->with(Application::APP_ID, 'userSubDomainsEnabled', false) + ->willReturn(true); + + $baseServerConfig = new BaseServerConfig($configMock); + $actual = $baseServerConfig->getUserSubDomainsEnabled(); + + $this->assertTrue($actual); + } + + /** + * @testdox BaseServerConfig should set value in AppConfig when asked to set UserSubDomainsEnabled + * @covers ::setUserSubDomainsEnabled + * + * @dataProvider provideBooleans + */ + public function testSetUserSubDomainsEnabled($expected) + { + $configMock = $this->createMock(IConfig::class); + $configMock->expects($this->atLeast(1)) + ->method('setAppValue') + ->with(Application::APP_ID, 'userSubDomainsEnabled', $expected) + ; + + $baseServerConfig = new BaseServerConfig($configMock); + $baseServerConfig->setUserSubDomainsEnabled($expected); + } + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + public function provideBooleans() + { + return [ + 'false' => [false], + 'true' => [true], + ]; + } +} diff --git a/solid/tests/Unit/Controller/GetStorageUrlTraitTest.php b/solid/tests/Unit/Controller/GetStorageUrlTraitTest.php new file mode 100644 index 00000000..34c73f5c --- /dev/null +++ b/solid/tests/Unit/Controller/GetStorageUrlTraitTest.php @@ -0,0 +1,190 @@ +trait = new class { + use GetStorageUrlTrait; + }; + } + + /////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + /** + * @testdox GetStorageUrlTrait should complain when called before given a UrlGenerator + * @covers ::getStorageUrl + */ + public function testGetStorageUrlWithoutUrlGenerator() + { + $this->expectException(Error::class); + $this->expectExceptionMessage('urlGenerator must not be accessed before initialization'); + + $this->trait->getStorageUrl(self::MOCK_USER_ID); + } + + /** + * @testdox GetStorageUrlTrait should complain when called before given a Configuration + * @covers ::getStorageUrl + */ + public function testGetStorageUrlWithoutConfig() + { + $mockUrlGenerator = $this->getMockUrlGenerator(self::MOCK_URL); + + $this->expectException(Error::class); + $this->expectExceptionMessage('config must not be accessed before initialization'); + + $this->trait->setUrlGenerator($mockUrlGenerator); + + $this->trait->getStorageUrl(self::MOCK_USER_ID); + } + + /** + * @testdox GetStorageUrlTrait should return a string when called with a UrlGenerator and Configuration + * @covers ::getStorageUrl + * @dataProvider provideSubDomainsDisabledUrls + */ + public function testGetStorageUrlWithUserSubDomainsDisabled($url, $userId, $expected) + { + $mockConfig = $this->getMockConfig(); + $mockUrlGenerator = $this->getMockUrlGenerator($url); + + $this->trait->setUrlGenerator($mockUrlGenerator); + $this->trait->setConfig($mockConfig); + + $actual = $this->trait->getStorageUrl($userId); + + $this->assertEquals($expected, $actual); + } + + /** + * @testdox GetStorageUrlTrait should return a string when called with a UrlGenerator and Configuration + * @covers ::getStorageUrl + * @covers ::buildUrl + * + * @dataProvider provideSubDomainsEnabledUrls + */ + public function testGetStorageUrlWithUserSubDomainsEnabled($url, $userId, $expected) + { + $mockUrlGenerator = $this->getMockUrlGenerator($url); + $mockConfig = $this->getMockConfig(true); + + $this->trait->setUrlGenerator($mockUrlGenerator); + $this->trait->setConfig($mockConfig); + + $actual = $this->trait->getStorageUrl($userId); + + $this->assertEquals($expected, $actual); + } + + /** + * @testdox GetStorageUrlTrait should return expected validity when asked to validateUrl + * + * @covers ::validateUrl + * + * @dataProvider provideRequests + */ + public function testValidateUrl(RequestInterface $response, $expected) + { + $actual = $this->trait->validateUrl($response); + + $this->assertEquals($expected, $actual); + } + + ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + public function getMockConfig($enabled = false): MockObject|ServerConfig + { + $mockConfig = $this->getMockBuilder(ServerConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $mockConfig->expects($this->any()) + ->method('getUserSubDomainsEnabled') + ->willReturn($enabled); + + return $mockConfig; + } + + public function getMockUrlGenerator($url): MockObject|IURLGenerator + { + $mockUrlGenerator = $this + ->getMockBuilder(IURLGenerator::class) + ->getMock(); + + $mockUrlGenerator->expects($this->atLeast(1)) + ->method('getAbsoluteURL') + ->willReturn($url); + + return $mockUrlGenerator; + } + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + public function provideRequests() + { + $request = new Request(); + + return [ + 'invalid: invalid URL' => ['request' => $request->withUri(new Uri('!@#$%^&*()_')), 'expected' => false], + 'invalid: no domain user' => ['request' => $request->withUri(new Uri('https://example.com/@alice/profile/card#me')), 'expected' => false], + 'invalid: no path or domain user' => ['request' => $request->withUri(new Uri('https://example.com/')), 'expected' => false], + 'invalid: no path user' => ['request' => $request->withUri(new Uri('https://alice.example.com/profile/card#me')), 'expected' => false], + 'invalid: no URL' => ['request' => $request, 'expected' => false], + 'invalid: path and domain user mismatch' => ['request' => $request->withUri(new Uri('https://bob.example.com/@alice/profile/card#me')), 'expected' => false], + 'valid: minimal path and domain user match' => ['request' => $request->withUri(new Uri('https://alice.example.com/apps/@alice')), 'expected' => true], + 'valid: path and domain user match' => ['request' => $request->withUri(new Uri('https://alice.example.com/apps/solid/@alice/profile/card#me')), 'expected' => true], + ]; + } + + public function provideSubDomainsDisabledUrls() + { + return [ + ['url' => 'example.com/foo', 'userId' => 'alice', 'expected' => 'example.com//'], + ['url' => 'https://example.com/foo', 'userId' => 'alice', 'expected' => 'https://example.com//'], + ['url' => 'http://example.com/foo', 'userId' => 'alice', 'expected' => 'http://example.com//'], + ['url' => 'https://bob.example.com/foo', 'userId' => 'alice', 'expected' => 'https://bob.example.com//'], + ['url' => 'http://bob.example.com/foo', 'userId' => 'alice', 'expected' => 'http://bob.example.com//'], + ['url' => 'https://bob.example.com/foo', 'userId' => 'bob', 'expected' => 'https://bob.example.com//'], + ['url' => 'http://bob.example.com/foo', 'userId' => 'bob', 'expected' => 'http://bob.example.com//'], + ]; + } + + public function provideSubDomainsEnabledUrls() + { + return [ + // @FIXME: "Undefined array key 'host'" caused by the use of `parse_url` + // ['url' => 'example.com/foo', 'userId' => 'alice', 'expected' => 'example.com//'], + + ['url' => 'https://example.com/foo', 'userId' => 'alice', 'expected' => 'https://alice.example.com//'], + ['url' => 'http://example.com/foo', 'userId' => 'alice', 'expected' => 'http://alice.example.com//'], + ['url' => 'https://bob.example.com/foo', 'userId' => 'alice', 'expected' => 'https://alice.bob.example.com//'], + ['url' => 'http://bob.example.com/foo', 'userId' => 'alice', 'expected' => 'http://alice.bob.example.com//'], + ['url' => 'https://bob.example.com/foo', 'userId' => 'bob', 'expected' => 'https://bob.example.com//'], + ['url' => 'http://bob.example.com/foo', 'userId' => 'bob', 'expected' => 'http://bob.example.com//'], + ]; + } +}