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..ee057ca0 --- /dev/null +++ b/.config/hadolint.yml @@ -0,0 +1,31 @@ +--- +# 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..19f6071b --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,122 @@ +--- +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" + # 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.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..3ef3d51d --- /dev/null +++ b/.github/workflows/shell.yml @@ -0,0 +1,60 @@ +--- +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: + args: >- + 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 90% rename from .github/workflows/ci.yml rename to .github/workflows/solid-tests-suites.yml index 4aa46902..739960d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/solid-tests-suites.yml @@ -28,15 +28,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # For the latest version information see: https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule + # For the latest version information see: + # https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule # Versions before 22 are not tested as they run on PHP versions lower than 8.0 # Versions before 24 are not tested as they do not support `.well-known` entries # 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 +47,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 }} @@ -69,7 +70,8 @@ jobs: . docker push "ghcr.io/pdsinterop/solid-nextcloud:${{ env.TAG }}" mkdir -p cache/solid-nextcloud - docker image save solid-nextcloud:${{ env.TAG }} --output ./cache/solid-nextcloud/${{ github.sha }}-${{ matrix.nextcloud_version }}.tar + docker image save solid-nextcloud:${{ env.TAG }} \ + --output ./cache/solid-nextcloud/${{ github.sha }}-${{ matrix.nextcloud_version }}.tar solid-testsuite: timeout-minutes: 30 @@ -82,16 +84,16 @@ 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('[29,30,31]'), matrix.nextcloud_version) == false }} steps: - name: Create docker tag from git reference @@ -101,15 +103,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..62d0c2eb --- /dev/null +++ b/.github/workflows/xml.yml @@ -0,0 +1,45 @@ +--- +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: + args: >- + 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..84340bee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ -ARG NEXTCLOUD_VERSION -FROM nextcloud:${NEXTCLOUD_VERSION} +#ARG NEXTCLOUD_VERSION +#FROM nextcloud:${NEXTCLOUD_VERSION} +FROM nextcloud:31 -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..c19f2732 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.9.2 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..671c009f 100644 --- a/solid/composer.json +++ b/solid/composer.json @@ -30,15 +30,15 @@ "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": { "doctrine/dbal": "*", "nextcloud/server": "*", - "phpunit/phpunit": "^8 || ^9" + "phpunit/phpunit": "^8.5.32" }, "repositories": [ { diff --git a/solid/composer.lock b/solid/composer.lock index 40fd3303..49a582ca 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": "630d8401030511a28cf54157d9bbd4cf", "packages": [ { "name": "arc/base", @@ -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", @@ -2128,6 +2128,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", @@ -2366,26 +2442,29 @@ }, { "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,36 +2484,36 @@ "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", - "version": "2.0.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -2461,7 +2540,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -2477,20 +2556,20 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "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 +2608,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,7 +2616,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nextcloud/server", @@ -2561,64 +2640,6 @@ } } }, - { - "name": "nikic/php-parser", - "version": "v5.4.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" - }, - "time": "2024-12-30T11:07:19+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -2739,44 +2760,40 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "7.0.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", + "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", "shasum": "" }, "require": { "ext-dom": "*", - "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", - "theseer/tokenizer": "^1.2.3" + "php": ">=7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.3 || ^4.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -2804,8 +2821,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.17" }, "funding": [ { @@ -2813,32 +2829,32 @@ "type": "github" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2024-03-02T06:09:37+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "69deeb8664f611f156a924154985fbd4911eb36b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/69deeb8664f611f156a924154985fbd4911eb36b", + "reference": "69deeb8664f611f156a924154985fbd4911eb36b", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2865,7 +2881,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.6" }, "funding": [ { @@ -2873,38 +2889,26 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2024-03-01T13:39:50+00:00" }, { - "name": "phpunit/php-invoker", - "version": "3.1.1", + "name": "phpunit/php-text-template", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" + "php": ">=5.3.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -2921,47 +2925,41 @@ "role": "lead" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "process" + "template" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2015-06-21T13:50:34+00:00" }, { - "name": "phpunit/php-text-template", - "version": "2.0.4", + "name": "phpunit/php-timer", + "version": "2.1.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a691211e94ff39a34811abd521c31bd5b305b0bb", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -2980,14 +2978,14 @@ "role": "lead" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "template" + "timer" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.4" }, "funding": [ { @@ -2995,32 +2993,33 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2024-03-01T13:42:41+00:00" }, { - "name": "phpunit/php-timer", - "version": "5.0.3", + "name": "phpunit/php-token-stream", + "version": "4.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -3035,18 +3034,17 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", "keywords": [ - "timer" + "tokenizer" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" }, "funding": [ { @@ -3054,54 +3052,53 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "abandoned": true, + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.22", + "version": "8.5.42", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "3a68a70824da546d26ac08ca4fced67341f4158f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3a68a70824da546d26ac08ca4fced67341f4158f", + "reference": "3a68a70824da546d26ac08ca4fced67341f4158f", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", + "doctrine/instantiator": "^1.5.0", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "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", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" + "php": ">=7.2", + "phpunit/php-code-coverage": "^7.0.17", + "phpunit/php-file-iterator": "^2.0.6", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1.4", + "sebastian/comparator": "^3.0.5", + "sebastian/diff": "^3.0.6", + "sebastian/environment": "^4.2.5", + "sebastian/exporter": "^3.1.6", + "sebastian/global-state": "^3.0.5", + "sebastian/object-enumerator": "^3.0.5", + "sebastian/resource-operations": "^2.0.3", + "sebastian/type": "^1.1.5", + "sebastian/version": "^2.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage", + "phpunit/php-invoker": "To allow enforcing time limits" }, "bin": [ "phpunit" @@ -3109,13 +3106,10 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-master": "8.5-dev" } }, "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], "classmap": [ "src/" ] @@ -3141,7 +3135,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/8.5.42" }, "funding": [ { @@ -3153,148 +3147,44 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2024-12-05T13:48:26+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2025-05-02T06:33:00+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -3316,7 +3206,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3" }, "funding": [ { @@ -3324,34 +3214,34 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2024-03-01T13:45:45+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3390,64 +3280,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T12:41:17+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" }, "funding": [ { @@ -3455,33 +3288,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2022-09-14T12:31:48+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3513,7 +3346,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.6" }, "funding": [ { @@ -3521,27 +3354,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-03-02T06:16:36+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "4.2.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "56932f6049a0482853056ffd617c91ffcc754205" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", + "reference": "56932f6049a0482853056ffd617c91ffcc754205", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^7.5" }, "suggest": { "ext-posix": "*" @@ -3549,7 +3382,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3576,7 +3409,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.5" }, "funding": [ { @@ -3584,34 +3417,34 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2024-03-01T13:49:59+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "3.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "php": ">=7.2", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -3646,14 +3479,14 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", + "homepage": "http://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.6" }, "funding": [ { @@ -3661,30 +3494,30 @@ "type": "github" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2024-03-02T06:21:38+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/91c7c47047a971f02de57ed6f040087ef110c5d9", + "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -3692,7 +3525,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3717,7 +3550,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.5" }, "funding": [ { @@ -3725,91 +3558,34 @@ "type": "github" } ], - "time": "2024-03-02T06:35:11+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2024-03-02T06:13:16+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "ac5b293dba925751b808e02923399fb44ff0d541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", + "reference": "ac5b293dba925751b808e02923399fb44ff0d541", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -3831,7 +3607,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5" }, "funding": [ { @@ -3839,32 +3615,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-03-01T13:54:02+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -3886,7 +3662,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3" }, "funding": [ { @@ -3894,32 +3670,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2024-03-01T13:56:04+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c", + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -3946,10 +3722,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2" }, "funding": [ { @@ -3957,32 +3733,29 @@ "type": "github" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2024-03-01T14:07:30+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.4", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/72a7f7674d053d548003b16ff5a106e7e0e06eee", + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -4003,7 +3776,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.3" }, "funding": [ { @@ -4011,32 +3784,32 @@ "type": "github" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2024-03-01T13:59:09+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/18f071c3a29892b037d35e6b20ddf3ea39b42874", + "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.2" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -4059,7 +3832,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "source": "https://github.com/sebastianbergmann/type/tree/1.1.5" }, "funding": [ { @@ -4067,29 +3840,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2024-03-01T14:04:07+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4112,15 +3885,9 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/master" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2016-10-03T07:35:21+00:00" }, { "name": "theseer/tokenizer", 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/js/vendor/simplyedit/simply-edit.js b/solid/js/vendor/simplyedit/simply-edit.js index e140b304..27eb96a9 100644 --- a/solid/js/vendor/simplyedit/simply-edit.js +++ b/solid/js/vendor/simplyedit/simply-edit.js @@ -17,9 +17,6 @@ if (!scriptEl) { scriptEl = document.querySelector("[data-api-key]"); } - if (!scriptEl) { - scriptEl = document.querySelector("[src*='simply-edit.js']"); - } return scriptEl; }; @@ -37,7 +34,7 @@ var scriptURL = document.createElement('a'); scriptURL.href = url; scriptURL.pathname = scriptURL.pathname.replace('simply-edit.js', '').replace(/\/js\/$/, '/'); - if (apiKey !== "" && apiKey !== "muze" && apiKey !== "github") { + if (apiKey !== "") { scriptURL.pathname = scriptURL.pathname + apiKey + "/"; } return scriptURL.href; @@ -103,7 +100,10 @@ var dataFields; if (target.nodeType == document.ELEMENT_NODE && target.getAttribute("data-simply-field")) { dataFields = [target]; - if (target.getAttribute("data-simply-content") === 'fixed') { // special case - if the target field has content fixed, we need to handle its children as well. + if ( + (target.getAttribute("data-simply-content") === 'fixed') || + (target.getAttribute("data-simply-content") === 'attributes') + ) { // special case - if the target field has content fixed or attributes, we need to handle its children as well. var extraFields = target.querySelectorAll("[data-simply-field]"); for (var x=0; x -1); + if (isSub) { + continue; + } + editor.list.init(dataLists[i], listDataItem, useDataBinding); } + if (clone.nodeType == document.ELEMENT_NODE && clone.getAttribute("data-simply-list")) { editor.list.init(clone, listDataItem, useDataBinding); } @@ -825,11 +860,37 @@ list.dataBinding.pauseListeners(list); } + var transformer = list.getAttribute('data-simply-transformer'); + if (transformer) { + if (editor.transformers[transformer] && (typeof editor.transformers[transformer].render === "function")) { + try { + listData = editor.transformers[transformer].render.call(list, listData); + } catch(e) { + console.log("Error thrown in transformer " + transformer); + console.log(e); + } + } else { + console.log("Warning: transformer " + transformer + " is not defined"); + } + } + + if (list.previousValue == JSON.stringify(listData)) { + if (list.dataBinding) { + list.dataBinding.resumeListeners(list); + } + return; // value is the same as the previous time we set it, just keep it; + } + + list.previousValue = JSON.stringify(listData); var previousStyle = list.getAttribute("style"); list.style.height = list.offsetHeight + "px"; // this will prevent the screen from bouncing and messing up the scroll offset. editor.list.clear(list); editor.list.append(list, listData); - list.setAttribute("style", previousStyle); + if (previousStyle) { + list.setAttribute("style", previousStyle); + } else { + list.removeAttribute("style"); + } editor.list.emptyClass(list); if (list.dataBinding) { list.dataBinding.resumeListeners(list); @@ -857,7 +918,7 @@ // Grr... android browser imports the nodes, except the contents of subtemplates. Find them and put them back where they belong. var originalTemplates = template.content.querySelectorAll("template"); - var importedTemplates = clone.querySelectorAll("template"); + var importedTemplates = clone.querySelectorAll("template:not([simply-component])"); for (i=0; i " + newparent); - value._bindings_[subbinding].parentKey = newparent; - if (value[subbinding] && value[subbinding].length) { - for (var i=0; i [data-simply-list-item]"); + //for (i=0; i [data-simply-list-item]"); + for (i=0; i 5) { - console.log("Warning: databinding resolve loop detected!"); + }; + + this.handleEvent = function (event) { + var target = event.currentTarget; + var dataBinding = target.dataBinding; + var elementBinding = target.elementBinding; + + if (typeof dataBinding === 'undefined') { + return; + } + if (dataBinding.paused) { + return; + } + if (target.dataBindingPaused) { + event.stopPropagation(); + return; + } + if (dataBinding.mode === "list") { + if (event.relatedNode && (target != event.relatedNode)) { + return; + } + } + + var i, data, items; + + switch (event.type) { + case "change": + case "databinding:valuechanged": + // Allow the browser to fix what it thinks needs to be fixed (node to be removed, cleaned etc) before setting the new data; + + // these are needed to keep the focus in an element while typing; + elementBinding.pauseListeners(); + dataBinding.set(elementBinding.getter()); + elementBinding.resumeListeners(); + + // these are needed to update after the browser is done doing its thing; window.setTimeout(function() { - binding.resolveCounter = 0; - }, 300); // 300 is a guess; could be any other number. It needs to be long enough so that everyone can settle down before we start resolving again. + elementBinding.pauseListeners(); + dataBinding.set(elementBinding.getter()); + elementBinding.resumeListeners(); + }, 1); // allow the rest of the mutation event to occur; + break; + } + elementBinding.fireEvent("domchanged"); + }; + this.fireEvent = function(event) { + self.dataBinding.fireEvent(self.element, event); + }; + this.fireParent = function(event) { + self.dataBinding.fireEvent(self.element.parentNode, event); + }; + this.isInDocument = function() { + if (document.contains && document.contains(this.element)) { + return true; + } + var parent = element.parentNode; + while (parent) { + if (parent === document) { return true; } - return false; - }; - - var setElements = function() { - if (binding.elementTimer) { - window.clearTimeout(binding.elementTimer); - } - for (var i=0; i -1) { - binding.removeListeners(element); - binding.elements.splice(binding.elements.indexOf(element), 1); - } - }; - - this.cleanupBindings = function() { - if (binding.elements.length < 2) { - return; + shadowValue._bindings_[i] = valueBindings[i]; } - - var inDocument = function(element) { - if (document.contains && document.contains(element)) { - return true; + } + + if (typeof oldValue !== "undefined" && !isEqual(oldValue, shadowValue)) { + binding.config.resolve.call(binding, key, dereference(shadowValue), dereference(oldValue)); + } + //if (typeof shadowValue === "object") { + // shadowValue = dereference(shadowValue); + //} + updateConvertedDataParent(shadowValue); + monitorChildData(shadowValue); + }; + + var updateConvertedDataParent = function(data) { + if ( + binding.config.data._parentBindings_ && + binding.config.data._parentBindings_[binding.key] && + binding.config.data._parentBindings_[binding.key].config.data._simplyListEntryMapping + ) { + var listEntryMapping = binding.config.data._parentBindings_[binding.key].config.data._simplyListEntryMapping; + var convertedParent = binding.config.data._parentBindings_[binding.key].config.data._simplyConvertedParent; + var arrayPaths = binding.config.data._parentBindings_[binding.key].config.data[listEntryMapping]._parentBindings_[binding.key].parentKey.split("/"); + var arrayIndex = arrayPaths.pop(); + arrayIndex = arrayPaths.pop(); + binding.config.data._parentBindings_[binding.key].config.data[binding.key] = data; + var parentData = convertedParent._parentBindings_[arrayIndex].config.data; + var parentKey = arrayPaths.pop(); + parentData[parentKey][arrayIndex][binding.key] = data; + } + }; + + var monitorChildData = function(data) { + // Watch for changes in our child data, because these also need to register as changes in the databound data/elements; + // This allows the use of simple data structures (1 key deep) as databound values and still resolve changes on a specific entry; + var parentData = data; + + if (typeof data === "object") { + var monitor = function(data, key) { + if (!data.hasOwnProperty("_parentBindings_")) { + var bindings = {}; + + Object.defineProperty(data, "_parentBindings_", { + get : function() { + return bindings; + }, + set : function(value) { + bindings[key] = binding; + } + }); + Object.defineProperty(data, "_parentData_", { + get : function() { + return parentData; + } + }); } - var parent = element.parentNode; - while (parent) { - if (parent === document) { - return true; + data._parentBindings_[key] = binding; + + var myvalue = data[key]; + + var renumber = function(key, value, parentBinding) { + var oldparent, newparent; + if (value && value._bindings_) { + for (var subbinding in value._bindings_) { + oldparent = value._bindings_[subbinding].parentKey; + newparent = parentBinding.parentKey + parentBinding.key + "/" + key + "/"; + // console.log(oldparent + " => " + newparent); + value._bindings_[subbinding].parentKey = newparent; + if (value[subbinding] && value[subbinding].length && (typeof value[subbinding] !== "string")) { + for (var i=0; i 5) { + console.log("Warning: databinding resolve loop detected!"); + window.setTimeout(function() { + binding.resolveCounter = 0; + }, 300); // 300 is a guess; could be any other number. It needs to be long enough so that everyone can settle down before we start resolving again. + return true; } + return false; }; - - dataBinding.prototype.addListeners = function(element) { - if (element.dataBinding) { - element.dataBinding.removeListeners(element); + + var setElements = function() { + if (binding.elementTimer) { + window.clearTimeout(binding.elementTimer); } - if (typeof element.mutationObserver === "undefined") { - if (typeof MutationObserver === "function") { - element.mutationObserver = new MutationObserver(this.handleMutation); + for (var i=0; i [data-simply-list-item]"); - for (i=0; i [data-simply-list-item]"); - for (i=0; i -1) { + element.removeListeners(); + binding.elements.splice(binding.elements.indexOf(element), 1); } - self.fireEvent(target, "domchanged"); }; - // Housekeeping, remove references to deleted nodes - document.addEventListener("DOMNodeRemoved", function(evt) { - var target = evt.target; - if (target.nodeType != document.ELEMENT_NODE) { // We don't care about removed text nodes; + this.cleanupBindings = function() { + if (binding.elements.length < 2) { return; } - if (!target.dataBinding) { // nor any element that doesn't have a databinding; - return; + + binding.elements.forEach(function(element) { + if (!element.isInDocument()) { + element.markedForRemoval = true; + } else { + element.markedForRemoval = false; + } + }); + + if (binding.cleanupTimer) { + clearTimeout(binding.cleanupTimer); } - window.setTimeout(function() { // chrome sometimes 'helpfully' removes the element and then inserts it back, probably as a rendering optimalization. We're fine cleaning up in a bit, if still needed. - if (!target.parentNode && target.dataBinding) { - target.dataBinding.unbind(target); - delete target.dataBinding; + + binding.cleanupTimer = window.setTimeout(function() { + binding.elements.filter(function(element) { + if (element.markedForRemoval && !element.isInDocument()) { + element.dataBinding.unbind(element); + return false; + } + element.markedForRemoval = false; + return true; + }); + }, 1000); // If after 1 second the element is still not in the dom, remove the binding; + }; + + initBindings(data, key); + // Call the custom init function, if it is there; + if (typeof binding.config.init === "function") { + binding.config.init.call(binding); + } + + if (binding.mode == "list") { + document.addEventListener("databind:resolved", function() { + if (!binding.skipOldValueUpdate) { + oldValue = dereference(binding.get()); } - }, 1000); - }); - - // polyfill to add :scope selector for IE - (function() { - if (!HTMLElement.prototype.querySelectorAll) { - throw new Error('rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll'); - } - - // A temporary element to query against for elements not currently in the DOM - // We'll also use this element to test for :scope support - var container = document.createElement('div'); - - // Check if the browser supports :scope - try { - // Browser supports :scope, do nothing - container.querySelectorAll(':scope *'); - } - catch (e) { - // Match usage of scope - var scopeRE = /\s*:scope/gi; - - // Overrides - function overrideNodeMethod(prototype, methodName) { - // Store the old method for use later - var oldMethod = prototype[methodName]; - - // Override the method - prototype[methodName] = function(query) { - var nodeList, - gaveId = false, - gaveContainer = false; - - if (query.match(scopeRE)) { - if (!this.parentNode) { - // Add to temporary container - container.appendChild(this); - gaveContainer = true; - } - - parentNode = this.parentNode; - - if (!this.id) { - // Give temporary ID - this.id = 'rootedQuerySelector_id_'+(new Date()).getTime(); - gaveId = true; - } - - // Remove :scope - query = query.replace(scopeRE, '#' + this.id + " "); - - // Find elements against parent node - // nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query); - nodeList = parentNode[methodName](query); - // Reset the ID - if (gaveId) { - this.id = ''; - } - - // Remove from temporary container - if (gaveContainer) { - container.removeChild(this); - } - - return nodeList; - } - else { - // No immediate child selector used - return oldMethod.call(this, query); - } - }; - } - - // Browser doesn't support :scope, add polyfill - overrideNodeMethod(HTMLElement.prototype, 'querySelector'); - overrideNodeMethod(HTMLElement.prototype, 'querySelectorAll'); - } - }()); - - editor.init({ - endpoint : document.querySelector("[data-simply-endpoint]") ? document.querySelector("[data-simply-endpoint]").getAttribute("data-simply-endpoint") : null, - toolbars : defaultToolbars, - profile : 'live' + }); + } +}; + +dataBinding.prototype.resumeListeners = function(element) { + element.dataBindingPaused--; + if (element.dataBindingPaused < 0) { + console.log("Warning: resume called of non-paused databinding"); + element.dataBindingPaused = 0; + } + if (element.dataBindingPaused === 0) { + if (element.mutationObserver) { + element.mutationObserver.observe(element, element.mutationObserverConfig); + element.mutationObserver.status = "observing"; + } else { + console.log("Warning: no mutation observer found"); + } + } +}; +dataBinding.prototype.pauseListeners = function(element) { + element.dataBindingPaused++; + if (element.mutationObserver) { + element.mutationObserver.status = "disconnected"; + element.mutationObserver.disconnect(); + } +}; + +// Housekeeping, remove references to deleted nodes +var removalObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type == "childList") { + mutation.removedNodes.forEach(function(target) { + if (target.nodeType != document.ELEMENT_NODE) { // We don't care about removed text nodes; + return; + } + if (!target.dataBinding) { // nor any element that doesn't have a databinding; + return; + } + window.setTimeout(function() { // chrome sometimes 'helpfully' removes the element and then inserts it back, probably as a rendering optimalization. We're fine cleaning up in a bit, if still needed. + if (!target.parentNode && target.dataBinding && target.elementBinding) { + target.dataBinding.unbind(target.elementBinding); + // if (target.dataBinding.mode == "field") { + // target.dataBinding.set(); + // } + delete target.dataBinding; + } + }, 400); + }); + } }); +}); + +removalObserver.observe(document.body, { + "childList" : true, + "subtree" : true +}); + +// polyfill to add :scope selector for IE +(function() { + if (!HTMLElement.prototype.querySelectorAll) { + throw new Error('rootedQuerySelectorAll: This polyfill can only be used with browsers that support querySelectorAll'); + } + + // A temporary element to query against for elements not currently in the DOM + // We'll also use this element to test for :scope support + var container = document.createElement('div'); + + // Check if the browser supports :scope + try { + // Browser supports :scope, do nothing + container.querySelectorAll(':scope *'); + } + catch (e) { + // Match usage of scope + var scopeRE = /\s*:scope/gi; + + // Overrides + function overrideNodeMethod(prototype, methodName) { + // Store the old method for use later + var oldMethod = prototype[methodName]; + + // Override the method + prototype[methodName] = function(query) { + var nodeList, + gaveId = false, + gaveContainer = false; + + if (query.match(scopeRE)) { + if (!this.parentNode) { + // Add to temporary container + container.appendChild(this); + gaveContainer = true; + } + + parentNode = this.parentNode; + + if (!this.id) { + // Give temporary ID + this.id = 'rootedQuerySelector_id_'+(new Date()).getTime(); + gaveId = true; + } + + // Remove :scope + query = query.replace(scopeRE, '#' + this.id + " "); + + // Find elements against parent node + // nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query); + nodeList = parentNode[methodName](query); + // Reset the ID + if (gaveId) { + this.id = ''; + } + + // Remove from temporary container + if (gaveContainer) { + container.removeChild(this); + } + + return nodeList; + } + else { + // No immediate child selector used + return oldMethod.call(this, query); + } + }; + } + + // Browser doesn't support :scope, add polyfill + overrideNodeMethod(HTMLElement.prototype, 'querySelector'); + overrideNodeMethod(HTMLElement.prototype, 'querySelectorAll'); + } }()); diff --git a/solid/js/vendor/simplyedit/simply.everything.js b/solid/js/vendor/simplyedit/simply.everything.js index ed6561a5..57e66774 100644 --- a/solid/js/vendor/simplyedit/simply.everything.js +++ b/solid/js/vendor/simplyedit/simply.everything.js @@ -1,227 +1,364 @@ -this.simply = (function(simply, global) { - - simply.view = function(app, view) { - - app.view = view || {}; - - var load = function() { - var data = app.view; - var path = editor.data.getDataPath(app.container); - app.view = editor.currentData[path]; - Object.keys(data).forEach(function(key) { - app.view[key] = data[key]; - }); - }; - - if (global.editor && editor.currentData) { - load(); - } else { - document.addEventListener('simply-content-loaded', function() { - load(); - }); - } - - return app.view; - }; +/** + * simply.observe + * This component lets you observe changes in a json compatible data structure + * It doesn't support linking the same object multiple times + * It doesn't register deletion of properties using the delete keyword, assign + * null to the property instead. + * It doesn't register addition of new properties. + * It doesn't register directly assigning new entries in an array on a previously + * non-existant index. + * + * usage: + * + * (function) simply.observe( (object) model, (string) path, (function) callback) + * + * var model = { foo: { bar: 'baz' } }; + * var removeObserver = simply.observe(model, 'foo.bar', function(value, sourcePath) { + * console.log(sourcePath+': '+value); + * }; + * + * The function returns a function that removes the observer when called. + * + * The component can observe in place changes in arrays, either by changing + * an item in a specific index, by calling methods on the array that change + * the array in place or by reassigning the array with a new value. + * + * The sourcePath contains the exact entry that was changed, the value is the + * value for the path passed to simply.observe. + * If an array method was called that changes the array in place, the sourcePath + * also contains that method and its arguments JSON serialized. + * + * sourcePath parts are always seperated with '.', even for array indexes. + * so if foo = [ 'bar' ], the path to 'bar' would be 'foo.0' + */ - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { + /* + FIXME: child properties added after initial observe() call aren't added to the + childListeners. onMissingChildren can't then find them. + TODO: onMissingChildren must loop through all fields to get only the direct child +properties for a given parent, keep seperate index for this? + */ - var routeInfo = []; +(function (global) { + 'use strict'; - function parseRoutes(routes) { - var paths = Object.keys(routes); - var matchParams = /:(\w+|\*)/g; - var matches, params, path; - for (var i=0; ipath.length; + }); + if (!allChildren.length) { + return; + } + var object = getByPath(model, path); + var keysSeen = {}; + allChildren.forEach(function(childPath) { + var key = head(childPath.substr(path.length+1)); + if (typeof object[key] == 'undefined') { + if (!keysSeen[key]) { + callback(object, key, path+'.'+key); + keysSeen[key] = true; + } + } else { + onMissingChildren(model, path+'.'+key, callback); + } + }); + } + + function addChangeListener(model, path, callback) { + if (!changeListeners.has(model)) { + changeListeners.set(model, {}); + } + if (!changeListeners.get(model)[path]) { + changeListeners.get(model)[path] = []; + } + changeListeners.get(model)[path].push(callback); + + if (!parentListeners.has(model)) { + parentListeners.set(model, {}); + } + var parentPath = parent(path); + onParents(model, parentPath, function(parentOb, key, currPath) { + if (!parentListeners.get(model)[currPath]) { + parentListeners.get(model)[currPath] = []; + } + parentListeners.get(model)[currPath].push(path); + }); + + if (!childListeners.has(model)) { + childListeners.set(model, {}); + } + onChildren(model, path, function(childOb, key, currPath) { + if (!childListeners.get(model)[currPath]) { + childListeners.get(model)[currPath] = []; + } + childListeners.get(model)[currPath].push(path); + }); + } + + function removeChangeListener(model, path, callback) { + if (!changeListeners.has(model)) { + return; + } + if (changeListeners.get(model)[path]) { + changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) { + return f != callback; + }); + } + } + + function pauseObservers() { + observersPaused++; + } + + function resumeObservers() { + observersPaused--; + } + + function attach(model, path, options) { + + var attachArray = function(object, path) { + var desc = Object.getOwnPropertyDescriptor(object, 'push'); + if (!desc || desc.configurable) { + for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) { + (function(f) { + try { + Object.defineProperty(object, f, { + value: function() { + pauseObservers(); + var result = Array.prototype[f].apply(this, arguments); + attach(model, path); + var args = [].slice.call(arguments).map(function(arg) { + return JSON.stringify(arg); + }); + resumeObservers(); + signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')'); + return result; + }, + readable: false, + enumerable: false, + configurable: false + }); + } catch(e) { + console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e); + } + }(f)); + } + for (var i=0, l=object.length; ipath.length; - }); - if (!allChildren.length) { - return; - } - var object = getByPath(model, path); - var keysSeen = {}; - allChildren.forEach(function(childPath) { - var key = head(childPath.substr(path.length+1)); - if (typeof object[key] == 'undefined') { - if (!keysSeen[key]) { - callback(object, key, path+'.'+key); - keysSeen[key] = true; - } - } else { - onMissingChildren(model, path+'.'+key, callback); + }, + init: function(params) { + if (params.root) { + options.root = params.root; } - }); - } - - function addChangeListener(model, path, callback) { - if (!changeListeners.has(model)) { - changeListeners.set(model, {}); - } - if (!changeListeners.get(model)[path]) { - changeListeners.get(model)[path] = []; } - changeListeners.get(model)[path].push(callback); - - if (!parentListeners.has(model)) { - parentListeners.set(model, {}); - } - var parentPath = parent(path); - onParents(model, parentPath, function(parentOb, key, currPath) { - if (!parentListeners.get(model)[currPath]) { - parentListeners.get(model)[currPath] = []; - } - parentListeners.get(model)[currPath].push(path); - }); + }; - if (!childListeners.has(model)) { - childListeners.set(model, {}); + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = route; + } else { + if (!global.simply) { + global.simply = {}; } - onChildren(model, path, function(childOb, key, currPath) { - if (!childListeners.get(model)[currPath]) { - childListeners.get(model)[currPath] = []; - } - childListeners.get(model)[currPath].push(path); - }); + global.simply.route = route; } +})(this); +(function(global) { + 'use strict'; - function removeChangeListener(model, path, callback) { - if (!changeListeners.has(model)) { - return; - } - if (changeListeners.get(model)[path]) { - changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) { - return f != callback; - }); - } - } + var listeners = {}; - function pauseObservers() { - observersPaused++; - } - - function resumeObservers() { - observersPaused--; - } - - function attach(model, path, options) { - - var attachArray = function(object, path) { - var desc = Object.getOwnPropertyDescriptor(object, 'push'); - if (!desc || desc.configurable) { - for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) { - (function(f) { - try { - Object.defineProperty(object, f, { - value: function() { - pauseObservers(); - var result = Array.prototype[f].apply(this, arguments); - attach(model, path); - var args = [].slice.call(arguments).map(function(arg) { - return JSON.stringify(arg); - }); - resumeObservers(); - signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')'); - return result; - }, - readable: false, - enumerable: false, - configurable: false - }); - } catch(e) { - console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e); - } - }(f)); - } - for (var i=0, l=object.length; i=0) { + knownCollections[name].splice(index, 1); + } + } + }, + update: function(element, value) { + element.value = value; + element.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: true + })); + } }; - var handleChanges = throttle(function() { - runWhenIdle(function() { - var links = document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]'); - if (links.length) { - includeLinks(links); + function findCollection(el) { + while (el && !el.dataset.simplyCollection) { + el = el.parentElement; + } + return el; + } + + global.addEventListener('change', function(evt) { + var root = null; + var name = ''; + if (evt.target.dataset.simplyElement) { + root = findCollection(evt.target); + if (root && root.dataset) { + name = root.dataset.simplyCollection; } - }); - }); - - var observe = function() { - observer = new MutationObserver(handleChanges); - observer.observe(document, { - subtree: true, - childList: true, - }); - }; - - observe(); + } + if (name && knownCollections[name]) { + var inputs = root.querySelectorAll('[data-simply-element]'); + var elements = [].reduce.call(inputs, function(elements, input) { + elements[input.dataset.simplyElement] = input; + return elements; + }, {}); + for (var i=knownCollections[name].length-1; i>=0; i--) { + var result = knownCollections[name][i].call(evt.target.form, elements); + if (result === false) { + break; + } + } + } + }, true); - return simply; + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = collect; + } else { + if (!global.simply) { + global.simply = {}; + } + global.simply.collect = collect; + } -})(this.simply || {}, this); -this.simply = (function(simply, global) { +})(this); +(function(global) { + 'use strict'; var defaultCommands = { 'simply-hide': function(el, value) { @@ -957,6 +951,16 @@ this.simply = (function(simply, global) { { match: 'input,select,textarea', get: function(el) { + if (el.tagName==='SELECT' && el.multiple) { + var values = [], opt; + for (var i=0,l=el.options.length;i { + if (e.isComposing || e.keyCode === 229) { + return; } - if (knownCollections[name].indexOf(callback) == -1) { - knownCollections[name].push(callback); + if (e.defaultPrevented) { + return; } - }, - removeListener: function(name, callback) { - if (knownCollections[name]) { - var index = knownCollections[name].indexOf(callback); - if (index>=0) { - knownCollections[name].splice(index, 1); - } + if (!e.target) { + return; } - }, - update: function(element, value) { - element.value = value; - element.dispatchEvent(new Event('change', { - bubbles: true, - cancelable: true - })); - } - }; - function findCollection(el) { - while (el && !el.dataset.simplyCollection) { - el = el.parentElement; + let selectedKeyboard = 'default'; + if (e.target.closest('[data-simply-keyboard]')) { + selectedKeyboard = e.target.closest('[data-simply-keyboard]').dataset.simplyKeyboard; + } + let key = ''; + if (e.ctrlKey && e.keyCode!=17) { + key+='Control+'; + } + if (e.metaKey && e.keyCode!=224) { + key+='Meta+'; + } + if (e.altKey && e.keyCode!=18) { + key+='Alt+'; + } + if (e.shiftKey && e.keyCode!=16) { + key+='Shift+'; + } + key+=e.key; + + if (keys[selectedKeyboard] && keys[selectedKeyboard][key]) { + let keyboard = keys[selectedKeyboard] + keyboard.app = app; + keyboard[key].call(keyboard,e); + } + }); + + return keys; + } + + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = keyboard; + } else { + if (!global.simply) { + global.simply = {}; } - return el; + global.simply.keyboard = keyboard; } - - document.addEventListener('change', function(evt) { - var root = null; - var name = ''; - if (evt.target.dataset.simplyElement) { - root = findCollection(evt.target); - if (root && root.dataset) { - name = root.dataset.simplyCollection; +})(this); +(function(global) { + 'use strict'; + + var defaultActions = { + 'simply-hide': function(el) { + el.classList.remove('simply-visible'); + return Promise.resolve(); + }, + 'simply-show': function(el) { + el.classList.add('simply-visible'); + return Promise.resolve(); + }, + 'simply-select': function(el,group,target,targetGroup) { + if (group) { + this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']')); } - } - if (name && knownCollections[name]) { - var inputs = root.querySelectorAll('[data-simply-element]'); - var elements = [].reduce.call(inputs, function(elements, input) { - elements[input.dataset.simplyElement] = input; - return elements; - }, {}); - for (var i=knownCollections[name].length-1; i>=0; i--) { - var result = knownCollections[name][i].call(evt.target.form, elements); - if (result === false) { + el.classList.add('simply-selected'); + if (target) { + this.call('simply-select',target,targetGroup); + } + return Promise.resolve(); + }, + 'simply-toggle-select': function(el,group,target,targetGroup) { + if (!el.classList.contains('simply-selected')) { + this.call('simply-select',el,group,target,targetGroup); + } else { + this.call('simply-deselect',el,target); + } + return Promise.resolve(); + }, + 'simply-toggle-class': function(el,className,target) { + if (!target) { + target = el; + } + return Promise.resolve(target.classList.toggle(className)); + }, + 'simply-deselect': function(el,target) { + if ( typeof el.length=='number' && typeof el.item=='function') { + el = Array.prototype.slice.call(el); + } + if ( Array.isArray(el) ) { + for (var i=0,l=el.length; i1 && curr) { - var key = parts.shift(); - if (typeof curr[key] == 'undefined' || curr[key]==null) { - curr[key] = {}; - } - curr = curr[key]; +})(this); +(function(global) { + 'use strict'; + + var resize = function(app, config) { + if (!config) { + config = {}; + } + if (!config.sizes) { + config.sizes = { + 'simply-tiny' : 0, + 'simply-xsmall' : 480, + 'simply-small' : 768, + 'simply-medium' : 992, + 'simply-large' : 1200 + }; } - curr[parts.shift()] = value; - } - function setValue(el, value, binding) { - if (el!=focusedElement) { - var fieldType = getFieldType(binding.fieldTypes, el); - if (fieldType) { - fieldType.set.call(el, (typeof value != 'undefined' ? value : ''), binding); - el.dispatchEvent(new Event('simply.bind.resolved', { - bubbles: true, - cancelable: false - })); + var lastSize = 0; + function resizeSniffer() { + var size = app.container.getBoundingClientRect().width; + if ( lastSize==size ) { + return; + } + lastSize = size; + var sizes = Object.keys(config.sizes); + var match = sizes.pop(); + while (match) { + if ( size=0;i--) { - if (el.matches(setters[i])) { - return binding.fieldTypes[setters[i]].get.call(el); - } + if ( global.attachEvent ) { + app.container.attachEvent('onresize', resizeSniffer); + } else { + global.setInterval(resizeSniffer, 200); } - } - function getFieldType(fieldTypes, el) { - var setters = Object.keys(fieldTypes); - for(var i=setters.length-1;i>=0;i--) { - if (el.matches(setters[i])) { - return fieldTypes[setters[i]]; - } + if ( simply.toolbar ) { + var toolbars = app.container.querySelectorAll('.simply-toolbar'); + [].forEach.call(toolbars, function(toolbar) { + simply.toolbar.init(toolbar); + if (simply.toolbar.scroll) { + simply.toolbar.scroll(toolbar); + } + }); } - return null; - } - function getPath(el, attribute) { - var attributes = attribute.split(','); - for (var attr of attributes) { - if (el.hasAttribute(attr)) { - return el.getAttribute(attr); - } + return resizeSniffer; + }; + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = resize; + } else { + if (!global.simply) { + global.simply = {}; } - return null; + global.simply.resize = resize; } +})(this);(function (global) { + 'use strict'; - function throttle( callbackFunction, intervalTime ) { + var throttle = function( callbackFunction, intervalTime ) { var eventId = 0; return function() { var myArguments = arguments; @@ -1236,7 +1373,7 @@ this.simply = (function(simply, global) { }, intervalTime ); } }; - } + }; var runWhenIdle = (function() { if (global.requestIdleCallback) { @@ -1247,165 +1384,649 @@ this.simply = (function(simply, global) { return global.requestAnimationFrame; })(); - function Binding(config, force) { - this.config = config; - if (!this.config) { - this.config = {}; + var rebaseHref = function(relative, base) { + let url = new URL(relative, base) + if (include.cacheBuster) { + url.searchParams.set('cb',include.cacheBuster) + } + return url.href + }; + + var observer, loaded = {}; + var head = global.document.querySelector('head'); + var currentScript = global.document.currentScript; + if (!currentScript) { + var getScriptURL = (function() { + var scripts = document.getElementsByTagName('script'); + var index = scripts.length - 1; + var myScript = scripts[index]; + return function() { return myScript.src; }; + })(); + var currentScriptURL = getScriptURL(); + } else { + var currentScriptURL = currentScript.src; + } + + var waitForPreviousScripts = function() { + // because of the async=false attribute, this script will run after + // the previous scripts have been loaded and run + // simply.include.next.js only fires the simply-next-script event + // that triggers the Promise.resolve method + return new Promise(function(resolve) { + var next = global.document.createElement('script'); + next.src = rebaseHref('simply.include.next.js', currentScriptURL); + next.async = false; + global.document.addEventListener('simply-include-next', function() { + head.removeChild(next); + resolve(); + }, { once: true, passive: true}); + head.appendChild(next); + }); + }; + + var scriptLocations = []; + + var include = { + cacheBuster: null, + scripts: function(scripts, base) { + var arr = []; + for(var i = scripts.length; i--; arr.unshift(scripts[i])); + var importScript = function() { + var script = arr.shift(); + if (!script) { + return; + } + var attrs = [].map.call(script.attributes, function(attr) { + return attr.name; + }); + var clone = global.document.createElement('script'); + attrs.forEach(function(attr) { + clone.setAttribute(attr, script.getAttribute(attr)); + }); + clone.removeAttribute('data-simply-location'); + if (!clone.src) { + // this is an inline script, so copy the content and wait for previous scripts to run + clone.innerHTML = script.innerHTML; + waitForPreviousScripts() + .then(function() { + var node = scriptLocations[script.dataset.simplyLocation]; + node.parentNode.insertBefore(clone, node); + node.parentNode.removeChild(node); + importScript(); + }); + } else { + clone.src = rebaseHref(clone.src, base); + if (!clone.hasAttribute('async') && !clone.hasAttribute('defer')) { + clone.async = false; //important! do not use clone.setAttribute('async', false) - it has no effect + } + var node = scriptLocations[script.dataset.simplyLocation]; + node.parentNode.insertBefore(clone, node); + node.parentNode.removeChild(node); + loaded[clone.src]=true; + importScript(); + } + }; + if (arr.length) { + importScript(); + } + }, + html: function(html, link) { + var fragment = global.document.createRange().createContextualFragment(html); + var stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style'); + // add all stylesheets to head + [].forEach.call(stylesheets, function(stylesheet) { + if (stylesheet.href) { + stylesheet.href = rebaseHref(stylesheet.href, link.href); + } + head.appendChild(stylesheet); + }); + // remove the scripts from the fragment, as they will not run in the + // order in which they are defined + var scriptsFragment = global.document.createDocumentFragment(); + // FIXME: this loses the original position of the script + // should add a placeholder so we can reinsert the clone + var scripts = fragment.querySelectorAll('script'); + [].forEach.call(scripts, function(script) { + var placeholder = global.document.createComment(script.src || 'inline script'); + script.parentNode.insertBefore(placeholder, script); + script.dataset.simplyLocation = scriptLocations.length; + scriptLocations.push(placeholder); + scriptsFragment.appendChild(script); + }); + // add the remainder before the include link + link.parentNode.insertBefore(fragment, link ? link : null); + global.setTimeout(function() { + if (global.editor && global.editor.data && fragment.querySelector('[data-simply-field],[data-simply-list]')) { + //TODO: remove this dependency and let simply.bind listen for dom node insertions (and simply-edit.js use simply.bind) + global.editor.data.apply(global.editor.currentData, global.document); + } + simply.include.scripts(scriptsFragment.childNodes, link ? link.href : global.location.href ); + }, 10); } - if (!this.config.model) { - this.config.model = {}; + }; + + var included = {}; + var includeLinks = function(links) { + // mark them as in progress, so handleChanges doesn't find them again + var remainingLinks = [].reduce.call(links, function(remainder, link) { + if (link.rel=='simply-include-once' && included[link.href]) { + link.parentNode.removeChild(link); + } else { + included[link.href]=true; + link.rel = 'simply-include-loading'; + remainder.push(link); + } + return remainder; + }, []); + [].forEach.call(remainingLinks, function(link) { + if (!link.href) { + return; + } + // fetch the html + fetch(link.href) + .then(function(response) { + if (response.ok) { + console.log('simply-include: loaded '+link.href); + return response.text(); + } else { + console.log('simply-include: failed to load '+link.href); + } + }) + .then(function(html) { + // if succesfull import the html + simply.include.html(html, link); + // remove the include link + link.parentNode.removeChild(link); + }); + }); + }; + + var handleChanges = throttle(function() { + runWhenIdle(function() { + var links = global.document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]'); + if (links.length) { + includeLinks(links); + } + }); + }); + + var observe = function() { + observer = new MutationObserver(handleChanges); + observer.observe(global.document, { + subtree: true, + childList: true, + }); + }; + + observe(); + handleChanges(); // check if there are include links in the dom already + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = include; + } else { + if (!global.simply) { + global.simply = {}; } - if (!this.config.attribute) { - this.config.attribute = 'data-simply-bind'; + global.simply.include = include; + } + + +})(this); +(function(global) { + 'use strict'; + var view = function(app, view) { + + app.view = view || {}; + + var load = function() { + var data = app.view; + var path = global.editor.data.getDataPath(app.container); + app.view = global.editor.currentData[path]; + Object.keys(data).forEach(function(key) { + app.view[key] = data[key]; + }); + }; + + if (global.editor && global.editor.currentData) { + load(); + } else { + global.document.addEventListener('simply-content-loaded', function() { + load(); + }); } - if (!this.config.selector) { - this.config.selector = '[data-simply-bind]'; + + return app.view; + }; + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = view; + } else { + if (!global.simply) { + global.simply = {}; } - if (!this.config.container) { - this.config.container = document; + global.simply.view = view; + } +})(this); +(function(global) { + 'use strict'; + + function etag() { + let d = ''; + while (d.length < 32) d += Math.random().toString(16).substr(2); + const vr = ((parseInt(d.substr(16, 1), 16) & 0x3) | 0x8).toString(16); + return `${d.substr(0, 8)}-${d.substr(8, 4)}-4${d.substr(13, 3)}-${vr}${d.substr(17, 3)}-${d.substr(20, 12)}`; + } + + function ViewModel(name, data, options) { + this.name = name; + this.data = data || []; + this.data.etag = etag(); + this.view = { + options: {}, + data: [] //Array.from(this.data).slice() + }; + this.options = options || {}; + this.plugins = { + start: [], + select: [], + order: [], + render: [], + finish: [] + }; + } + + ViewModel.prototype.update = function(params) { + if (!params) { + params = {}; } - if (typeof this.config.twoway == 'undefined') { - this.config.twoway = true; + if (params.data) { + // this.data is a reference to the data passed, so that any changes in it will get applied + // to the original + this.data = params.data; + this.data.etag = etag() } - this.fieldTypes = { - '*': { - set: function(value) { - this.innerHTML = value; - }, - get: function() { - return this.innerHTML; + // the view is a shallow copy of the array, so that changes in sort order and filtering + // won't get applied to the original, but databindings on its children will still work + this.view.data = Array.from(this.data).slice(); + this.view.data.etag = this.data.etag; + let data = this.view.data; + let plugins = this.plugins.start.concat(this.plugins.select, this.plugins.order, this.plugins.render, this.plugins.finish); + plugins.forEach(plugin => { + data = plugin.call(this, params, data); + if (!data) { + data = this.view.data; + } + this.view.data = data + }); + + if (global.editor) { + global.editor.addDataSource(this.name,{ + load: function(el, callback) { + callback(self.view.data); } + }); + updateDataSource(this.name); + } + }; + + ViewModel.prototype.addPlugin = function(pipe, plugin) { + if (typeof this.plugins[pipe] == 'undefined') { + throw new Error('Unknown pipeline '+pipe); + } + this.plugins[pipe].push(plugin); + }; + + ViewModel.prototype.removePlugin = function(pipe, plugin) { + if (typeof this.plugins[pipe] == 'undefined') { + throw new Error('Unknown pipeline '+pipe); + } + this.plugins[pipe] = this.plugins[pipe].filter(function(p) { + return p != plugin; + }); + }; + + var updateDataSource = function(name) { + global.document.querySelectorAll('[data-simply-data="'+name+'"]').forEach(function(list) { + global.editor.list.applyDataSource(list, name); + }); + }; + + var createSort = function(options) { + var defaultOptions = { + name: 'sort', + getSort: function(params) { + return Array.prototype.sort; } }; - if (this.config.fieldTypes) { - Object.assign(this.fieldTypes, this.config.fieldTypes); - } - this.attach(this.config.container.querySelectorAll(this.config.selector), this.config.model, force); - if (this.config.twoway) { - var self = this; - var observer = new MutationObserver( - throttle(function() { - runWhenIdle(function() { - self.attach(self.config.container.querySelectorAll(self.config.selector), self.config.model); - }); - }) - ); - observer.observe(this.config.container, { - subtree: true, - childList: true - }); + options = Object.assign(defaultOptions, options || {}); + + return function(params) { + this.options[options.name] = options; + if (params[options.name]) { + options = Object.assign(options, params[options.name]); + } + this.view.data.sort(options.getSort.call(this, options)); + }; + }; + + var createPaging = function(options) { + var defaultOptions = { + name: 'paging', + page: 1, + pageSize: 100, + max: 1, + prev: 0, + next: 0 + }; + options = Object.assign(defaultOptions, options || {}); + + return function(params) { + this.options[options.name] = options; + if (this.view.data) { + options.max = Math.max(1, Math.ceil(Array.from(this.view.data).length / options.pageSize)); + } else { + options.max = 1; + } + if (this.view.changed) { + options.page = 1; // reset to page 1 when something in the view data has changed + } + if (params[options.name]) { + options = Object.assign(options, params[options.name]); + } + options.page = Math.max(1, Math.min(options.max, options.page)); // clamp page nr + options.prev = options.page - 1; // calculate previous page, 0 is allowed + if (options.page {}; + cache.$options = Object.assign({}, options); + return new Proxy( cache, getApiHandler(cache.$options) ); + }, - var attachElement = function(jsonPath) { - el.dataset.simplyBound = true; - initElement(el); - setValue(el, getByPath(model, jsonPath), self); - simply.observe(model, jsonPath, function(value) { - if (el != focusedElement) { - setValue(el, value, self); + /** + * Fetches the options.baseURL using the fetch api and returns a promise + * Extra options in addition to those of global.fetch(): + * - user (and password): if set, a basic authentication header will be added + * - paramsFormat: either 'formData', 'json' or 'search' + * By default params, if set, will be added to the baseURL as searchParams + * @param method one of the http verbs, e.g. get, post, etc. + * @param options the options for fetch(), with some additions + * @param params the parameters to send with the request, as javascript/json data + * @return Promise + */ + fetch: function(method, params, options) { + if (!options.url) { + if (!options.baseURL) { + throw new Error('No url or baseURL in options object'); } - }); - }; - - var addMutationObserver = function(jsonPath) { - if (el.dataset.simplyList) { - return; - } - var update = throttle(function() { - runWhenIdle(function() { - var v = getValue(el, self); - var s = getByPath(model, jsonPath); - if (v != s) { - focusedElement = el; - setByPath(model, jsonPath, v); - focusedElement = null; - } - }); - }, 250); - var observer = new MutationObserver(function() { - if (observersPaused) { - return; + while (options.baseURL[options.baseURL.length-1]=='/') { + options.baseURL = options.baseURL.substr(0, options.baseURL.length-1); } - update(); - }); - observer.observe(el, { - characterData: true, - subtree: true, - childList: true, - attributes: true - }); - if (!observers.has(el)) { - observers.set(el, []); + var url = new URL(options.baseURL+options.path); + } else { + var url = options.url; } - observers.get(el).push(observer); - return observer; - }; - - /** - * Runs the init() method of the fieldType, if it is defined. - **/ - var initElement = function(el) { - if (initialized.has(el)) { - return; + var fetchOptions = Object.assign({}, options); + if (!fetchOptions.headers) { + fetchOptions.headers = {}; } - initialized.set(el, true); - var selectors = Object.keys(self.fieldTypes); - for (var i=selectors.length-1; i>=0; i--) { - if (self.fieldTypes[selectors[i]].init && el.matches(selectors[i])) { - self.fieldTypes[selectors[i]].init.call(el, self); - return; + if (params) { + if (method=='GET') { + var paramsFormat = 'search'; + } else { + var paramsFormat = options.paramsFormat; + } + switch(paramsFormat) { + case 'formData': + var formData = new FormData(); + for (const name in params) { + formData.append(name, params[name]); + } + if (!fetchOptions.headers['Content-Type']) { + fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + break; + case 'json': + var formData = JSON.stringify(params); + if (!fetchOptions.headers['Content-Type']) { + fetchOptions.headers['Content-Type'] = 'application/json'; + } + break; + case 'search': + var searchParams = url.searchParams; //new URLSearchParams(url.search.slice(1)); + for (const name in params) { + searchParams.set(name, params[name]); + } + url.search = searchParams.toString(); + break; + default: + throw Error('Unknown options.paramsFormat '+options.paramsFormat+'. Select one of formData, json or search.'); + break; } } - }; - - var self = this; - if (el instanceof HTMLElement) { - if (!force && el.dataset.simplyBound) { - return; + if (formData) { + fetchOptions.body = formData } - var jsonPath = getPath(el, this.config.attribute); - if (illegalNesting(el)) { - el.dataset.simplyBound = 'Error: nested binding'; - console.error('Error: found nested data-binding element:',el); - return; + if (options.user) { + fetchOptions.headers['Authorization'] = 'Basic '+btoa(options.user+':'+options.password); } - attachElement(jsonPath); - if (this.config.twoway) { - addMutationObserver(jsonPath); + fetchOptions.method = method.toUpperCase(); + var fetchURL = url.toString() + return fetch(fetchURL, fetchOptions); + }, + /** + * Creates a function to call one or more graphql queries + */ + graphqlQuery: function(url, query, options) { + options = Object.assign({ paramsFormat: 'json', url: url, responseFormat: 'json' }, options); + return function(params, operationName) { + let postParams = { + query: query + }; + if (operationName) { + postParams.operationName = operationName; + } + postParams.variables = params || {}; + return simply.api.fetch('POST', postParams, options ) + .then(function(response) { + return simply.api.getResult(response, options); + }); + } + }, + /** + * Handles the response and returns a Promise with the response data as specified + * @param response Response + * @param options + * - responseFormat: one of 'text', 'formData', 'blob', 'arrayBuffer', 'unbuffered' or 'json'. + * The default is json. + */ + getResult: function(response, options) { + if (response.ok) { + switch(options.responseFormat) { + case 'text': + return response.text(); + break; + case 'formData': + return response.formData(); + break; + case 'blob': + return response.blob(); + break; + case 'arrayBuffer': + return response.arrayBuffer(); + break; + case 'unbuffered': + return response.body; + break; + case 'json': + default: + return response.json(); + break; + } + } else { + throw { + status: response.status, + message: response.statusText, + response: response + } } - } else { - [].forEach.call(el, function(element) { - self.attach(element, model, force); - }); + }, + logError: function(error, options) { + console.error(error.status, error.message); } - }; + } - Binding.prototype.pauseObservers = function() { - observersPaused++; + var defaultOptions = { + path: '', + responseFormat: 'json', + paramsFormat: 'search', + verbs: ['get','post'], + handlers: { + fetch: api.fetch, + result: api.getResult, + error: api.logError + } }; - Binding.prototype.resumeObservers = function() { - observersPaused--; - }; + function cd(path, name) { + name = name.replace(/\//g,''); + if (!path.length || path[path.length-1]!=='/') { + path+='/'; + } + return path+encodeURIComponent(name); + } - simply.bind = function(config, force) { - return new Binding(config, force); - }; + function fetchChain(prop, params) { + var options = this; + return this.handlers.fetch + .call(this, prop, params, options) + .then(function(res) { + return options.handlers.result.call(options, res, options); + }) + .catch(function(error) { + return options.handlers.error.call(options, error, options); + }); + } + + function getApiHandler(options) { + options.handlers = Object.assign({}, defaultOptions.handlers, options.handlers); + options = Object.assign({}, defaultOptions, options); + + return { + get: function(cache, prop) { + if (!cache[prop]) { + if (options.verbs.indexOf(prop)!=-1) { + cache[prop] = function(params) { + return fetchChain.call(options, prop, params); + } + } else { + cache[prop] = api.proxy(Object.assign({}, options, { + path: cd(options.path, prop) + })); + } + } + return cache[prop]; + }, + apply: function(cache, thisArg, params) { + return fetchChain.call(options, 'get', params[0] ? params[0] : null) + } + } + } + + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = api; + } else { + if (!global.simply) { + global.simply = {}; + } + global.simply.api = api; + } + +})(this); +(function(global) { + 'use strict'; - return simply; -})(this.simply || {}, this);this.simply = (function(simply, global) { - simply.app = function(options) { + var app = function(options) { if (!options) { options = {}; } @@ -1419,12 +2040,22 @@ this.simply = (function(simply, global) { } if ( options.routes ) { simply.route.load(options.routes); + if (options.routeEvents) { + Object.keys(options.routeEvents).forEach(function(action) { + Object.keys(options.routeEvents[action]).forEach(function(route) { + options.routeEvents[action][route].forEach(function(callback) { + simply.route.addListener(action, route, callback); + }); + }); + }); + } simply.route.handleEvents(); global.setTimeout(function() { simply.route.match(global.location.pathname+global.location.hash); - },1); + }); } - this.container = options.container || document.body; + this.container = options.container || document.body; + this.keyboard = simply.keyboard ? simply.keyboard(this, options.keyboard || {}) : false; this.actions = simply.action ? simply.action(this, options.actions) : false; this.commands = simply.command ? simply.command(this, options.commands) : false; this.resize = simply.resize ? simply.resize(this, options.resize) : false; @@ -1442,187 +2073,16 @@ this.simply = (function(simply, global) { return this.container.querySelector('[data-simply-id='+id+']') || document.getElementById(id); }; - var app = new simplyApp(options); - - return app; - }; - - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { - - var listeners = {}; - - simply.activate = { - addListener: function(name, callback) { - if (!listeners[name]) { - listeners[name] = []; - } - listeners[name].push(callback); - initialCall(name); - }, - removeListener: function(name, callback) { - if (!listeners[name]) { - return false; - } - listeners[name] = listeners[name].filter(function(listener) { - return listener!=callback; - }); - } - }; - - var initialCall = function(name) { - var nodes = document.querySelectorAll('[data-simply-activate="'+name+'"]'); - if (nodes) { - [].forEach.call(nodes, function(node) { - callListeners(node); - }); - } - }; - - var callListeners = function(node) { - if (node && node.dataset.simplyActivate - && listeners[node.dataset.simplyActivate] - ) { - listeners[node.dataset.simplyActivate].forEach(function(callback) { - callback.call(node); - }); - } - }; - - var handleChanges = function(changes) { - var activateNodes = []; - for (var change of changes) { - if (change.type=='childList') { - [].forEach.call(change.addedNodes, function(node) { - if (node.querySelectorAll) { - var toActivate = [].slice.call(node.querySelectorAll('[data-simply-activate]')); - if (node.matches('[data-simply-activate]')) { - toActivate.push(node); - } - activateNodes = activateNodes.concat(toActivate); - } - }); - } - } - if (activateNodes.length) { - activateNodes.forEach(function(node) { - callListeners(node); - }); - } - }; - - var observer = new MutationObserver(handleChanges); - observer.observe(document, { - subtree: true, - childList: true - }); - - return simply; -})(this.simply || {}, this); -this.simply = (function(simply, global) { - var defaultActions = { - 'simply-hide': function(el) { - el.classList.remove('simply-visible'); - return Promise.resolve(); - }, - 'simply-show': function(el) { - el.classList.add('simply-visible'); - return Promise.resolve(); - }, - 'simply-select': function(el,group,target,targetGroup) { - if (group) { - this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']')); - } - el.classList.add('simply-selected'); - if (target) { - this.call('simply-select',target,targetGroup); - } - return Promise.resolve(); - }, - 'simply-toggle-select': function(el,group,target,targetGroup) { - if (!el.classList.contains('simply-selected')) { - this.call('simply-select',el,group,target,targetGroup); - } else { - this.call('simply-deselect',el,target); - } - return Promise.resolve(); - }, - 'simply-toggle-class': function(el,className,target) { - if (!target) { - target = el; - } - return Promise.resolve(target.classList.toggle(className)); - }, - 'simply-deselect': function(el,target) { - if ( typeof el.length=='number' && typeof el.item=='function') { - el = Array.prototype.slice.call(el); - } - if ( Array.isArray(el) ) { - for (var i=0,l=el.length; iget(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..046cef08 100644 --- a/solid/lib/Controller/CalendarController.php +++ b/solid/lib/Controller/CalendarController.php @@ -185,20 +185,31 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - 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); - - return $this->handleRequest($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: + // - 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); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/ContactsController.php b/solid/lib/Controller/ContactsController.php index 1fc3fcec..3d2f52bb 100644 --- a/solid/lib/Controller/ContactsController.php +++ b/solid/lib/Controller/ContactsController.php @@ -186,20 +186,31 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - 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); - - return $this->handleRequest($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: + // - 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); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/GetStorageUrlTrait.php b/solid/lib/Controller/GetStorageUrlTrait.php new file mode 100644 index 00000000..a5ab4a82 --- /dev/null +++ b/solid/lib/Controller/GetStorageUrlTrait.php @@ -0,0 +1,91 @@ +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); + $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..287d87fc 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 @@ -206,20 +204,30 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - 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); - - return $this->handleRequest($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: + // - 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); + } /** * @PublicPage * @NoAdminRequired @@ -287,6 +295,7 @@ private function getUserProfile($userId) { } } } + //TODO: privateTypeIndex and publisTypeIndex need to user getStorageURL if ($user !== null) { $profile = array( 'id' => $userId, @@ -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..ed1b95d9 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 @@ -378,20 +375,29 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - 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); - - return $this->handleRequest($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: + // - 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); + } /** * @PublicPage * @NoAdminRequired @@ -434,7 +440,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/templates/applauncher/index.php b/solid/templates/applauncher/index.php index d6fc1d65..bfd38195 100644 --- a/solid/templates/applauncher/index.php +++ b/solid/templates/applauncher/index.php @@ -44,6 +44,14 @@ +