Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27d7c18b77 |
+4
-4
@@ -24,7 +24,7 @@ package.json @nextcloud/server-dependabot @next
|
||||
package-lock.json @nextcloud/server-dependabot
|
||||
|
||||
# App maintainers
|
||||
/apps/admin_audit/appinfo/info.xml @luka-nextcloud @samin-z
|
||||
/apps/admin_audit/appinfo/info.xml @luka-nextcloud @blizzz
|
||||
/apps/cloud_federation_api/appinfo/info.xml @nfebe @mejo-
|
||||
/apps/comments/appinfo/info.xml @edward-ly @sorbaugh
|
||||
/apps/contactsinteraction/appinfo/info.xml @kesselb @SebastianKrupinski
|
||||
@@ -46,16 +46,16 @@ package-lock.json @nextcloud/server-dependabot
|
||||
/apps/files_versions/appinfo/info.xml @artonge @icewind1991
|
||||
/apps/oauth2/appinfo/info.xml @julien-nc @ChristophWurst
|
||||
/apps/provisioning_api/appinfo/info.xml @provokateurin @nickvergessen
|
||||
/apps/settings/appinfo/info.xml @hweihwang @sorbaugh
|
||||
/apps/settings/appinfo/info.xml @JuliaKirschenheuter @sorbaugh
|
||||
/apps/sharebymail/appinfo/info.xml @Altahrim @skjnldsv
|
||||
/apps/systemtags/appinfo/info.xml @Antreesy @marcelklehr
|
||||
/apps/theming/appinfo/info.xml @skjnldsv @juliusknorr
|
||||
/apps/twofactor_backupcodes/appinfo/info.xml @miaulalala @ChristophWurst
|
||||
/apps/updatenotification/appinfo/info.xml @enjeck @sorbaugh
|
||||
/apps/updatenotification/appinfo/info.xml @JuliaKirschenheuter @sorbaugh
|
||||
/apps/user_ldap/appinfo/info.xml @come-nc @blizzz
|
||||
/apps/user_status/appinfo/info.xml @Antreesy @nickvergessen
|
||||
/apps/weather_status/appinfo/info.xml @julien-nc @juliusknorr
|
||||
/apps/webhook_listeners/appinfo/info.xml @janepie @julien-nc
|
||||
/apps/webhook_listeners/appinfo/info.xml @come-nc @julien-nc
|
||||
/apps/workflowengine/appinfo/info.xml @blizzz @juliusknorr
|
||||
|
||||
# Files frontend expertise
|
||||
|
||||
@@ -73,7 +73,7 @@ body:
|
||||
options:
|
||||
- "32"
|
||||
- "33"
|
||||
- "34 (master)"
|
||||
- "master"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -23,7 +23,3 @@
|
||||
- [ ] [Backports requested](https://github.com/nextcloud/backportbot/#usage) where applicable (ex: critical bugfixes)
|
||||
- [ ] [Labels added](https://github.com/nextcloud/server/labels) where applicable (ex: bug/enhancement, `3. to review`, feature component)
|
||||
- [ ] [Milestone added](https://github.com/nextcloud/server/milestones) for target branch/version (ex: 32.x for `stable32`)
|
||||
|
||||
## AI (if applicable)
|
||||
|
||||
- [ ] The content of this PR was partly or fully generated using AI
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -74,9 +74,6 @@ jobs:
|
||||
autocheckers:
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src != 'false'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['8.2']
|
||||
@@ -91,7 +88,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -128,4 +125,4 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.changes.outputs.src != 'false' && (needs.autocheckers.result != 'success' || needs.autoloader.result != 'success') }}; then exit 1; fi
|
||||
run: if ${{ needs.autocheckers.result != 'success' || (needs.changes.outputs.src != 'false' && needs.autoloader.result != 'success') }}; then exit 1; fi
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check requirement
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
name: Auto-label bug reports
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
add-version-label:
|
||||
if: contains(github.event.issue.title, '[Bug]')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Extract version number and apply label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.issue.body || '';
|
||||
const normalizedBody = body.replace(/\r\n?/g, '\n');
|
||||
let label = '';
|
||||
|
||||
// Extract Nextcloud Server version number from a block like:
|
||||
// ### Nextcloud Server version
|
||||
// 32
|
||||
const versionMatch = normalizedBody.match(/### Nextcloud Server version\s*\n+([0-9]{1,3})\b/);
|
||||
let nextcloudVersion = null;
|
||||
if (versionMatch) {
|
||||
nextcloudVersion = parseInt(versionMatch[1], 10);
|
||||
label = nextcloudVersion + '-feedback';
|
||||
}
|
||||
|
||||
if (label) {
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: [label]
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to add label "${label}": ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: ./.github/codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.package-engines-versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.package-engines-versions.outputs.nodeVersion }}
|
||||
cache: npm
|
||||
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: none
|
||||
pull-requests: read
|
||||
|
||||
# On pull requests and if the comment starts with `/update-3rdparty`
|
||||
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/update-3rdparty')
|
||||
@@ -28,25 +27,8 @@ jobs:
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
# issue_comment events carry no pull_request context in their payload, so we
|
||||
# must fetch the PR via the API. This also gives us base.ref for free, avoiding
|
||||
# a second API call. The GITHUB_TOKEN needs pull-requests:read (granted above).
|
||||
- name: Get pull request metadata
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
id: get-pr
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const pull = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
core.setOutput('head_repo', pull.data.head.repo?.full_name ?? '');
|
||||
core.setOutput('base_ref', pull.data.base.ref);
|
||||
|
||||
- name: Disabled on forks
|
||||
if: steps.get-pr.outputs.head_repo != github.repository
|
||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
run: |
|
||||
echo 'Can not execute /update-3rdparty on forks'
|
||||
exit 1
|
||||
@@ -64,17 +46,24 @@ jobs:
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
|
||||
- name: Register server reference to fallback to master branch
|
||||
run: |
|
||||
base_ref="${{ steps.get-pr.outputs.base_ref }}"
|
||||
if [[ "$base_ref" == "main" || "$base_ref" == "master" ]]; then
|
||||
echo "server_ref=master" >> "$GITHUB_ENV"
|
||||
echo "Setting server_ref to master"
|
||||
elif [[ "$base_ref" =~ ^stable[0-9]+$ ]]; then
|
||||
echo "server_ref=$base_ref" >> "$GITHUB_ENV"
|
||||
echo "Setting server_ref to $base_ref"
|
||||
else
|
||||
echo "Not based on master/main/stable*, so skipping pull 3rdparty command"
|
||||
fi
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const baseRef = context.payload.pull_request.base.ref
|
||||
if (baseRef === 'main' || baseRef === 'master') {
|
||||
core.exportVariable('server_ref', 'master');
|
||||
console.log('Setting server_ref to master');
|
||||
} else {
|
||||
const regex = /^stable(\d+)$/
|
||||
const match = baseRef.match(regex)
|
||||
if (match) {
|
||||
core.exportVariable('server_ref', match[0]);
|
||||
console.log('Setting server_ref to ' + match[0]);
|
||||
} else {
|
||||
console.log('Not based on master/main/stable*, so skipping pull 3rdparty command');
|
||||
}
|
||||
}
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
|
||||
@@ -41,6 +41,12 @@ jobs:
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
|
||||
steps:
|
||||
- name: Disabled on forks
|
||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
run: |
|
||||
echo 'Can not run cypress on forks'
|
||||
exit 1
|
||||
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -66,7 +72,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
@@ -154,7 +160,7 @@ jobs:
|
||||
path: ./
|
||||
|
||||
- name: Set up node ${{ needs.init.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ needs.init.outputs.nodeVersion }}
|
||||
|
||||
@@ -165,11 +171,15 @@ jobs:
|
||||
run: ./node_modules/cypress/bin/cypress install
|
||||
|
||||
- name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
|
||||
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
|
||||
uses: cypress-io/github-action@84d178e4bbce871e23f2ffa3085898cde0e4f0ec # v7.1.2
|
||||
with:
|
||||
# We already installed the dependencies in the init job
|
||||
install: false
|
||||
component: ${{ matrix.containers == 'component' }}
|
||||
group: ${{ matrix.use-cypress-cloud && matrix.containers == 'component' && 'Run component' || matrix.use-cypress-cloud && 'Run E2E' || '' }}
|
||||
# cypress env
|
||||
ci-build-id: ${{ matrix.use-cypress-cloud && format('{0}-{1}', github.sha, github.run_number) || '' }}
|
||||
tag: ${{ matrix.use-cypress-cloud && github.event_name || '' }}
|
||||
env:
|
||||
# Needs to be prefixed with CYPRESS_
|
||||
CYPRESS_BRANCH: ${{ env.BRANCH }}
|
||||
@@ -178,13 +188,14 @@ jobs:
|
||||
# Needed for some specific code workarounds
|
||||
TESTING: true
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
SPLIT: ${{ matrix.total-containers }}
|
||||
SPLIT_INDEX: ${{ matrix.containers == 'component' && 0 || matrix.containers }}
|
||||
SPLIT_RANDOM_SEED: ${{ github.run_id }}
|
||||
SETUP_TESTING: ${{ matrix.containers == 'setup' && 'true' || '' }}
|
||||
|
||||
- name: Upload snapshots and videos
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: snapshots_${{ matrix.containers }}
|
||||
@@ -207,7 +218,7 @@ jobs:
|
||||
run: docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} tar -cvjf - data > data.tar
|
||||
|
||||
- name: Upload data archive
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: failure() && matrix.containers != 'component'
|
||||
with:
|
||||
name: nc_data_${{ matrix.containers }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
if [[ "${{ matrix.ftpd }}" == 'pure-ftpd' ]]; then docker run --name ftp -d --net host -e "PUBLICHOST=localhost" -e FTP_USER_NAME=test -e FTP_USER_PASS=test -e FTP_USER_HOME=/home/test -v /tmp/ftp:/home/test -v /tmp/ftp:/etc/pure-ftpd/passwd stilliard/pure-ftpd; fi
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-ftp
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-s3
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-s3
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -73,11 +73,11 @@ jobs:
|
||||
- name: Set up sftpd
|
||||
run: |
|
||||
sudo mkdir /tmp/sftp
|
||||
sudo chmod -R 0777 /tmp/sftp
|
||||
sudo chown -R 0777 /tmp/sftp
|
||||
if [[ '${{ matrix.sftpd }}' == 'openssh' ]]; then docker run -p 2222:22 --name sftp -d -v /tmp/sftp:/home/test atmoz/sftp 'test:test:::data'; fi
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-sftp
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-smb
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-webdav
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-generic
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up php 8.2
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: 8.2
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -52,69 +52,69 @@ jobs:
|
||||
name: ${{ matrix.service }} (${{ matrix.endpoint }} endpoint) php${{ matrix.php-versions }}
|
||||
|
||||
steps:
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
# https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: 'none'
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
# https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
coverage: 'none'
|
||||
ini-file: development
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: LizardByte/actions/actions/setup_python@0affa4f7bcb27562658960eee840eff8ff844578 # v2026.328.161128
|
||||
with:
|
||||
python-version: '2.7'
|
||||
- name: Set up Python
|
||||
uses: LizardByte/actions/actions/setup_python@9bf3ef783775e17fe6b8dde3585d94ec570b93c2 # v2026.212.22356
|
||||
with:
|
||||
python-version: '2.7'
|
||||
|
||||
- name: Set up CalDAVTester
|
||||
run: |
|
||||
git clone --depth=1 https://github.com/apple/ccs-caldavtester.git CalDAVTester
|
||||
git clone --depth=1 https://github.com/apple/ccs-pycalendar.git pycalendar
|
||||
- name: Set up CalDAVTester
|
||||
run: |
|
||||
git clone --depth=1 https://github.com/apple/ccs-caldavtester.git CalDAVTester
|
||||
git clone --depth=1 https://github.com/apple/ccs-pycalendar.git pycalendar
|
||||
|
||||
- name: Set up Nextcloud
|
||||
run: |
|
||||
mkdir data
|
||||
./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
|
||||
# disable the trashbin, so recurrent deletion of the same object works
|
||||
./occ config:app:set dav calendarRetentionObligation --value=0
|
||||
# Prepare users
|
||||
OC_PASS=user01 ./occ user:add --password-from-env user01
|
||||
OC_PASS=user02 ./occ user:add --password-from-env user02
|
||||
# Prepare calendars
|
||||
./occ dav:create-calendar user01 calendar
|
||||
./occ dav:create-calendar user01 shared
|
||||
./occ dav:create-calendar user02 calendar
|
||||
# Prepare address books
|
||||
./occ dav:create-addressbook user01 addressbook
|
||||
./occ dav:create-addressbook user02 addressbook
|
||||
- name: Set up Nextcloud
|
||||
run: |
|
||||
mkdir data
|
||||
./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
|
||||
# disable the trashbin, so recurrent deletion of the same object works
|
||||
./occ config:app:set dav calendarRetentionObligation --value=0
|
||||
# Prepare users
|
||||
OC_PASS=user01 ./occ user:add --password-from-env user01
|
||||
OC_PASS=user02 ./occ user:add --password-from-env user02
|
||||
# Prepare calendars
|
||||
./occ dav:create-calendar user01 calendar
|
||||
./occ dav:create-calendar user01 shared
|
||||
./occ dav:create-calendar user02 calendar
|
||||
# Prepare address books
|
||||
./occ dav:create-addressbook user01 addressbook
|
||||
./occ dav:create-addressbook user02 addressbook
|
||||
|
||||
- name: Run Nextcloud
|
||||
run: |
|
||||
php -S localhost:8888 &
|
||||
- name: Run Nextcloud
|
||||
run: |
|
||||
php -S localhost:8888 &
|
||||
|
||||
- name: Run CalDAVTester
|
||||
run: |
|
||||
cp "apps/dav/tests/testsuits/caldavtest/serverinfo-${{ matrix.endpoint }}${{ matrix.endpoint == 'old' && (matrix.service == 'CardDAV' && '-carddav' || '-caldav') || '' }}-endpoint.xml" "apps/dav/tests/testsuits/caldavtest/serverinfo.xml"
|
||||
pushd CalDAVTester
|
||||
PYTHONPATH="../pycalendar/src" python testcaldav.py --print-details-onfail --basedir "../apps/dav/tests/testsuits/caldavtest" -o cdt.txt \
|
||||
"${{ matrix.service }}/current-user-principal.xml" \
|
||||
"${{ matrix.service }}/sync-report.xml" \
|
||||
${{ matrix.endpoint == 'new' && format('{0}/sharing-{1}.xml', matrix.service, matrix.service == 'CalDAV' && 'calendars' || 'addressbooks') || ';' }}
|
||||
popd
|
||||
- name: Run CalDAVTester
|
||||
run: |
|
||||
cp "apps/dav/tests/testsuits/caldavtest/serverinfo-${{ matrix.endpoint }}${{ matrix.endpoint == 'old' && (matrix.service == 'CardDAV' && '-carddav' || '-caldav') || '' }}-endpoint.xml" "apps/dav/tests/testsuits/caldavtest/serverinfo.xml"
|
||||
pushd CalDAVTester
|
||||
PYTHONPATH="../pycalendar/src" python testcaldav.py --print-details-onfail --basedir "../apps/dav/tests/testsuits/caldavtest" -o cdt.txt \
|
||||
"${{ matrix.service }}/current-user-principal.xml" \
|
||||
"${{ matrix.service }}/sync-report.xml" \
|
||||
${{ matrix.endpoint == 'new' && format('{0}/sharing-{1}.xml', matrix.service, matrix.service == 'CalDAV' && 'calendars' || 'addressbooks') || ';' }}
|
||||
popd
|
||||
|
||||
- name: Print Nextcloud logs
|
||||
if: always()
|
||||
run: |
|
||||
cat data/nextcloud.log
|
||||
- name: Print Nextcloud logs
|
||||
if: always()
|
||||
run: |
|
||||
cat data/nextcloud.log
|
||||
|
||||
caldav-integration-summary:
|
||||
permissions:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -95,6 +95,7 @@ jobs:
|
||||
|
||||
- name: Wait for S3
|
||||
run: |
|
||||
sleep 10
|
||||
curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready
|
||||
|
||||
- name: Set up Nextcloud
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
ref: ${{ matrix.activity-versions }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -44,9 +44,6 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src != 'false'
|
||||
|
||||
name: php-cs
|
||||
|
||||
steps:
|
||||
@@ -56,7 +53,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: 8.2
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
outputs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
|
||||
@@ -18,37 +18,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest-low
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
outputs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- '.github/workflows/**'
|
||||
- '**/src/**'
|
||||
- '**/appinfo/info.xml'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '**.css'
|
||||
- '**.scss'
|
||||
- '**.vue'
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src != 'false'
|
||||
|
||||
name: stylelint
|
||||
|
||||
steps:
|
||||
@@ -65,7 +37,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
@@ -79,18 +51,3 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run stylelint
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, lint]
|
||||
|
||||
if: always()
|
||||
|
||||
# This is the summary, we just avoid to rename it so that branch protection rules still match
|
||||
name: stylelint
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
@@ -83,11 +83,14 @@ jobs:
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
# - name: Test
|
||||
# run: npm run test --if-present
|
||||
|
||||
- name: Test and process coverage
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./coverage/lcov.info,./coverage/legacy/lcov.info
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
fallbackNpm: '^11.3'
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-azure
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -111,6 +111,7 @@ jobs:
|
||||
|
||||
- name: Wait for S3
|
||||
run: |
|
||||
sleep 10
|
||||
curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready
|
||||
|
||||
- name: PHPUnit
|
||||
@@ -122,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-s3
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-swift
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: '8.2'
|
||||
|
||||
@@ -15,13 +15,7 @@ jobs:
|
||||
performance-testing:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Skip entirely on fork PRs so the job result is 'skipped' rather than
|
||||
# 'failure'. The profiler action uses github.event.pull_request.head.repo.clone_url
|
||||
# and GITHUB_TOKEN in ways that do not work reliably from forks, and a
|
||||
# clean skip is far less confusing for contributors than a mid-run error.
|
||||
if: >-
|
||||
github.repository_owner != 'nextcloud-gmbh' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -34,6 +28,11 @@ jobs:
|
||||
name: performance-${{ matrix.php-versions }}
|
||||
|
||||
steps:
|
||||
- name: Disabled on forks
|
||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
run: |
|
||||
echo 'Can not run performance tests on forks'
|
||||
exit 1
|
||||
|
||||
- name: Checkout server before PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Run before measurements
|
||||
uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044
|
||||
uses: nextcloud/profiler@6801ee10fc80f10b444388fb6ca9b36ad8a2ea83
|
||||
with:
|
||||
run: |
|
||||
curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test
|
||||
@@ -86,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Run after measurements
|
||||
id: compare
|
||||
uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044
|
||||
uses: nextcloud/profiler@6801ee10fc80f10b444388fb6ca9b36ad8a2ea83
|
||||
with:
|
||||
run: |
|
||||
curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test
|
||||
@@ -100,7 +99,7 @@ jobs:
|
||||
|
||||
- name: Upload profiles
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
name: profiles
|
||||
path: |
|
||||
|
||||
@@ -56,4 +56,4 @@ jobs:
|
||||
- name: PHPUnit
|
||||
uses: docker://ghcr.io/nextcloud/continuous-integration-php8.4-32bit:latest
|
||||
with:
|
||||
args: /bin/sh -c "composer run test -- --exclude-group PRIMARY-azure --exclude-group PRIMARY-s3 --exclude-group PRIMARY-swift --exclude-group Memcached --exclude-group Redis --exclude-group RoutingWeirdness"
|
||||
args: /bin/sh -c "composer run test -- --exclude-group PRIMARY-azure,PRIMARY-s3,PRIMARY-swift,Memcached,Redis,RoutingWeirdness"
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mariadb
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-memcached
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mysql
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mysql
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Upload nodb code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.nodb.xml
|
||||
flags: phpunit-nodb
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src}}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
curl -f -m 1 --retry-connrefused --retry 10 --retry-delay 10 http://localhost:9000/minio/health/ready
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:db -- --log-junit junit.xml
|
||||
run: composer run test:db
|
||||
|
||||
- name: S3 logs
|
||||
if: always()
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-oci
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-postgres
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-sqlite
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Set up php${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
|
||||
@@ -50,7 +50,6 @@ jobs:
|
||||
run: |
|
||||
composer remove nextcloud/ocp --dev --no-scripts
|
||||
composer i
|
||||
git restore lib/composer/composer
|
||||
|
||||
- name: Rector
|
||||
run: composer run rector
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
with:
|
||||
php-version: '8.2'
|
||||
extensions: apcu,ctype,curl,dom,fileinfo,ftp,gd,imagick,intl,json,ldap,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v9
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v9
|
||||
with:
|
||||
repo-token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
stale-issue-message: >
|
||||
|
||||
@@ -21,35 +21,10 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
outputs:
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
|
||||
steps:
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
continue-on-error: true
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- '.github/workflows/**'
|
||||
- '3rdparty/**'
|
||||
- '**/appinfo/**'
|
||||
- '**/lib/**'
|
||||
- '**/templates/**'
|
||||
- 'vendor/**'
|
||||
- 'vendor-bin/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- '**.php'
|
||||
|
||||
static-code-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -59,7 +34,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: '8.2'
|
||||
@@ -81,8 +56,7 @@ jobs:
|
||||
static-code-analysis-security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.src != 'false' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
@@ -95,7 +69,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: '8.2'
|
||||
@@ -114,15 +88,14 @@ jobs:
|
||||
|
||||
- name: Upload Security Analysis results to GitHub
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3
|
||||
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
static-code-analysis-ocp:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -132,7 +105,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: '8.2'
|
||||
@@ -154,8 +127,7 @@ jobs:
|
||||
static-code-analysis-ncu:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -165,7 +137,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
php-version: '8.2'
|
||||
@@ -183,8 +155,7 @@ jobs:
|
||||
static-code-analysis-strict:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -194,7 +165,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
|
||||
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
|
||||
with:
|
||||
php-version: '8.2'
|
||||
extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip
|
||||
@@ -207,28 +178,3 @@ jobs:
|
||||
|
||||
- name: Psalm
|
||||
run: composer run psalm:strict -- --threads=1 --monochrome --no-progress --output-format=github
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, static-code-analysis, static-code-analysis-security, static-code-analysis-ocp, static-code-analysis-ncu, static-code-analysis-strict]
|
||||
|
||||
if: always()
|
||||
|
||||
name: static-code-analysis-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: |
|
||||
if ${{ needs.changes.outputs.src != 'false' && (
|
||||
needs.static-code-analysis-security.result != 'success' ||
|
||||
(github.event_name != 'push' && (
|
||||
needs.static-code-analysis.result != 'success' ||
|
||||
needs.static-code-analysis-ocp.result != 'success' ||
|
||||
needs.static-code-analysis-ncu.result != 'success' ||
|
||||
needs.static-code-analysis-strict.result != 'success'
|
||||
))
|
||||
) }}; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
|
||||
signoff: true
|
||||
branch: "automated/noid/update-min-supported-desktop-version"
|
||||
branch: "automated/noid/${{ matrix.branches }}-update-min-supported-desktop-version"
|
||||
title: "chore: Update minimum supported desktop version to ${{ steps.extract-version.outputs.VERSION }}"
|
||||
base: "master"
|
||||
body: |
|
||||
|
||||
+4
-5
@@ -17,17 +17,16 @@ node_modules/
|
||||
|
||||
# ignore all apps except core ones
|
||||
/apps*/*
|
||||
!/apps/admin_audit
|
||||
!/apps/appstore
|
||||
!/apps/cloud_federation_api
|
||||
!/apps/comments
|
||||
!/apps/contactsinteraction
|
||||
!/apps/dashboard
|
||||
!/apps/dav
|
||||
!/apps/encryption
|
||||
!/apps/files
|
||||
!/apps/federation
|
||||
!/apps/federatedfilesharing
|
||||
!/apps/files
|
||||
!/apps/sharebymail
|
||||
!/apps/encryption
|
||||
!/apps/files_external
|
||||
!/apps/files_reminders
|
||||
!/apps/files_sharing
|
||||
@@ -39,9 +38,9 @@ node_modules/
|
||||
!/apps/profile
|
||||
!/apps/provisioning_api
|
||||
!/apps/settings
|
||||
!/apps/sharebymail
|
||||
!/apps/systemtags
|
||||
!/apps/testing
|
||||
!/apps/admin_audit
|
||||
!/apps/updatenotification
|
||||
!/apps/theming
|
||||
!/apps/twofactor_backupcodes
|
||||
|
||||
@@ -129,10 +129,11 @@
|
||||
## Rule: Map /remote* --> /remote.php* including the query string
|
||||
##
|
||||
## Context:
|
||||
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
|
||||
## - XXX: Is this even used anymore? Seems a relic from <NC12
|
||||
##
|
||||
|
||||
RewriteRule ^remote/(.*) remote.php [L]
|
||||
RewriteRule ^remote/(.*) remote.php [QSA,L]
|
||||
|
||||
##
|
||||
## Rule: Prevent access to non-public files
|
||||
@@ -147,19 +148,21 @@
|
||||
## - Intentionally excludes URIs used for HTTPS certificate verifications
|
||||
## - RFC 8555 / ACME HTTP Challenges (acme-challenge)
|
||||
## - File-based Validations (pki-validation)
|
||||
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
|
||||
## - XXX: Sometimes we are using `/index.php` and other times `index.php` as our replacement URI; this may be incorrect
|
||||
##
|
||||
|
||||
RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [L]
|
||||
RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [QSA,L]
|
||||
|
||||
##
|
||||
## Rule: Map the ocm-provider handling to our main frontend controller (/index.php)
|
||||
##
|
||||
## Context:
|
||||
## - XXX: `QSA` seems unnecessary (no-op) here (query string is passed by default when the replacement URI doesn't contain a query string)
|
||||
## - XXX: Sometimes we are using `/index.php` and other times `index.php` as our replacement URI; this may be incorrect
|
||||
##
|
||||
|
||||
RewriteRule ^ocm-provider/?$ index.php [L]
|
||||
RewriteRule ^ocm-provider/?$ index.php [QSA,L]
|
||||
|
||||
##
|
||||
## Rule: Prevent access to more non-public files
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
# Global exclude
|
||||
.editorconfig
|
||||
.envrc
|
||||
.git
|
||||
.git-blame-ignore-revs
|
||||
.gitattributes
|
||||
@@ -12,44 +11,12 @@
|
||||
.gitignore
|
||||
.gitmodules
|
||||
.idea
|
||||
.jshint
|
||||
.jshintrc
|
||||
.l10nignore
|
||||
.mailmap
|
||||
.nextcloudignore
|
||||
.noopenapi
|
||||
.npmignore
|
||||
.php-cs-fixer*
|
||||
.pre-commit-config.yaml
|
||||
.tag
|
||||
.tx
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
COPYING-README
|
||||
DESIGN.md
|
||||
Makefile
|
||||
README.md
|
||||
SECURITY.md
|
||||
codecov.yml
|
||||
cs-fixer
|
||||
csfixer
|
||||
custom.d.ts
|
||||
cypress
|
||||
cypress.config.ts
|
||||
eslint.config.js
|
||||
flake.lock
|
||||
flake.nix
|
||||
openapi-extractor
|
||||
phpunit
|
||||
psalm
|
||||
psalm*.xml
|
||||
rector
|
||||
stylelint.config.js
|
||||
tests
|
||||
tsconfig.json
|
||||
vite.config.ts
|
||||
vitest.config.ts
|
||||
window.d.ts
|
||||
|
||||
# Server specific
|
||||
/.devcontainer
|
||||
@@ -60,5 +27,3 @@ window.d.ts
|
||||
/config/config.php
|
||||
/contribute
|
||||
/data
|
||||
/openapi.json
|
||||
/vendor-bin
|
||||
|
||||
@@ -8,12 +8,6 @@ source_file = translationfiles/templates/admin_audit.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:nextcloud:p:nextcloud:r:appstore]
|
||||
file_filter = translationfiles/<lang>/appstore.po
|
||||
source_file = translationfiles/templates/appstore.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:nextcloud:p:nextcloud:r:cloud_federation_api]
|
||||
file_filter = translationfiles/<lang>/cloud_federation_api.po
|
||||
source_file = translationfiles/templates/cloud_federation_api.pot
|
||||
|
||||
+1
-1
Submodule 3rdparty updated: f257bfe47e...34fdf0b083
@@ -1,7 +0,0 @@
|
||||
OC.L10N.register(
|
||||
"admin_audit",
|
||||
{
|
||||
"Auditing / Logging" : "নিরীক্ষা",
|
||||
"Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "নেক্সটক্লাউডের নিরীক্ষামূলক সক্ষমতা প্রদান করে যেমন লগিং ফাইল অ্যাক্সেস বা অন্য কোনো জরুরী পদক্ষেপসমূহ"
|
||||
},
|
||||
"nplurals=2; plural=(n != 1);");
|
||||
@@ -1,5 +0,0 @@
|
||||
{ "translations": {
|
||||
"Auditing / Logging" : "নিরীক্ষা",
|
||||
"Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "নেক্সটক্লাউডের নিরীক্ষামূলক সক্ষমতা প্রদান করে যেমন লগিং ফাইল অ্যাক্সেস বা অন্য কোনো জরুরী পদক্ষেপসমূহ"
|
||||
},"pluralForm" :"nplurals=2; plural=(n != 1);"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
version = 1
|
||||
SPDX-PackageName = "nextcloud"
|
||||
SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
|
||||
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server"
|
||||
|
||||
[[annotations]]
|
||||
path = ["tests/fixtures/categories.json", "tests/fixtures/categories-api-response.json"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors"
|
||||
SPDX-License-Identifier = "CC-BY-SA-4.0"
|
||||
|
||||
[[annotations]]
|
||||
path = ["img/app.svg"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2018-2024 Google LLC"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>settings</id>
|
||||
<name>Nextcloud Appstore</name>
|
||||
<summary>Nextcloud Appstore</summary>
|
||||
<description>Nextcloud Appstore</description>
|
||||
<version>1.0.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Nextcloud</author>
|
||||
<namespace>Appstore</namespace>
|
||||
|
||||
<category>customization</category>
|
||||
<bugs>https://github.com/nextcloud/server/issues</bugs>
|
||||
<dependencies>
|
||||
<nextcloud min-version="33" max-version="33"/>
|
||||
</dependencies>
|
||||
</info>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitAppstore::getLoader();
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"config" : {
|
||||
"vendor-dir": ".",
|
||||
"optimize-autoloader": true,
|
||||
"classmap-authoritative": true,
|
||||
"autoloader-suffix": "Appstore"
|
||||
},
|
||||
"autoload" : {
|
||||
"psr-4": {
|
||||
"OCA\\Appstore\\": "../lib/"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d751713988987e9331980363e24189ce",
|
||||
"packages": [],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = $vendorDir;
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'OCA\\Appstore\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Appstore\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Appstore\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php',
|
||||
'OCA\\Appstore\\Controller\\DiscoverController' => $baseDir . '/../lib/Controller/DiscoverController.php',
|
||||
'OCA\\Appstore\\Controller\\PageController' => $baseDir . '/../lib/Controller/PageController.php',
|
||||
'OCA\\Appstore\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php',
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = $vendorDir;
|
||||
|
||||
return array(
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = $vendorDir;
|
||||
|
||||
return array(
|
||||
'OCA\\Appstore\\' => array($baseDir . '/../lib'),
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInitAppstore
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInitAppstore', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInitAppstore', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInitAppstore::getInitializer($loader));
|
||||
|
||||
$loader->setClassMapAuthoritative(true);
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInitAppstore
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'O' =>
|
||||
array (
|
||||
'OCA\\Appstore\\' => 13,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'OCA\\Appstore\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/../lib',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'OCA\\Appstore\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Appstore\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Appstore\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php',
|
||||
'OCA\\Appstore\\Controller\\DiscoverController' => __DIR__ . '/..' . '/../lib/Controller/DiscoverController.php',
|
||||
'OCA\\Appstore\\Controller\\PageController' => __DIR__ . '/..' . '/../lib/Controller/PageController.php',
|
||||
'OCA\\Appstore\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInitAppstore::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInitAppstore::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInitAppstore::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"packages": [],
|
||||
"dev": false,
|
||||
"dev-package-names": []
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '3efb1d80e9851e0c33311a7722f523e020654691',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../',
|
||||
'aliases' => array(),
|
||||
'dev' => false,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '3efb1d80e9851e0c33311a7722f523e020654691',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Appstore\AppInfo;
|
||||
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'appstore';
|
||||
|
||||
/**
|
||||
* @param array $urlParams
|
||||
*/
|
||||
public function __construct(array $urlParams = []) {
|
||||
parent::__construct(self::APP_ID, $urlParams);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Appstore\Controller;
|
||||
|
||||
use OC\App\AppManager;
|
||||
use OC\App\AppStore\Bundles\BundleFetcher;
|
||||
use OC\App\AppStore\Fetcher\AppFetcher;
|
||||
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
||||
use OC\App\AppStore\Version\VersionParser;
|
||||
use OC\App\DependencyAnalyzer;
|
||||
use OC\Installer;
|
||||
use OCA\Appstore\AppInfo\Application;
|
||||
use OCP\App\AppPathNotFoundException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\ApiRoute;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCS\OCSException;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Server;
|
||||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)]
|
||||
class ApiController extends OCSController {
|
||||
|
||||
/** @var array */
|
||||
private $allApps = [];
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private IConfig $config,
|
||||
private IAppConfig $appConfig,
|
||||
private AppManager $appManager,
|
||||
private DependencyAnalyzer $dependencyAnalyzer,
|
||||
private CategoryFetcher $categoryFetcher,
|
||||
private AppFetcher $appFetcher,
|
||||
private IFactory $l10nFactory,
|
||||
private BundleFetcher $bundleFetcher,
|
||||
private Installer $installer,
|
||||
private IRegistry $subscriptionRegistry,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available categories
|
||||
*/
|
||||
#[ApiRoute('GET', '/api/v1/apps/categories')]
|
||||
public function listCategories(): DataResponse {
|
||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||
|
||||
$categories = $this->categoryFetcher->get();
|
||||
$categories = array_map(fn ($category) => [
|
||||
'id' => $category['id'],
|
||||
'displayName' => $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name'],
|
||||
], $categories);
|
||||
|
||||
return new DataResponse(array_values($categories));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available apps
|
||||
*/
|
||||
#[ApiRoute('GET', '/api/v1/apps')]
|
||||
public function listApps(): DataResponse {
|
||||
$apps = $this->getAllApps();
|
||||
|
||||
$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
|
||||
if (!is_array($ignoreMaxApps)) {
|
||||
$this->logger->warning('The value given for app_install_overwrite is not an array. Ignoring...');
|
||||
$ignoreMaxApps = [];
|
||||
}
|
||||
|
||||
// Extend existing app details
|
||||
$apps = array_map(function (array $appData) use ($ignoreMaxApps) {
|
||||
if (isset($appData['appstoreData'])) {
|
||||
$appstoreData = $appData['appstoreData'];
|
||||
$appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? '');
|
||||
$appData['category'] = $appstoreData['categories'];
|
||||
$appData['releases'] = $appstoreData['releases'];
|
||||
}
|
||||
|
||||
$newVersion = $this->installer->isUpdateAvailable($appData['id']);
|
||||
if ($newVersion) {
|
||||
$appData['update'] = $newVersion;
|
||||
}
|
||||
|
||||
// fix groups to be an array
|
||||
$groups = [];
|
||||
if (is_string($appData['groups'])) {
|
||||
$groups = json_decode($appData['groups']);
|
||||
// ensure 'groups' is an array
|
||||
if (!is_array($groups)) {
|
||||
$groups = [$groups];
|
||||
}
|
||||
}
|
||||
$appData['groups'] = $groups;
|
||||
// analyze dependencies
|
||||
$ignoreMax = in_array($appData['id'], $ignoreMaxApps);
|
||||
$missing = $this->dependencyAnalyzer->analyze($appData, $ignoreMax);
|
||||
$appData['missingDependencies'] = $missing;
|
||||
|
||||
$appData['missingMinNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
|
||||
$appData['missingMaxNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
|
||||
$appData['isCompatible'] = $this->dependencyAnalyzer->isMarkedCompatible($appData);
|
||||
$appData['internal'] = in_array($appData['id'], $this->appManager->getAlwaysEnabledApps());
|
||||
|
||||
return $appData;
|
||||
}, $apps);
|
||||
|
||||
usort($apps, $this->sortApps(...));
|
||||
|
||||
return new DataResponse(array_values($apps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable one apps
|
||||
*
|
||||
* App will be enabled for specific groups only if $groups is defined
|
||||
*
|
||||
* @param string $appId - The app to enable
|
||||
* @param array $groups - The groups to enable the app for
|
||||
* @return DataResponse
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[ApiRoute('POST', '/api/v1/apps/enable')]
|
||||
public function enableApp(string $appId, array $groups = []): DataResponse {
|
||||
try {
|
||||
$updateRequired = false;
|
||||
|
||||
$appId = $this->appManager->cleanAppId($appId);
|
||||
|
||||
// Check if app is already downloaded
|
||||
if (!$this->installer->isDownloaded($appId)) {
|
||||
$this->installer->downloadApp($appId);
|
||||
}
|
||||
|
||||
$this->installer->installApp($appId);
|
||||
|
||||
if (count($groups) > 0) {
|
||||
$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
|
||||
} else {
|
||||
$this->appManager->enableApp($appId);
|
||||
}
|
||||
$updateRequired = $updateRequired || $this->appManager->isUpgradeRequired($appId);
|
||||
return new DataResponse(['update_required' => $updateRequired]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('could not enable app', ['exception' => $e]);
|
||||
throw new OCSException('could not enable app', Http::STATUS_INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable an app
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[ApiRoute('POST', '/api/v1/apps/disable')]
|
||||
public function disableApp(string $appId): DataResponse {
|
||||
try {
|
||||
$appId = $this->appManager->cleanAppId($appId);
|
||||
$this->appManager->disableApp($appId);
|
||||
return new DataResponse([]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('could not disable app', ['exception' => $e]);
|
||||
throw new OCSException('could not disable app', Http::STATUS_INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall an app
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[ApiRoute('POST', '/api/v1/apps/uninstall')]
|
||||
public function uninstallApp(string $appId): DataResponse {
|
||||
$appId = $this->appManager->cleanAppId($appId);
|
||||
$result = $this->installer->removeApp($appId);
|
||||
if ($result !== false) {
|
||||
// If this app was force enabled, remove the force-enabled-state
|
||||
$this->appManager->removeOverwriteNextcloudRequirement($appId);
|
||||
$this->appManager->clearAppsCache();
|
||||
return new DataResponse([]);
|
||||
}
|
||||
throw new OCSException('could not remove app', Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an app
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[ApiRoute('POST', '/api/v1/apps/update')]
|
||||
public function updateApp(string $appId): DataResponse {
|
||||
$appId = $this->appManager->cleanAppId($appId);
|
||||
|
||||
$this->config->setSystemValue('maintenance', true);
|
||||
try {
|
||||
$result = $this->installer->updateAppstoreApp($appId);
|
||||
$this->config->setSystemValue('maintenance', false);
|
||||
if ($result === false) {
|
||||
throw new \Exception('Update failed');
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->config->setSystemValue('maintenance', false);
|
||||
throw new OCSException('could not update app', Http::STATUS_INTERNAL_SERVER_ERROR, $ex);
|
||||
}
|
||||
|
||||
return new DataResponse([]);
|
||||
}
|
||||
/**
|
||||
* Force enable an app.
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[ApiRoute('POST', '/api/v1/apps/force')]
|
||||
public function force(string $appId): DataResponse {
|
||||
$appId = $this->appManager->cleanAppId($appId);
|
||||
$this->appManager->overwriteNextcloudRequirement($appId);
|
||||
return new DataResponse([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert URL to proxied URL so CSP is no problem
|
||||
*/
|
||||
private function createProxyPreviewUrl(string $url): string {
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url);
|
||||
}
|
||||
|
||||
private function fetchApps() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach ($apps as $app) {
|
||||
$app['installed'] = true;
|
||||
|
||||
if (isset($app['screenshot'][0])) {
|
||||
$appScreenshot = $app['screenshot'][0] ?? null;
|
||||
if (is_array($appScreenshot)) {
|
||||
// Screenshot with thumbnail
|
||||
$appScreenshot = $appScreenshot['@value'];
|
||||
}
|
||||
|
||||
$app['screenshot'] = $this->createProxyPreviewUrl($appScreenshot);
|
||||
}
|
||||
$this->allApps[$app['id']] = $app;
|
||||
}
|
||||
|
||||
$apps = $this->getAppsForCategory('');
|
||||
$supportedApps = $this->subscriptionRegistry->delegateGetSupportedApps();
|
||||
foreach ($apps as $app) {
|
||||
$app['appstore'] = true;
|
||||
if (!array_key_exists($app['id'], $this->allApps)) {
|
||||
$this->allApps[$app['id']] = $app;
|
||||
} else {
|
||||
$this->allApps[$app['id']] = array_merge($app, $this->allApps[$app['id']]);
|
||||
}
|
||||
|
||||
if (in_array($app['id'], $supportedApps)) {
|
||||
$this->allApps[$app['id']]['level'] = \OC_App::supportedApp;
|
||||
}
|
||||
}
|
||||
|
||||
// add bundle information
|
||||
$bundles = $this->bundleFetcher->getBundles();
|
||||
foreach ($bundles as $bundle) {
|
||||
foreach ($bundle->getAppIdentifiers() as $identifier) {
|
||||
foreach ($this->allApps as &$app) {
|
||||
if ($app['id'] === $identifier) {
|
||||
$app['bundleIds'][] = $bundle->getIdentifier();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getAllApps() {
|
||||
if (empty($this->allApps)) {
|
||||
$this->fetchApps();
|
||||
}
|
||||
return $this->allApps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all apps for a category from the app store
|
||||
*
|
||||
* @param string $requestedCategory
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getAppsForCategory($requestedCategory = ''): array {
|
||||
$versionParser = new VersionParser();
|
||||
$formattedApps = [];
|
||||
$apps = $this->appFetcher->get();
|
||||
foreach ($apps as $app) {
|
||||
// Skip all apps not in the requested category
|
||||
if ($requestedCategory !== '') {
|
||||
$isInCategory = false;
|
||||
foreach ($app['categories'] as $category) {
|
||||
if ($category === $requestedCategory) {
|
||||
$isInCategory = true;
|
||||
}
|
||||
}
|
||||
if (!$isInCategory) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($app['releases'][0]['rawPlatformVersionSpec'])) {
|
||||
continue;
|
||||
}
|
||||
$nextcloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
|
||||
$nextcloudVersionDependencies = [];
|
||||
if ($nextcloudVersion->getMinimumVersion() !== '') {
|
||||
$nextcloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextcloudVersion->getMinimumVersion();
|
||||
}
|
||||
if ($nextcloudVersion->getMaximumVersion() !== '') {
|
||||
$nextcloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextcloudVersion->getMaximumVersion();
|
||||
}
|
||||
$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
|
||||
|
||||
try {
|
||||
$this->appManager->getAppPath($app['id']);
|
||||
$existsLocally = true;
|
||||
} catch (AppPathNotFoundException) {
|
||||
$existsLocally = false;
|
||||
}
|
||||
|
||||
$phpDependencies = [];
|
||||
if ($phpVersion->getMinimumVersion() !== '') {
|
||||
$phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion();
|
||||
}
|
||||
if ($phpVersion->getMaximumVersion() !== '') {
|
||||
$phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion();
|
||||
}
|
||||
if (isset($app['releases'][0]['minIntSize'])) {
|
||||
$phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize'];
|
||||
}
|
||||
$authors = '';
|
||||
foreach ($app['authors'] as $key => $author) {
|
||||
$authors .= $author['name'];
|
||||
if ($key !== count($app['authors']) - 1) {
|
||||
$authors .= ', ';
|
||||
}
|
||||
}
|
||||
|
||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||
$enabledValue = $this->appConfig->getValueString($app['id'], 'enabled', 'no');
|
||||
$groups = null;
|
||||
if ($enabledValue !== 'no' && $enabledValue !== 'yes') {
|
||||
$groups = $enabledValue;
|
||||
}
|
||||
|
||||
$currentVersion = '';
|
||||
if ($this->appManager->isEnabledForAnyone($app['id'])) {
|
||||
$currentVersion = $this->appManager->getAppVersion($app['id']);
|
||||
} else {
|
||||
$currentVersion = $app['releases'][0]['version'];
|
||||
}
|
||||
|
||||
$formattedApps[] = [
|
||||
'id' => $app['id'],
|
||||
'app_api' => false,
|
||||
'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'],
|
||||
'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'],
|
||||
'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'],
|
||||
'license' => $app['releases'][0]['licenses'],
|
||||
'author' => $authors,
|
||||
'shipped' => $this->appManager->isShipped($app['id']),
|
||||
'internal' => in_array($app['id'], $this->appManager->getAlwaysEnabledApps()),
|
||||
'version' => $currentVersion,
|
||||
'types' => [],
|
||||
'documentation' => [
|
||||
'admin' => $app['adminDocs'],
|
||||
'user' => $app['userDocs'],
|
||||
'developer' => $app['developerDocs']
|
||||
],
|
||||
'website' => $app['website'],
|
||||
'bugs' => $app['issueTracker'],
|
||||
'dependencies' => array_merge(
|
||||
$nextcloudVersionDependencies,
|
||||
$phpDependencies
|
||||
),
|
||||
'level' => ($app['isFeatured'] === true) ? 200 : 100,
|
||||
'missingMaxNextcloudVersion' => false,
|
||||
'missingMinNextcloudVersion' => false,
|
||||
'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '',
|
||||
'score' => $app['ratingOverall'],
|
||||
'ratingNumOverall' => $app['ratingNumOverall'],
|
||||
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
|
||||
'removable' => $existsLocally,
|
||||
'active' => $this->appManager->isEnabledForUser($app['id']),
|
||||
'needsDownload' => !$existsLocally,
|
||||
'groups' => $groups,
|
||||
'fromAppStore' => true,
|
||||
'appstoreData' => $app,
|
||||
];
|
||||
}
|
||||
|
||||
return $formattedApps;
|
||||
}
|
||||
|
||||
private function getGroupList(array $groups) {
|
||||
$groupManager = Server::get(IGroupManager::class);
|
||||
$groupsList = [];
|
||||
foreach ($groups as $group) {
|
||||
$groupItem = $groupManager->get($group);
|
||||
if ($groupItem instanceof IGroup) {
|
||||
$groupsList[] = $groupManager->get($group);
|
||||
}
|
||||
}
|
||||
return $groupsList;
|
||||
}
|
||||
|
||||
private function sortApps($a, $b) {
|
||||
$a = (string)$a['name'];
|
||||
$b = (string)$b['name'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Appstore\Controller;
|
||||
|
||||
use OC\App\AppStore\Fetcher\AppDiscoverFetcher;
|
||||
use OCA\Appstore\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Security\RateLimiting\ILimiter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
|
||||
class DiscoverController extends Controller {
|
||||
|
||||
private IAppData $appData;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
IAppDataFactory $appDataFactory,
|
||||
private IClientService $clientService,
|
||||
private AppDiscoverFetcher $discoverFetcher,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->appData = $appDataFactory->get(Application::APP_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active entries for the app discover section
|
||||
*/
|
||||
#[NoCSRFRequired]
|
||||
#[FrontpageRoute('GET', '/api/v1/discover')]
|
||||
public function getAppDiscoverJSON(): JSONResponse {
|
||||
$data = $this->discoverFetcher->get(true);
|
||||
return new JSONResponse(array_values($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a image for the app discover section - this is proxied for privacy and CSP reasons
|
||||
*
|
||||
* @param string $fileName - The image file name
|
||||
*/
|
||||
#[NoCSRFRequired]
|
||||
#[FrontpageRoute('GET', '/api/v1/discover/media')]
|
||||
public function getAppDiscoverMedia(string $fileName, ILimiter $limiter, IUserSession $session): FileDisplayResponse|NotFoundResponse {
|
||||
$getEtag = $this->discoverFetcher->getETag() ?? date('Y-m');
|
||||
$etag = trim($getEtag, '"');
|
||||
|
||||
$folder = null;
|
||||
try {
|
||||
$folder = $this->appData->getFolder('app-discover-cache');
|
||||
$this->cleanUpImageCache($folder, $etag);
|
||||
} catch (\Throwable $e) {
|
||||
$folder = $this->appData->newFolder('app-discover-cache');
|
||||
}
|
||||
|
||||
// Get the current cache folder
|
||||
try {
|
||||
$folder = $folder->getFolder($etag);
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $folder->newFolder($etag);
|
||||
}
|
||||
|
||||
$info = pathinfo($fileName);
|
||||
$hashName = md5($fileName);
|
||||
$allFiles = $folder->getDirectoryListing();
|
||||
// Try to find the file
|
||||
$file = array_filter($allFiles, function (ISimpleFile $file) use ($hashName) {
|
||||
return str_starts_with($file->getName(), $hashName);
|
||||
});
|
||||
// Get the first entry
|
||||
$file = reset($file);
|
||||
// If not found request from Web
|
||||
if ($file === false) {
|
||||
$user = $session->getUser();
|
||||
// this route is not public thus we can assume a user is logged-in
|
||||
assert($user !== null);
|
||||
// Register a user request to throttle fetching external data
|
||||
// this will prevent using the server for DoS of other systems.
|
||||
$limiter->registerUserRequest(
|
||||
'settings-discover-media',
|
||||
// allow up to 24 media requests per hour
|
||||
// this should be a sane default when a completely new section is loaded
|
||||
// keep in mind browsers request all files from a source-set
|
||||
24,
|
||||
60 * 60,
|
||||
$user,
|
||||
);
|
||||
|
||||
if (!$this->checkCanDownloadMedia($fileName)) {
|
||||
$this->logger->warning('Tried to load media files for app discover section from untrusted source');
|
||||
return new NotFoundResponse(Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->clientService->newClient();
|
||||
$fileResponse = $client->get($fileName);
|
||||
$contentType = $fileResponse->getHeader('Content-Type');
|
||||
$extension = $info['extension'] ?? '';
|
||||
$file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody());
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]);
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
} else {
|
||||
// File was found so we can get the content type from the file name
|
||||
$contentType = base64_decode(explode('.', $file->getName())[1] ?? '');
|
||||
}
|
||||
|
||||
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]);
|
||||
// cache for 7 days
|
||||
$response->cacheFor(604800, false, true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function checkCanDownloadMedia(string $filename): bool {
|
||||
$urlInfo = parse_url($filename);
|
||||
if (!isset($urlInfo['host']) || !isset($urlInfo['path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always allowed hosts
|
||||
if ($urlInfo['host'] === 'nextcloud.com') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hosts that need further verification
|
||||
// Github is only allowed if from our organization
|
||||
$ALLOWED_HOSTS = ['github.com', 'raw.githubusercontent.com'];
|
||||
if (!in_array($urlInfo['host'], $ALLOWED_HOSTS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_starts_with($urlInfo['path'], '/nextcloud/') || str_starts_with($urlInfo['path'], '/nextcloud-gmbh/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove orphaned folders from the image cache that do not match the current etag
|
||||
* @param ISimpleFolder $folder The folder to clear
|
||||
* @param string $etag The etag (directory name) to keep
|
||||
*/
|
||||
private function cleanUpImageCache(ISimpleFolder $folder, string $etag): void {
|
||||
// Cleanup old cache folders
|
||||
$allFiles = $folder->getDirectoryListing();
|
||||
foreach ($allFiles as $dir) {
|
||||
try {
|
||||
if ($dir->getName() !== $etag) {
|
||||
$dir->delete();
|
||||
}
|
||||
} catch (NotPermittedException $e) {
|
||||
// ignore folder for now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Appstore\Controller;
|
||||
|
||||
use OC\App\AppManager;
|
||||
use OC\App\AppStore\Bundles\BundleFetcher;
|
||||
use OC\Installer;
|
||||
use OCA\AppAPI\Service\ExAppsPageService;
|
||||
use OCA\Appstore\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Server;
|
||||
use OCP\Util;
|
||||
|
||||
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
|
||||
class PageController extends Controller {
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private IL10N $l10n,
|
||||
private IConfig $config,
|
||||
private Installer $installer,
|
||||
private AppManager $appManager,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private IInitialState $initialState,
|
||||
private BundleFetcher $bundleFetcher,
|
||||
private INavigationManager $navigationManager,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass AppAPI is shipped since 30.0.1
|
||||
*
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
#[NoCSRFRequired]
|
||||
#[FrontpageRoute('GET', '/settings/apps', root: '')]
|
||||
#[FrontpageRoute('GET', '/settings/apps/{category}', defaults: ['category' => ''], root: '')]
|
||||
#[FrontpageRoute('GET', '/settings/apps/{category}/{id}', defaults: ['category' => '', 'id' => ''], root: '')]
|
||||
public function viewApps(): TemplateResponse {
|
||||
$this->navigationManager->setActiveEntry('core_apps');
|
||||
|
||||
$this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true));
|
||||
$this->initialState->provideInitialState('appstoreBundles', $this->getBundles());
|
||||
$this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual'));
|
||||
$this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates()));
|
||||
|
||||
if ($this->appManager->isEnabledForAnyone('app_api')) {
|
||||
try {
|
||||
Server::get(ExAppsPageService::class)->provideAppApiState($this->initialState);
|
||||
} catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
|
||||
|
||||
$templateResponse = new TemplateResponse(Application::APP_ID, 'empty', ['pageTitle' => $this->l10n->t('App store')]);
|
||||
$templateResponse->setContentSecurityPolicy($policy);
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'main');
|
||||
Util::addScript(Application::APP_ID, 'main');
|
||||
|
||||
return $templateResponse;
|
||||
}
|
||||
|
||||
|
||||
private function getAppsWithUpdates() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach ($apps as $key => $app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
if ($newVersion === false) {
|
||||
unset($apps[$key]);
|
||||
}
|
||||
}
|
||||
return $apps;
|
||||
}
|
||||
|
||||
private function getBundles() {
|
||||
$result = [];
|
||||
$bundles = $this->bundleFetcher->getBundles();
|
||||
foreach ($bundles as $bundle) {
|
||||
$result[] = [
|
||||
'name' => $bundle->getName(),
|
||||
'id' => $bundle->getIdentifier(),
|
||||
'appIdentifiers' => $bundle->getAppIdentifiers()
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "settings-administration",
|
||||
"version": "0.0.1",
|
||||
"description": "Nextcloud settings",
|
||||
"license": {
|
||||
"name": "agpl"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"basic_auth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
},
|
||||
"bearer_auth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {}
|
||||
},
|
||||
"paths": {
|
||||
"/index.php/settings/admin/log/download": {
|
||||
"get": {
|
||||
"operationId": "log_settings-download",
|
||||
"summary": "download logfile",
|
||||
"description": "This endpoint requires admin access",
|
||||
"tags": [
|
||||
"log_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logfile returned",
|
||||
"headers": {
|
||||
"Content-Disposition": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"attachment; filename=\"nextcloud.log\""
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Logged in account must be an admin",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -1,709 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "settings-full",
|
||||
"version": "0.0.1",
|
||||
"description": "Nextcloud settings",
|
||||
"license": {
|
||||
"name": "agpl"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"basic_auth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
},
|
||||
"bearer_auth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"DeclarativeForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"priority",
|
||||
"section_type",
|
||||
"section_id",
|
||||
"storage_type",
|
||||
"title",
|
||||
"app",
|
||||
"fields"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"section_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"admin",
|
||||
"personal"
|
||||
]
|
||||
},
|
||||
"section_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"internal",
|
||||
"external"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"doc_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DeclarativeFormField"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeclarativeFormField": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"type",
|
||||
"default",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"text",
|
||||
"password",
|
||||
"email",
|
||||
"tel",
|
||||
"url",
|
||||
"number",
|
||||
"checkbox",
|
||||
"multi-checkbox",
|
||||
"radio",
|
||||
"select",
|
||||
"multi-select"
|
||||
]
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {
|
||||
"type": "object"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sensitive": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OCSMeta": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"statuscode"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"statuscode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"totalitems": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemsperpage": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/index.php/settings/admin/log/download": {
|
||||
"get": {
|
||||
"operationId": "log_settings-download",
|
||||
"summary": "download logfile",
|
||||
"description": "This endpoint requires admin access",
|
||||
"tags": [
|
||||
"log_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logfile returned",
|
||||
"headers": {
|
||||
"Content-Disposition": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"attachment; filename=\"nextcloud.log\""
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Logged in account must be an admin",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/settings/api/declarative/value": {
|
||||
"post": {
|
||||
"operationId": "declarative_settings-set-value",
|
||||
"summary": "Sets a declarative settings value",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"app",
|
||||
"formId",
|
||||
"fieldId",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "ID of the app"
|
||||
},
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "ID of the form"
|
||||
},
|
||||
"fieldId": {
|
||||
"type": "string",
|
||||
"description": "ID of the field"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Value to be saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Value set successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Not logged in or not an admin user",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid arguments to save value",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/settings/api/declarative/value-sensitive": {
|
||||
"post": {
|
||||
"operationId": "declarative_settings-set-sensitive-value",
|
||||
"summary": "Sets a declarative settings value. Password confirmation is required for sensitive values.",
|
||||
"description": "This endpoint requires password confirmation",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"app",
|
||||
"formId",
|
||||
"fieldId",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "ID of the app"
|
||||
},
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "ID of the form"
|
||||
},
|
||||
"fieldId": {
|
||||
"type": "string",
|
||||
"description": "ID of the field"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Value to be saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Value set successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Not logged in or not an admin user",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid arguments to save value",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/settings/api/declarative/forms": {
|
||||
"get": {
|
||||
"operationId": "declarative_settings-get-forms",
|
||||
"summary": "Gets all declarative forms with the values prefilled.",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Forms returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DeclarativeForm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -1,632 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "settings",
|
||||
"version": "0.0.1",
|
||||
"description": "Nextcloud settings",
|
||||
"license": {
|
||||
"name": "agpl"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"basic_auth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
},
|
||||
"bearer_auth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"DeclarativeForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"priority",
|
||||
"section_type",
|
||||
"section_id",
|
||||
"storage_type",
|
||||
"title",
|
||||
"app",
|
||||
"fields"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"section_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"admin",
|
||||
"personal"
|
||||
]
|
||||
},
|
||||
"section_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"internal",
|
||||
"external"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"doc_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DeclarativeFormField"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeclarativeFormField": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"type",
|
||||
"default",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"text",
|
||||
"password",
|
||||
"email",
|
||||
"tel",
|
||||
"url",
|
||||
"number",
|
||||
"checkbox",
|
||||
"multi-checkbox",
|
||||
"radio",
|
||||
"select",
|
||||
"multi-select"
|
||||
]
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {
|
||||
"type": "object"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sensitive": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OCSMeta": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"statuscode"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"statuscode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"totalitems": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemsperpage": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/ocs/v2.php/settings/api/declarative/value": {
|
||||
"post": {
|
||||
"operationId": "declarative_settings-set-value",
|
||||
"summary": "Sets a declarative settings value",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"app",
|
||||
"formId",
|
||||
"fieldId",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "ID of the app"
|
||||
},
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "ID of the form"
|
||||
},
|
||||
"fieldId": {
|
||||
"type": "string",
|
||||
"description": "ID of the field"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Value to be saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Value set successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Not logged in or not an admin user",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid arguments to save value",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/settings/api/declarative/value-sensitive": {
|
||||
"post": {
|
||||
"operationId": "declarative_settings-set-sensitive-value",
|
||||
"summary": "Sets a declarative settings value. Password confirmation is required for sensitive values.",
|
||||
"description": "This endpoint requires password confirmation",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"app",
|
||||
"formId",
|
||||
"fieldId",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "ID of the app"
|
||||
},
|
||||
"formId": {
|
||||
"type": "string",
|
||||
"description": "ID of the form"
|
||||
},
|
||||
"fieldId": {
|
||||
"type": "string",
|
||||
"description": "ID of the field"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Value to be saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Value set successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Not logged in or not an admin user",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid arguments to save value",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/settings/api/declarative/forms": {
|
||||
"get": {
|
||||
"operationId": "declarative_settings-get-forms",
|
||||
"summary": "Gets all declarative forms with the values prefilled.",
|
||||
"tags": [
|
||||
"declarative_settings"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Forms returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DeclarativeForm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -1,61 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
|
||||
import NcContent from '@nextcloud/vue/components/NcContent'
|
||||
import AppstoreNavigation from './views/AppstoreNavigation.vue'
|
||||
import AppstoreSidebar from './views/AppstoreSidebar.vue'
|
||||
import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const currentCategory = computed(() => {
|
||||
if (route.params.category) {
|
||||
return [route.params.category].flat()[0]!
|
||||
}
|
||||
if (route.name === 'apps-bundles') {
|
||||
return 'bundles'
|
||||
}
|
||||
return 'discover'
|
||||
})
|
||||
const heading = computed(() => APPSTORE_CATEGORY_NAMES[currentCategory.value] ?? currentCategory.value)
|
||||
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
|
||||
|
||||
const showSidebar = computed(() => !!route.params.id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcContent appName="appstore">
|
||||
<AppstoreNavigation />
|
||||
<NcAppContent
|
||||
:class="$style.appstoreApp__content"
|
||||
:pageHeading="t('appstore', 'App store')"
|
||||
:pageTitle>
|
||||
<h2 v-if="heading" :class="$style.appstoreApp__heading">
|
||||
{{ heading }}
|
||||
</h2>
|
||||
<router-view />
|
||||
</NcAppContent>
|
||||
<AppstoreSidebar v-if="showSidebar" />
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appstoreApp__content {
|
||||
padding-inline-end: var(--body-container-margin);
|
||||
}
|
||||
|
||||
.appstoreApp__heading {
|
||||
margin-block-start: var(--app-navigation-padding);
|
||||
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
|
||||
min-height: var(--default-clickable-area);
|
||||
line-height: var(--default-clickable-area);
|
||||
vertical-align: center;
|
||||
}
|
||||
</style>
|
||||
Vendored
-112
@@ -1,112 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper for localized values
|
||||
*/
|
||||
export type ILocalizedValue<T> = Record<string, T | undefined> & { en: T }
|
||||
|
||||
export interface IAppDiscoverElement {
|
||||
/**
|
||||
* Type of the element
|
||||
*/
|
||||
type: typeof APP_DISCOVER_KNOWN_TYPES[number]
|
||||
|
||||
/**
|
||||
* Identifier for this element
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Order of this element to pin elements (smaller = shown on top)
|
||||
*/
|
||||
order?: number
|
||||
|
||||
/**
|
||||
* Optional, localized, headline for the element
|
||||
*/
|
||||
headline?: ILocalizedValue<string>
|
||||
|
||||
/**
|
||||
* Optional link target for the element
|
||||
*/
|
||||
link?: string
|
||||
|
||||
/**
|
||||
* Optional date when this element will get valid (only show since then)
|
||||
*/
|
||||
date?: number
|
||||
|
||||
/**
|
||||
* Optional date when this element will be invalid (only show until then)
|
||||
*/
|
||||
expiryDate?: number
|
||||
}
|
||||
|
||||
/** Wrapper for media source and MIME type */
|
||||
type MediaSource = { src: string, mime: string }
|
||||
|
||||
/**
|
||||
* Media content type for posts
|
||||
*/
|
||||
interface IAppDiscoverMediaContent {
|
||||
/**
|
||||
* The media source to show - either one or a list of sources with their MIME type for fallback options
|
||||
*/
|
||||
src: MediaSource | MediaSource[]
|
||||
|
||||
/**
|
||||
* Alternative text for the media
|
||||
*/
|
||||
alt: string
|
||||
|
||||
/**
|
||||
* Optional link target for the media (e.g. to the full video)
|
||||
*/
|
||||
link?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for post media
|
||||
*/
|
||||
interface IAppDiscoverMedia {
|
||||
/**
|
||||
* The alignment of the media element
|
||||
*/
|
||||
alignment?: 'start' | 'end' | 'center'
|
||||
|
||||
/**
|
||||
* The (localized) content
|
||||
*/
|
||||
content: ILocalizedValue<IAppDiscoverMediaContent>
|
||||
}
|
||||
|
||||
/**
|
||||
* An app element only used for the showcase type
|
||||
*/
|
||||
export interface IAppDiscoverApp {
|
||||
/** The App ID */
|
||||
type: 'app'
|
||||
appId: string
|
||||
}
|
||||
|
||||
export interface IAppDiscoverPost extends IAppDiscoverElement {
|
||||
type: 'post'
|
||||
text?: ILocalizedValue<string>
|
||||
media?: IAppDiscoverMedia
|
||||
}
|
||||
|
||||
export interface IAppDiscoverShowcase extends IAppDiscoverElement {
|
||||
type: 'showcase'
|
||||
content: (IAppDiscoverPost | IAppDiscoverApp)[]
|
||||
}
|
||||
|
||||
export interface IAppDiscoverCarousel extends IAppDiscoverElement {
|
||||
type: 'carousel'
|
||||
text?: ILocalizedValue<string>
|
||||
content: IAppDiscoverPost[]
|
||||
}
|
||||
|
||||
export type IAppDiscoverElements = IAppDiscoverPost | IAppDiscoverCarousel | IAppDiscoverShowcase
|
||||
@@ -1,64 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
|
||||
|
||||
import { mdiCogOutline } from '@mdi/js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
|
||||
const { app, noFallback, size = 20 } = defineProps<{
|
||||
app: IAppstoreApp | IAppstoreExApp
|
||||
noFallback?: boolean
|
||||
size?: number
|
||||
}>()
|
||||
|
||||
const isSvg = computed(() => app.icon?.endsWith('.svg'))
|
||||
const svgIcon = ref<string>('')
|
||||
watch(() => app.icon, async () => {
|
||||
svgIcon.value = ''
|
||||
if (app.icon?.endsWith('.svg')) {
|
||||
const response = await fetch(app.icon)
|
||||
if (response.ok) {
|
||||
svgIcon.value = await response.text()
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="$style.appIcon">
|
||||
<NcIconSvgWrapper
|
||||
v-if="svgIcon"
|
||||
:size
|
||||
:svg="svgIcon" />
|
||||
<img
|
||||
v-else-if="app.icon && !isSvg"
|
||||
:class="$style.appIcon__image"
|
||||
alt=""
|
||||
:src="app.icon"
|
||||
:height="size"
|
||||
:width="size">
|
||||
<NcIconSvgWrapper
|
||||
v-else-if="!noFallback"
|
||||
:path="mdiCogOutline"
|
||||
:size />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appIcon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.appImage__image {
|
||||
filter: var(--invert-if-dark);
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
|
||||
|
||||
import { mdiCogOutline } from '@mdi/js'
|
||||
import { NcLoadingIcon } from '@nextcloud/vue'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
|
||||
const props = defineProps<{
|
||||
app: IAppstoreApp | IAppstoreExApp
|
||||
}>()
|
||||
|
||||
const isError = ref(false)
|
||||
const isLoading = ref(true)
|
||||
watchEffect(() => {
|
||||
if (props.app.screenshot) {
|
||||
isError.value = false
|
||||
isLoading.value = true
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
image.onerror = () => {
|
||||
isError.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
image.src = props.app.screenshot
|
||||
} else {
|
||||
isLoading.value = false
|
||||
isError.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.appImage">
|
||||
<NcIconSvgWrapper
|
||||
v-if="isError || !props.app.screenshot"
|
||||
:size="80"
|
||||
:path="mdiCogOutline" />
|
||||
|
||||
<NcLoadingIcon v-else-if="isLoading" :size="80" />
|
||||
|
||||
<img :class="$style.appImage__image" :src="props.app.screenshot" alt="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appImage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.appImage__image {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* This component either shows a native link to the installed app or external size
|
||||
* or a router link to the appstore page of the app if not installed
|
||||
*/
|
||||
|
||||
import type { RouterLinkProps } from 'vue-router'
|
||||
import type { INavigationEntry } from '../../../../core/src/types/navigation.d.ts'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
href: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const knownRoutes = Object.fromEntries(loadState<INavigationEntry[]>('core', 'apps').map((app) => [app.app ?? app.id, app.href]))
|
||||
|
||||
const routerProps = ref<RouterLinkProps>()
|
||||
const linkProps = ref<Record<string, string>>()
|
||||
|
||||
watchEffect(() => {
|
||||
const match = props.href.match(/^app:(\/\/)?([^/]+)(\/.+)?$/)
|
||||
routerProps.value = undefined
|
||||
linkProps.value = undefined
|
||||
|
||||
// not an app url
|
||||
if (match === null) {
|
||||
linkProps.value = {
|
||||
href: props.href,
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const appId = match[2]!
|
||||
// Check if specific route was requested
|
||||
if (match[3]) {
|
||||
// we do no know anything about app internal path so we only allow generic app paths
|
||||
linkProps.value = {
|
||||
href: generateUrl(`/apps/${appId}${match[3]}`),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we know any route for that app we open it
|
||||
if (appId in knownRoutes) {
|
||||
linkProps.value = {
|
||||
href: knownRoutes[appId]!,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to show the app store entry
|
||||
routerProps.value = {
|
||||
to: {
|
||||
name: 'apps-details',
|
||||
params: {
|
||||
category: route.params?.category ?? 'discover',
|
||||
id: appId,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a v-if="linkProps" v-bind="linkProps">
|
||||
<slot />
|
||||
</a>
|
||||
<RouterLink v-else-if="routerProps" v-bind="routerProps">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
@@ -1,108 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<li
|
||||
:class="[$style.appListItem, {
|
||||
[$style.appListItem_selected]: isSelected,
|
||||
}]">
|
||||
<div class="app-image app-image-icon">
|
||||
<div v-if="!app?.app_api && !props.app.preview" class="icon-settings-dark" />
|
||||
<NcIconSvgWrapper
|
||||
v-else-if="app.app_api && !props.app.preview"
|
||||
:path="mdiCogOutline"
|
||||
:size="24"
|
||||
style="min-width: auto; min-height: auto; height: 100%;" />
|
||||
|
||||
<svg
|
||||
v-else-if="app.preview && !app.app_api"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32">
|
||||
<image
|
||||
x="0"
|
||||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
:xlink:href="app.preview"
|
||||
class="app-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-name">
|
||||
<router-link
|
||||
class="app-name--link"
|
||||
:to="{
|
||||
name: 'apps-details',
|
||||
params: {
|
||||
category: category,
|
||||
id: app.id,
|
||||
},
|
||||
}"
|
||||
:aria-label="t('appstore', 'Show details for {appName} app', { appName: app.name })">
|
||||
{{ app.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<AppListVersion :app />
|
||||
<div class="app-level">
|
||||
<AppLevelBadge :level="app.level" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp } from '../../apps.d.ts'
|
||||
|
||||
import { mdiCogOutline } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import AppLevelBadge from './AppLevelBadge.vue'
|
||||
import AppListVersion from './AppListVersion.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
app: IAppstoreApp
|
||||
category: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const isSelected = ref(false)
|
||||
watchEffect(() => {
|
||||
isSelected.value = props.app.id === route.params.id
|
||||
})
|
||||
|
||||
const screenshotLoaded = ref(false)
|
||||
watchEffect(() => {
|
||||
if (props.app.screenshot) {
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
screenshotLoaded.value = true
|
||||
}
|
||||
image.src = props.app.screenshot
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.appListItem {
|
||||
--app-item-padding: calc(var(--default-grid-baseline) * 2);
|
||||
--app-item-height: calc(var(--default-clickable-area) + var(--app-item-padding) * 2);
|
||||
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--app-item-padding);
|
||||
height: var(--app-item-height);
|
||||
}
|
||||
}
|
||||
|
||||
.appListItem:hover {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
|
||||
.appListItem_selected {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp } from '../../apps.d.ts'
|
||||
|
||||
defineProps<{
|
||||
app: IAppstoreApp
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.appListVersion">
|
||||
<span v-if="app.version">{{ app.version }}</span>
|
||||
<span v-else-if="app.appstoreData?.releases[0]?.version">
|
||||
{{ app.appstoreData.releases[0].version }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appListVersion {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp, IAppstoreExApp } from '../../apps.d.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { computed, useTemplateRef } from 'vue'
|
||||
import AppTableRow from './AppTableRow.vue'
|
||||
|
||||
defineProps<{
|
||||
apps: (IAppstoreApp | IAppstoreExApp)[]
|
||||
}>()
|
||||
|
||||
const tableElement = useTemplateRef('table')
|
||||
const { width: tableWidth } = useElementSize(tableElement)
|
||||
|
||||
const isNarrow = computed(() => tableWidth.value < 768)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table ref="table" :class="$style.appTable">
|
||||
<thead hidden>
|
||||
<tr>
|
||||
<th>{{ t('appstore', 'App name') }}</th>
|
||||
<th>{{ t('appstore', 'Version') }}</th>
|
||||
<th v-if="!isNarrow">
|
||||
{{ t('appstore', 'Support level') }}
|
||||
</th>
|
||||
<th>{{ t('appstore', 'Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<AppTableRow
|
||||
v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app
|
||||
:isNarrow />
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appTable {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreApp, IAppstoreExApp } from '../../apps.d.ts'
|
||||
|
||||
import { mdiInformationOutline } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActionRouter from '@nextcloud/vue/components/NcActionRouter'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import AppDaemonBadge from '../AppDaemonBadge.vue'
|
||||
import AppIcon from '../AppIcon.vue'
|
||||
import AppLevelBadge from '../AppLevelBadge.vue'
|
||||
import { useActions } from '../../composables/useActions.ts'
|
||||
|
||||
const { app, isNarrow } = defineProps<{
|
||||
app: IAppstoreApp | IAppstoreExApp
|
||||
isNarrow?: boolean
|
||||
}>()
|
||||
|
||||
const actions = useActions(() => app)
|
||||
const inlineActions = computed(() => {
|
||||
if (actions.value.length === 1) {
|
||||
return [...actions.value]
|
||||
}
|
||||
if (isNarrow) {
|
||||
return []
|
||||
}
|
||||
return actions.value.slice(0, 1)
|
||||
.filter((action) => action.inline !== false)
|
||||
})
|
||||
const menuActions = computed(() => actions.value.slice(inlineActions.value.length))
|
||||
|
||||
const route = useRoute()
|
||||
const detailsRoute = computed(() => ({
|
||||
name: route.name!,
|
||||
params: {
|
||||
...route.params,
|
||||
id: app.id,
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr :class="$style.appTableRow">
|
||||
<td :class="$style.appTableRow__nameCell">
|
||||
<NcButton
|
||||
alignment="start"
|
||||
:title="t('appstore', 'Show details')"
|
||||
:to="detailsRoute"
|
||||
variant="tertiary-no-background"
|
||||
wide>
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="app.loading" :size="24" />
|
||||
<AppIcon v-else :app :size="24" />
|
||||
</template>
|
||||
{{ app.name }}
|
||||
<span v-if="app.loading" class="hidden-visually">({{ t('appstore', 'is loading…') }})</span>
|
||||
<span class="hidden-visually">({{ t('appstore', 'Show details') }})</span>
|
||||
</NcButton>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="$style.appTableRow__versionCell">{{ app.version }}</span>
|
||||
</td>
|
||||
<td v-if="!isNarrow">
|
||||
<div :class="$style.appTableRow__levelCell">
|
||||
<AppLevelBadge v-if="app.level" :level="app.level" />
|
||||
<AppDaemonBadge v-if="'daemon' in app && app.daemon" :daemon="app.daemon" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div :class="$style.appTableRow__actionsCell">
|
||||
<NcButton
|
||||
v-for="action in inlineActions"
|
||||
:key="action.id"
|
||||
:ariaLabel="isNarrow ? action.label(app) : undefined"
|
||||
:title="isNarrow ? action.label(app) : undefined"
|
||||
:variant="action.variant"
|
||||
@click="action.callback(app)">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="action.icon" />
|
||||
</template>
|
||||
<template v-if="!isNarrow" #default>
|
||||
{{ action.label(app) }}
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcActions forceMenu>
|
||||
<NcActionButton
|
||||
v-for="action in menuActions"
|
||||
:key="action.id"
|
||||
closeAfterClick
|
||||
:variant="action.variant"
|
||||
@click="action.callback(app)">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="action.icon" />
|
||||
</template>
|
||||
{{ action.label(app) }}
|
||||
</NcActionButton>
|
||||
<NcActionRouter closeAfterClick :to="detailsRoute">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiInformationOutline" />
|
||||
</template>
|
||||
{{ t('appstore', 'Show details') }}
|
||||
</NcActionRouter>
|
||||
</NcActions>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.appTableRow {
|
||||
height: calc(var(--default-clickable-area) + var(--default-grid-baseline));
|
||||
}
|
||||
|
||||
.appTableRow td {
|
||||
padding-block: var(--default-grid-baseline);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.appTableRow__nameCell {
|
||||
/* Padding is needed to have proper focus-visible */
|
||||
padding-inline: var(--default-grid-baseline);
|
||||
}
|
||||
|
||||
.appTableRow__levelCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--default-grid-baseline)
|
||||
}
|
||||
|
||||
.appTableRow__versionCell {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.appTableRow__actionsCell {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppstoreExApp, IDeployDaemon } from '../../apps.d.ts'
|
||||
|
||||
import { mdiFormatListBulleted } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import DaemonSelectionDialogList from './DaemonSelectionDialogList.vue'
|
||||
import { useExAppsStore } from '../../store/exApps.ts'
|
||||
|
||||
defineProps<{
|
||||
/**
|
||||
* The app to enable
|
||||
*/
|
||||
app: IAppstoreExApp
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
close: [daemon?: IDeployDaemon]
|
||||
}>()
|
||||
|
||||
const store = useExAppsStore()
|
||||
const appApiAdminPage = generateUrl('/settings/admin/app_api')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcDialog
|
||||
:name="t('appstore', 'Choose Deploy Daemon for {appName}', { appName: app.name })"
|
||||
size="normal"
|
||||
@update:open="$event || $emit('close')">
|
||||
<NcEmptyContent
|
||||
v-if="store.dockerDaemons.length > 0"
|
||||
class="daemon-selection-list__empty-content"
|
||||
:name="t('appstore', 'No Deploy daemons configured')"
|
||||
:description="t('appstore', 'Register a custom one or setup from available templates')">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiFormatListBulleted" />
|
||||
</template>
|
||||
<template #action>
|
||||
<NcButton :href="appApiAdminPage">
|
||||
{{ t('appstore', 'Manage Deploy daemons') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<DaemonSelectionDialogList
|
||||
v-else
|
||||
:app="app"
|
||||
@selected="$emit('close', $event)" />
|
||||
</NcDialog>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDeployDaemon } from '../../apps.d.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import DaemonSelectionDialogListEntry from './DaemonSelectionDialogListEntry.vue'
|
||||
import { useExAppsStore } from '../../store/exApps.ts'
|
||||
|
||||
defineEmits<{
|
||||
selected: [daemon: IDeployDaemon]
|
||||
}>()
|
||||
|
||||
const store = useExAppsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
:class="$style.DaemonSelectionDialogList"
|
||||
:aria-label="t('appstore', 'Registered Deploy daemons list')">
|
||||
<DaemonSelectionDialogListEntry
|
||||
v-for="daemon in store.dockerDaemons"
|
||||
:key="daemon.id"
|
||||
:daemon
|
||||
:isDefault="store.defaultDaemon!.name === daemon.name"
|
||||
@selected="$emit('selected', daemon)" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.DaemonSelectionDialogList {
|
||||
max-height: 350px;
|
||||
overflow-y: scroll;
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDeployDaemon } from '../../apps.d.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import NcListItem from '@nextcloud/vue/components/NcListItem'
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* The daemon to use
|
||||
*/
|
||||
daemon: IDeployDaemon
|
||||
/**
|
||||
* Whether this daemon is the default one
|
||||
*/
|
||||
isDefault: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selected: []
|
||||
}>()
|
||||
|
||||
const itemTitle = computed(() => `${props.daemon.name} - ${props.daemon.display_name}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcListItem
|
||||
:active="isDefault"
|
||||
:counterNumber="daemon.exAppsCount"
|
||||
counterType="highlighted"
|
||||
:details="isDefault ? t('appstore', 'Default') : ''"
|
||||
forceDisplayActions
|
||||
:name="itemTitle"
|
||||
@click.stop="emit('selected')">
|
||||
<template #subname>
|
||||
{{ daemon.accepts_deploy_id }}
|
||||
</template>
|
||||
</NcListItem>
|
||||
</template>
|
||||
@@ -1,33 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { cleanup, render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import MarkdownPreview from './MarkdownPreview.vue'
|
||||
|
||||
describe('MarkdownPreview component', () => {
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('renders', () => {
|
||||
const component = render(MarkdownPreview, {
|
||||
props: {
|
||||
minHeadingLevel: 2,
|
||||
text: `# Heading one
|
||||
This is [a link](http://example.com)!
|
||||
## Heading two
|
||||
> This is a block quote
|
||||
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(component.getByRole('heading', { level: 2, name: 'Heading one' })).toBeTruthy()
|
||||
expect(component.getByRole('heading', { level: 3, name: 'Heading two' })).toBeTruthy()
|
||||
expect(component.getByText('This is a block quote')).toBeInstanceOf(HTMLQuoteElement)
|
||||
expect(component.getByRole('link', { name: 'a link' })).toBeInstanceOf(HTMLAnchorElement)
|
||||
expect(component.getByRole('link', { name: 'a link' }).getAttribute('href')).toBe('http://example.com')
|
||||
expect(() => component.getByRole('img')).toThrow() // its a text
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '../composables/useMarkdown.ts'
|
||||
|
||||
const {
|
||||
text,
|
||||
minHeadingLevel = 1,
|
||||
} = defineProps<{
|
||||
/**
|
||||
* The markdown text to render
|
||||
*/
|
||||
text: string
|
||||
/**
|
||||
* Limit the minimum heading level
|
||||
*/
|
||||
minHeadingLevel?: number
|
||||
}>()
|
||||
|
||||
const renderMarkdown = useMarkdown(() => text, { minHeadingLevel })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="settings-markdown" v-html="renderMarkdown" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-markdown :deep {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
&::after {
|
||||
content: '↗';
|
||||
padding-inline: calc(var(--default-grid-baseline) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1em 1.3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
p code {
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: var(--border-radius);
|
||||
padding: .1em .3em;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-inline-start: 10px;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ul > li > ul > li {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
ul > li > ul > li ul li {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-inline-start: 1em;
|
||||
border-inline-start: 4px solid var(--color-primary-element);
|
||||
color: var(--color-text-maxcontrast);
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,125 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
|
||||
|
||||
import { mdiAlertCircleCheckOutline, mdiCheck, mdiClose, mdiDownload, mdiTrashCanOutline, mdiUpdate } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useAppsStore } from '../store/apps.ts'
|
||||
import { useUpdatesStore } from '../store/updates.ts'
|
||||
import { canDisable, canEnable, canInstall, canUninstall, canUpdate, needForceEnable } from '../utils/appStatus.ts'
|
||||
|
||||
type AppAction = {
|
||||
id: string
|
||||
icon: string
|
||||
label: (app: IAppstoreApp | IAppstoreExApp) => string
|
||||
callback: (app: IAppstoreApp | IAppstoreExApp) => Promise<void>
|
||||
variant?: 'primary' | 'error' | 'warning'
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
const AppAction = Object.freeze({
|
||||
INSTALL: {
|
||||
id: 'install',
|
||||
icon: mdiDownload,
|
||||
label: (app: IAppstoreApp | IAppstoreExApp) => {
|
||||
if (app.app_api) {
|
||||
return t('appstore', 'Deploy and enable')
|
||||
}
|
||||
if (app.needsDownload) {
|
||||
return t('appstore', 'Download and enable')
|
||||
}
|
||||
return t('appstore', 'Install and enable')
|
||||
},
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useAppsStore()
|
||||
await store.enableApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
ENABLE: {
|
||||
id: 'enable',
|
||||
icon: mdiCheck,
|
||||
variant: 'primary',
|
||||
label: () => t('appstore', 'Enable'),
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useAppsStore()
|
||||
await store.enableApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
FORCE_ENABLE: {
|
||||
id: 'force-enable',
|
||||
icon: mdiAlertCircleCheckOutline,
|
||||
inline: false,
|
||||
label: () => t('appstore', 'Force enable'),
|
||||
variant: 'warning',
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useAppsStore()
|
||||
await store.forceEnableApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
DISABLE: {
|
||||
id: 'disable',
|
||||
icon: mdiClose,
|
||||
label: () => t('appstore', 'Disable'),
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useAppsStore()
|
||||
await store.disableApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
REMOVE: {
|
||||
id: 'remove',
|
||||
icon: mdiTrashCanOutline,
|
||||
variant: 'error',
|
||||
inline: false,
|
||||
label: () => t('appstore', 'Remove'),
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useAppsStore()
|
||||
await store.uninstallApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
UPDATE: {
|
||||
id: 'update',
|
||||
icon: mdiUpdate,
|
||||
variant: 'primary',
|
||||
label: (app: IAppstoreApp | IAppstoreExApp) => t('appstore', 'Update to {version}', { version: app.update! }),
|
||||
async callback(app: IAppstoreApp | IAppstoreExApp) {
|
||||
const store = useUpdatesStore()
|
||||
await store.updateApp(app.id)
|
||||
},
|
||||
} as AppAction,
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the available actions for an app
|
||||
*
|
||||
* @param app - The app to get the actions for
|
||||
*/
|
||||
export function useActions(app: MaybeRefOrGetter<IAppstoreApp | IAppstoreExApp>) {
|
||||
return computed(() => {
|
||||
const actions: typeof AppAction[keyof typeof AppAction][] = []
|
||||
if (canUpdate(toValue(app))) {
|
||||
actions.push(AppAction.UPDATE)
|
||||
}
|
||||
|
||||
if (canDisable(toValue(app))) {
|
||||
actions.push(AppAction.DISABLE)
|
||||
}
|
||||
|
||||
if (needForceEnable(toValue(app))) {
|
||||
actions.push(AppAction.FORCE_ENABLE)
|
||||
} else if (canInstall(toValue(app))) {
|
||||
actions.push(AppAction.INSTALL)
|
||||
} else if (canEnable(toValue(app))) {
|
||||
actions.push(AppAction.ENABLE)
|
||||
}
|
||||
|
||||
if (canUninstall(toValue(app))) {
|
||||
actions.push(AppAction.REMOVE)
|
||||
}
|
||||
return actions
|
||||
})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect, test } from 'vitest'
|
||||
import { useMarkdown } from './useMarkdown.ts'
|
||||
|
||||
test('renders links', () => {
|
||||
const rendered = useMarkdown('This is [a link](http://example.com)!')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<p>This is <a href="http://example.com" rel="noreferrer noopener">a link</a>!</p>\n"')
|
||||
})
|
||||
|
||||
test('removes links with invalid URL', () => {
|
||||
const rendered = useMarkdown('This is [a link](ftp://example.com)!')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<p>This is !</p>\n"')
|
||||
})
|
||||
|
||||
test('renders images', () => {
|
||||
const rendered = useMarkdown('')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<p>alt text</p>\n"')
|
||||
})
|
||||
|
||||
test('renders images with title', () => {
|
||||
const rendered = useMarkdown('')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<p>Title</p>\n"')
|
||||
})
|
||||
|
||||
test('renders images with alt text and title', () => {
|
||||
const rendered = useMarkdown('')
|
||||
expect(rendered.value).toMatchInlineSnapshot(`
|
||||
"<p>alt text</p>\n"
|
||||
`)
|
||||
})
|
||||
|
||||
test('renders block quotes', () => {
|
||||
const rendered = useMarkdown('> This is a block quote')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<blockquote>This is a block quote</blockquote>"')
|
||||
})
|
||||
|
||||
test('renders headings', () => {
|
||||
const rendered = useMarkdown('# level 1\n## level 2\n### level 3\n#### level 4\n##### level 5\n###### level 6\n')
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<h1>level 1</h1><h2>level 2</h2><h3>level 3</h3><h4>level 4</h4><h5>level 5</h5><h6>level 6</h6>"')
|
||||
})
|
||||
|
||||
test('renders headings with minHeadingLevel', () => {
|
||||
const rendered = useMarkdown(
|
||||
'# level 1\n## level 2\n### level 3\n#### level 4\n##### level 5\n###### level 6\n',
|
||||
{ minHeadingLevel: 4 },
|
||||
)
|
||||
expect(rendered.value).toMatchInlineSnapshot('"<h4>level 1</h4><h5>level 2</h5><h6>level 3</h6><h6>level 4</h6><h6>level 5</h6><h6>level 6</h6>"')
|
||||
})
|
||||
@@ -1,134 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Tokens } from 'marked'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import dompurify from 'dompurify'
|
||||
import { marked } from 'marked'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
export interface MarkdownOptions {
|
||||
minHeadingLevel?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Markdown to HTML
|
||||
*
|
||||
* @param text - The Markdown source
|
||||
* @param options - Markdown options
|
||||
*/
|
||||
export function useMarkdown(text: MaybeRefOrGetter<string>, options?: MarkdownOptions) {
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.blockquote = markedBlockquote
|
||||
renderer.link = markedLink
|
||||
renderer.image = markedImage
|
||||
|
||||
return computed(() => {
|
||||
const minHeading = options?.minHeadingLevel ?? 1
|
||||
renderer.heading = getMarkedHeading(minHeading)
|
||||
const markdown = toValue(text).trim()
|
||||
|
||||
return dompurify.sanitize(
|
||||
marked(markdown, {
|
||||
async: false,
|
||||
renderer,
|
||||
gfm: false,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
}),
|
||||
{
|
||||
ALLOWED_TAGS: [
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'strong',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'em',
|
||||
'del',
|
||||
'blockquote',
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom link renderer that only allows http and https links
|
||||
*
|
||||
* @param ctx - The render context
|
||||
* @param ctx.href - The link href
|
||||
* @param ctx.title - The link title
|
||||
* @param ctx.text - The link text
|
||||
*/
|
||||
function markedLink({ href, title, text }: Tokens.Link) {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(href)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let out = '<a href="' + href + '" rel="noreferrer noopener"'
|
||||
if (title) {
|
||||
out += ' title="' + title + '"'
|
||||
}
|
||||
out += '>' + text + '</a>'
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Only render image alt text or title
|
||||
*
|
||||
* @param ctx - The render context
|
||||
* @param ctx.title - The image title
|
||||
* @param ctx.text - The image alt text
|
||||
*/
|
||||
function markedImage({ title, text }: Tokens.Image): string {
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
return title ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Render block quotes without any special styling
|
||||
*
|
||||
* @param ctx - The render context
|
||||
* @param ctx.text - The blockquote text
|
||||
*/
|
||||
function markedBlockquote({ text }: Tokens.Blockquote): string {
|
||||
return `<blockquote>${text}</blockquote>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a custom heading renderer that clamps heading levels
|
||||
*
|
||||
* @param minHeading - The heading to clamp to
|
||||
*/
|
||||
function getMarkedHeading(minHeading: number) {
|
||||
/**
|
||||
* Custom heading renderer that adjusts heading levels
|
||||
*
|
||||
* @param ctx - The render context
|
||||
* @param ctx.text - The heading text
|
||||
* @param ctx.depth - The heading depth
|
||||
*/
|
||||
return ({ text, depth }: Tokens.Heading): string => {
|
||||
depth = Math.min(6, depth + (minHeading - 1))
|
||||
return `<h${depth}>${text}</h${depth}>`
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user