Compare commits

...

154 Commits

Author SHA1 Message Date
Ferdinand Thiessen e9a6556e44 fixup 2026-04-07 04:06:33 +02:00
Ferdinand Thiessen 2a61e0bd98 refactor: migrate app level badge to css modules
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:33 +02:00
Ferdinand Thiessen fc0fc06124 refactor: migrate app bundles to Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:33 +02:00
Ferdinand Thiessen 4cb22ad89f refactor: migrated app management to Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 6a674f449d refactor(appstore): migrate app management views to Vue 3 and Typescript
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 67afe77f1c refactor(appstore): migrate app discover section to Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 7482a3a0f3 refactor(appstore): migrate daemon selection dialog to Typescript and Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 89fe742a83 refactor(appstore): prepare Vue 3 migration
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen c320f30268 refactor(appstore): split controllers and use proper root
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 7c0d70dc98 refactor(appstore): migrate markdown component to Typescript
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:09 +02:00
Ferdinand Thiessen 3ad1cfd623 refactor: split appstore from settings app
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 04:06:08 +02:00
Ferdinand Thiessen 2a3321f1ff build: harden module name generation
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-07 00:04:05 +02:00
Nextcloud bot b012e5cc16 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-06 00:21:20 +00:00
Nextcloud bot 1afeb1c2d3 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-05 00:18:52 +00:00
Nextcloud bot 9a24eaa275 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-04 00:18:47 +00:00
github-actions[bot] fc18ce9cae Merge pull request #59414 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/lodash-4.18.1
chore(deps): Bump lodash from 4.17.23 to 4.18.1 in /build/frontend-legacy
2026-04-03 16:11:25 +00:00
nextcloud-command 137e94bec2 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-04-03 15:55:16 +00:00
dependabot[bot] 8c55673a25 chore(deps): Bump lodash in /build/frontend-legacy
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 17:47:41 +02:00
Ferdinand Thiessen 92e4c882fc Merge pull request #59415 from hamidout/fix/user-ldap-last-login-config-key
fix(user_ldap): fix lastLogin reading wrong appid and configkey
2026-04-03 15:00:44 +02:00
hamid b7d6822592 fix(user_ldap): fix lastLogin reading wrong appid and configkey
fetchDetails() was calling getValueInt($uid, 'user_ldap', 'email')
instead of getValueInt($uid, 'login', 'lastLogin'), causing lastLogin
to always return 0 for offline LDAP users.

Fixes #58421

Signed-off-by: hamid <hamid@webpick.net>
2026-04-03 13:06:49 +01:00
Nextcloud bot ae45f67a75 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-03 00:18:44 +00:00
Ferdinand Thiessen 521e61828f Merge pull request #59335 from mykh-hailo/fix/duplicate-dashboard-widget
fix: duplicate dashboard widget
2026-04-02 23:22:12 +02:00
mykh-hailo 708fa13428 fix: move sanitize directly to sanitizeLayout
Signed-off-by: mykh-hailo <kristianderonta0205@gmail.com>
2026-04-02 20:20:19 +02:00
mykh-hailo 5a8292fe43 fix: duplicate dashboard widget
Signed-off-by: mykh-hailo <kristianderonta0205@gmail.com>
2026-04-02 20:20:19 +02:00
Ferdinand Thiessen c0cab68f8b Merge pull request #59388 from nextcloud/fix/noid/files-search-default-limit
fix(file-search): use default limit to search query if not provided on request body
2026-04-02 20:13:40 +02:00
Git'Fellow 1a2d36af39 Merge pull request #59267 from nextcloud/backport/59262/master
fix(files_sharing): do not prevent share creation if password is enabled but not supported by the current share type
2026-04-02 17:35:23 +02:00
Joas Schilling 60d71a99e2 Merge pull request #59370 from nextcloud/docs/adapt-codeowners
docs(webhook_listeners): adapt codeowners
2026-04-02 12:15:47 +02:00
Joas Schilling 532ba405cf Merge pull request #59341 from nextcloud/3rdparty/upgrade-zipstreamer
chore(dependencies): upgrade zipstreamer in 3rdparty
2026-04-02 11:33:25 +02:00
John Molakvoæ ca3f77bea2 Merge pull request #58872 from nextcloud/fix/templatespath 2026-04-02 11:27:31 +02:00
Joas Schilling 95e94556e1 Merge pull request #59394 from nextcloud/bugfix/noid/dont-register-invalid-routes
fix(settings): Don't register invalid routes
2026-04-02 11:26:09 +02:00
Ferdinand Thiessen 28724fe496 Merge pull request #59365 from nextcloud/fix/default-values
fix(files_external): properly set default values for backend options
2026-04-02 11:04:06 +02:00
Git'Fellow f637e1c6a2 Merge pull request #59330 from nextcloud/btnOpenShare-2
fix: update email button text to 'Open shared item'
2026-04-02 11:03:43 +02:00
Salvatore Martire c5f4ac1253 chore(dependencies): upgrade zipstreamer in 3rdparty
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-04-02 10:44:10 +02:00
Joas Schilling baec1727b3 fix(settings): Don't register invalid routes
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-04-02 09:30:15 +02:00
nextcloud-command b3fa62fc3d chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-04-02 01:56:07 +00:00
Ferdinand Thiessen d3e5a764ba fix(files_external): properly set default values for backend options
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-02 03:47:33 +02:00
Ferdinand Thiessen 456684f138 Merge pull request #59362 from nextcloud/test/deprecations
test: resolve PHPUnit deprecation warning about `addMethods`
2026-04-02 03:46:55 +02:00
Nextcloud bot 04c12502d1 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-02 00:19:14 +00:00
nextcloud-command 1bbceea598 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-04-01 21:22:25 +00:00
skjnldsv 661739c9d7 fix(files_sharing): do not prevent share creation if password is enabled but not supported by the current share type
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2026-04-01 21:20:21 +00:00
Ferdinand Thiessen 03c318d730 Merge pull request #58353 from nextcloud/refactor/55428/comments
refactor(comments): migrate to Vue 3
2026-04-01 20:49:24 +02:00
Cristian Scheid 3539476d12 fix(file-search): use default limit to search query if not provided on request body
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-04-01 14:55:04 -03:00
nextcloud-command 78098c8325 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-04-01 13:41:02 +00:00
Ferdinand Thiessen e8c47cf3ec fix: properly load async components
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-01 14:54:43 +02:00
Ferdinand Thiessen 324ebd144c fix: add compatibility layer for apps expecting Vue 2 API
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-01 14:54:43 +02:00
Ferdinand Thiessen e26588d78c chore: fix ESLint errors
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-01 14:54:43 +02:00
Edward Ly c56ebcecb2 refactor(comments): migrate to Vue 3
Signed-off-by: Edward Ly <contact@edward.ly>
2026-04-01 14:54:43 +02:00
Edward Ly 3aa22804e1 refactor(comments): migrate to TypeScript
Signed-off-by: Edward Ly <contact@edward.ly>
2026-04-01 14:54:43 +02:00
Edward Ly 4edf863803 build(comments): move from frontend-legacy to frontend
Signed-off-by: Edward Ly <contact@edward.ly>
2026-04-01 14:54:43 +02:00
Jana Peper b87d71c4e5 docs(webhook_listeners): adapt codeowners
Signed-off-by: Jana Peper <jana.peper@nextcloud.com>
2026-04-01 14:48:50 +02:00
John Molakvoæ bb1ad1ca15 Merge pull request #59372 from nextcloud/carl/silent-warning-zip 2026-04-01 14:19:13 +02:00
Carl Schwan 7eb0ba9feb fix(zip): Fix warning when downloading Zip file
The HEAD request, create a GET subrequest which is not compatible with
the ZIP plugin since the ZIP plugin is directly streaming the content to
php://output, so we were sending the content in a HEAD request and
creating the ZIP twice and this was creating various warning in logs
too.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-04-01 12:19:43 +02:00
Joas Schilling 530b635cea Merge pull request #58986 from nextcloud/Jerome-Herbinet-activity_use_cached_mountpoints_flag_retroactivity_notice
activity_use_cached_mountpoints flag retroactivity notice
2026-04-01 08:03:38 +02:00
Nextcloud bot 84112d8fc7 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-04-01 00:19:19 +00:00
Ferdinand Thiessen f6379da3a1 Merge pull request #59294 from nextcloud/dragDropDirCreate
fix(filesDrop): drag-and-drop folder upload
2026-04-01 01:30:48 +02:00
github-actions[bot] 560ea9abe7 Merge pull request #59363 from nextcloud/dependabot/github_actions/github-actions-38c73435d9
chore(deps): Bump the github-actions group with 7 updates
2026-03-31 23:22:03 +00:00
Ferdinand Thiessen e0c1b74419 test: resolve PHPUnit deprecation warning about addMethods
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-04-01 01:12:52 +02:00
Ferdinand Thiessen 047a758a31 Merge pull request #58786 from nextcloud/fix/templates_enabled
fix(files): only disable template creation when both skeleton directories are empty
2026-04-01 00:40:25 +02:00
nextcloud-command d5cc1ad2ca chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-31 22:21:32 +00:00
dependabot[bot] 36e4de2552 chore(deps): Bump the github-actions group with 7 updates
Bumps the github-actions group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [dorny/paths-filter](https://github.com/dorny/paths-filter) | `3.0.2` | `4.0.1` |
| [shivammathur/setup-php](https://github.com/shivammathur/setup-php) | `2.36.0` | `2.37.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.32.4` | `4.35.1` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.2.0` | `6.3.0` |
| [cypress-io/github-action](https://github.com/cypress-io/github-action) | `7.1.5` | `7.1.9` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `5.5.2` | `6.0.0` |
| [LizardByte/actions](https://github.com/lizardbyte/actions) | `2026.227.200013` | `2026.328.161128` |


Updates `dorny/paths-filter` from 3.0.2 to 4.0.1
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/de90cc6fb38fc0963ad72b210f1f284cd68cea36...fbd0ab8f3e69293af611ebaee6363fc25e6d187d)

Updates `shivammathur/setup-php` from 2.36.0 to 2.37.0
- [Release notes](https://github.com/shivammathur/setup-php/releases)
- [Commits](https://github.com/shivammathur/setup-php/compare/44454db4f0199b8b9685a5d763dc37cbf79108e1...accd6127cb78bee3e8082180cb391013d204ef9f)

Updates `github/codeql-action` from 4.32.4 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/89a39a4e59826350b863aa6b6252a07ad50cf83e...c10b8064de6f491fea524254123dbe5e09572f13)

Updates `actions/setup-node` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/6044e13b5dc448c55e2357c09f80417699197238...53b83947a5a98c8d113130e565377fae1a50d02f)

Updates `cypress-io/github-action` from 7.1.5 to 7.1.9
- [Release notes](https://github.com/cypress-io/github-action/releases)
- [Changelog](https://github.com/cypress-io/github-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/github-action/compare/bc22e01685c56e89e7813fd8e26f33dc47f87e15...783cb3f07983868532cabaedaa1e6c00ff4786a8)

Updates `codecov/codecov-action` from 5.5.2 to 6.0.0
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/671740ac38dd9b0130fbe1cec585b89eea48d3de...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

Updates `LizardByte/actions` from 2026.227.200013 to 2026.328.161128
- [Release notes](https://github.com/lizardbyte/actions/releases)
- [Commits](https://github.com/lizardbyte/actions/compare/70bb8d394d1c92f6113aeec6ae9cc959a5763d15...0affa4f7bcb27562658960eee840eff8ff844578)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: shivammathur/setup-php
  dependency-version: 2.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: cypress-io/github-action
  dependency-version: 7.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: LizardByte/actions
  dependency-version: 2026.328.161128
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 22:18:07 +00:00
Git'Fellow 5a9fa36bb1 fix(filesDrop): drag-and-drop folder upload directories
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-04-01 00:13:53 +02:00
Ferdinand Thiessen 9e9cccd9ec Merge pull request #58903 from nextcloud/chore/noid/update-workflows
chore(workflows): remove cypress cloud
2026-03-31 23:36:53 +02:00
Ferdinand Thiessen 533125defc Merge pull request #59342 from nextcloud/ci/cypress-contributors
ci(cypress): allow running on forks by removing Cypress cloud
2026-03-31 15:59:21 +02:00
Ferdinand Thiessen 6ba57a70a9 Merge pull request #59311 from nextcloud/jtr/feat-reminders-utc-headers
feat(files_reminders): add explicit UTC indicators to `list` command headers
2026-03-31 15:43:47 +02:00
Ferdinand Thiessen 266dfbce24 Merge pull request #58970 from nextcloud/fix/sftp-mtime
fix: manually set modified time to SFTP files after editing
2026-03-31 13:39:47 +02:00
Joas Schilling 820e0915e9 Merge pull request #59299 from AlessioDiPretoroAsdea/fix/suggestion-ticket-59266
dav: Fix fatal error when ORGANIZER is missing in CalDAV schedule
2026-03-31 13:30:14 +02:00
Stephan Orbaugh 22c75f5b25 Merge pull request #59317 from psimaker/fix/weather-status-missing-codes
fix(weather_status): add all missing met.no symbol codes to weatherOptions
2026-03-31 12:59:45 +02:00
Ferdinand Thiessen 0cd74df34e chore: update psalm baseline
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-03-31 12:36:06 +02:00
Hendrik Leidinger 6e88edb0a6 fix: make psalm and rector happy
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
2026-03-31 12:35:09 +02:00
Hendrik Leidinger 33e35afad2 fix: modified touch function of the SFTP storage to update mtimes instead
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
2026-03-31 12:35:08 +02:00
Hendrik Leidinger 6d26c71b48 fix: manually set modified time to SFTP files after editing
Signed-off-by: Hendrik Leidinger <hendrik.leidinger@nextcloud.com>
2026-03-31 12:35:08 +02:00
psimaker 19b0c911aa chore(weather_status): compile frontend assets
Signed-off-by: psimaker <psimaker@users.noreply.github.com>
2026-03-31 12:23:55 +02:00
psimaker af8cb33a40 fix(weather_status): add all missing met.no symbol codes to weatherOptions
Signed-off-by: psimaker <psimaker@users.noreply.github.com>
2026-03-31 12:12:33 +02:00
Ferdinand Thiessen d6e6525861 ci(cypress): allow running on forks by removing Cypress cloud
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-03-31 12:01:11 +02:00
Joas Schilling 5acf3878f3 Merge pull request #59318 from nextcloud/jtr/ci-perf-lint-php-cs-changes-bypass
ci: stop running `lint` in `lint-php-cs` unconditionally even if no changes
2026-03-31 11:56:26 +02:00
Joas Schilling 810d8b4c98 Merge pull request #59319 from nextcloud/jtr/ci-perf-static-code-analysis-changes-check
ci: add path filtering to static-code-analysis workflow
2026-03-31 11:55:19 +02:00
Git'Fellow 545a9c1c73 fix: update email button text to 'Open shared item'
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-03-31 11:27:58 +02:00
Nextcloud bot 32a264842f fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-31 00:19:16 +00:00
AlessioDiPretoroAsdea 85b851a7df fix(dav): Avoid fatal error for events without an organizer
Signed-off-by: AlessioDiPretoroAsdea <alessio.dipretoro@asdeahw.net>
2026-03-30 14:22:56 -04:00
Josh 330ae40ecf ci: add path filtering to static-code-analysis workflow
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-30 12:56:55 -04:00
Josh b400f1b93b ci: stop running lint in lint-php-cs unconditionally even if no changes
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-30 12:31:49 -04:00
Robin Appelman 78513504ca Merge pull request #59202 from nextcloud/encp-cache-key-validation
fix: cache validation of system keys
2026-03-30 17:52:31 +02:00
Josh d89da91a56 feat(files_reminders): add explicit UTC indicators to list command headers
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-30 10:42:35 -04:00
Côme Chilliet 7040916f96 Merge pull request #59222 from nextcloud/artonge/fix/systemtag_user_context
feat(systemtag): Add $user context to create and update tag
2026-03-30 16:33:52 +02:00
Salvatore Martire 9756097e28 Merge pull request #59252 from nextcloud/fix/move-with-unjailed-source-path
fix: use unjailed source path when moving jailed files
2026-03-30 15:42:49 +02:00
Oleksandr Piskun 5ce08e9704 Merge pull request #58376 from nextcloud/feat/taskprocessing/keda-autoscaler
feat(taskprocessing): Add queue_stats API endpoint for external autoscalers
2026-03-30 16:27:54 +03:00
Oleksander Piskun f573285040 feat(taskprocessing): Add queue_stats API endpoint for external autoscalers
Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
2026-03-30 12:03:19 +00:00
Joas Schilling b119cace4f Merge pull request #59303 from nextcloud/bugfix/noid/dont-redirect-on-error
fix(sharing): Don't redirect when accepting a share failed
2026-03-30 13:12:48 +02:00
Joas Schilling 2c560b0837 fix(sharing): Don't redirect when accepting a share failed
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-03-30 12:07:42 +02:00
Côme Chilliet 4e0217ef13 chore(tests): Fix bogus parameter passed by tests to createTag
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-30 11:38:15 +02:00
Côme Chilliet 17d60a4af0 Merge pull request #59204 from nextcloud/jtr/feat-external-s3-auth-fields
feat(files_external/s3): make some auth labels clearer
2026-03-30 11:24:23 +02:00
Côme Chilliet 3a5c445c78 Merge pull request #59261 from nextcloud/feat/config/sensitive-values/sentry-csp-report-url
feat(config): add Sentry CSP Report URL to sensitive values
2026-03-30 10:15:02 +02:00
Git'Fellow 04ee3aff1a Merge pull request #59270 from nextcloud/ensureValueOneParallel
fix(chunkedUploads): Ensure max parallel count is at least 1
2026-03-30 10:11:27 +02:00
Louis Chmn 3aa2e130cd feat(systemtag): Add $user context to create and update tag
Signed-off-by: Louis Chmn <louis@chmn.me>
2026-03-30 09:54:54 +02:00
Nextcloud bot b71661a38b fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-30 00:29:17 +00:00
Git'Fellow 5b86c7a402 docs: Add a note to documentation
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-03-29 15:02:43 +02:00
Nextcloud bot e2dc439c71 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-29 00:33:57 +00:00
Josh c86e2d1e67 test: add coverage for ChunkedUploadConfig
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-27 23:44:45 -04:00
Nextcloud bot f5faddaf31 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-28 00:30:27 +00:00
Git'Fellow 99a26b0a97 fix(chunkedUploads): Ensure max parallel count is at least 1
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-03-27 18:57:19 +01:00
Andy Scherzinger aaf2ded381 Merge pull request #59240 from nextcloud/hskpng/code-owners
docs: update CODEOWNERS with new assignments
2026-03-27 18:48:53 +01:00
Joda Stößer c8dbc73017 fix(config): correct indentation for Sentry CSP Report URL
Signed-off-by: Joda Stößer <git@simjo.st>
2026-03-27 16:11:28 +01:00
Joda Stößer a71584ee96 feat(config): add Sentry CSP Report URL to sensitive values
Signed-off-by: Joda Stößer <git@simjo.st>
2026-03-27 16:01:41 +01:00
Salvatore Martire bf24b24841 fix: use unjailed source path when moving jailed files
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-03-27 14:30:46 +01:00
Nextcloud bot f8cc0adefb fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-27 00:29:10 +00:00
Robin Appelman d1fd735698 Merge pull request #57374 from nextcloud/rename-dav-permissions
fix: allow renaming files with just update permissions
2026-03-26 22:12:28 +01:00
Robin Appelman 1e2f273c09 test: adjust tests to caching of key validation
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-26 21:13:18 +01:00
Robin Appelman 9b504039f4 fix: cache validation of system keys
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-26 21:12:05 +01:00
Arthur Schiwon 0ff7ad3535 docs: update CODEOWNERS with new assignments
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2026-03-26 21:06:50 +01:00
nextcloud-command 8f7b0b4264 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-26 18:18:56 +01:00
Robin Appelman c08592de28 fix: properly expose rename permissions over dav
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-26 18:18:55 +01:00
Robin Appelman 005b2b04e8 fix: update rename permissions check in files ui
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-26 17:10:03 +01:00
Robin Appelman 63a4778744 fix: allow renaming files with just update permissions
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-26 17:10:01 +01:00
Côme Chilliet ca245b4cc6 Merge pull request #58896 from nextcloud/fix/fix-tags-user-event
fix: Fix user in Tags class, do not depend upon session
2026-03-26 17:06:27 +01:00
Louis 309d12abe1 Merge pull request #58990 from mosi-kha/fix/group-displayname-event-oldvalue
fix(group): pass previous display name in GroupChangedEvent
2026-03-26 16:07:50 +01:00
Kate 7e9e1269a0 Merge pull request #59218 from nextcloud/feat/audit-log-ai-config-changes 2026-03-26 12:22:37 +01:00
Marcel Klehr 861f9bdb31 fix(settings): Check all values first, then apply them
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
2026-03-26 11:32:42 +01:00
Marcel Klehr a375265111 fix(settings): Throw JSON errors and return 400 status if JSON encode fails
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
2026-03-26 11:03:31 +01:00
Marcel Klehr 7fe3d1c6a0 feat(settings): Log AI config changes to audit log
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
2026-03-26 10:50:01 +01:00
Kate 0cf140e903 Merge pull request #58761 from nextcloud/feat/1700/limit-recent-files 2026-03-26 09:05:14 +01:00
github-actions[bot] 3d27f8d288 Merge pull request #59207 from nextcloud/dependabot/npm_and_yarn/multi-bf05dc1ecf
chore(deps): Bump picomatch
2026-03-26 02:18:48 +00:00
Nextcloud bot 52dac4be2b fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-26 00:19:23 +00:00
dependabot[bot] 252e24b83d chore(deps): Bump picomatch
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 22:31:46 +00:00
Josh b7bdd76731 feat(files_external/s3): rename legacy signature (v2) for clarity
Avoid the use of "authentication" here since this is really about signing compatibility, not the source of credentials. Also prefix with "Use" for UI consistency.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-25 16:21:35 -04:00
Josh 00100427be feat(files_external/s3): Adjust auth meth/parameter field names
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-25 16:10:35 -04:00
Ferdinand Thiessen 2ae66ff888 Merge pull request #59043 from nextcloud/jtr/chore-trashbin-drop-abortOperation-usage
chore(trashbin): deprecate abortOperation on BeforeNodeRestoredEvent / switch to AbortedEventException
2026-03-25 18:14:59 +01:00
Cristian Scheid 6594d54476 feat(recent-search): pass recent_limit config value to getRecentSearch function
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-25 13:53:26 -03:00
Joas Schilling 9b125a7605 Merge pull request #59179 from nextcloud/automated/noid/master-update-code-signing-crl
[master] fix(security): Update code signing revocation list
2026-03-25 07:35:57 +01:00
nextcloud-command eb06a5628f fix(security): Update code signing revocation list
Signed-off-by: GitHub <noreply@github.com>
2026-03-25 03:12:18 +00:00
Nextcloud bot 2a5e57d2f4 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-25 00:19:21 +00:00
Cristian Scheid 9b605de474 refactor(recent-files): move recent limit to app-scoped config
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 20:44:22 -03:00
Cristian Scheid 48ce8c3fa1 refactor(recent-files): remove user config from frontend files
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:38:53 -03:00
Cristian Scheid 3925176b49 refactor(files-settings): remove recent file settings section
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:15:34 -03:00
Cristian Scheid 981978d960 feat(recent-files): handle invalid config when only min or max are set for recent files limit
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:15:34 -03:00
Cristian Scheid 282e6b707a feat(file-info): encapsulate logic to get last activity in getLastActivity function
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:15:34 -03:00
Cristian Scheid 5bcac61c73 refactor(recent-search): use order by fields to determine extended cache join and cast config limit to number
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:15:34 -03:00
Cristian Scheid 8225ab726f feat(recent-files): add nc:last_activity property to allow sorting by max between upload_time and mtime
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-24 15:15:34 -03:00
Cristian Scheid 62f3ea390e feat(recent-files): add recent_files_limit config on files settings
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-24 15:15:34 -03:00
John Molakvoæ d677a3a5e2 Merge pull request #59089 from nextcloud/fix/expand-theming-capabilities 2026-03-24 17:45:07 +01:00
skjnldsv 04d9f79985 chore: openapi conf update
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2026-03-24 15:24:18 +01:00
John Molakvoæ 8fa69c1c29 fix(theming): deprecation message
Co-authored-by: Ferdinand Thiessen <opensource@fthiessen.de>
Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
2026-03-24 15:24:18 +01:00
skjnldsv f2c73ebffd fix(theming): deprecate OCA.Theming and move to capabilities
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2026-03-24 15:24:18 +01:00
Josh 8bd63c5fe2 chore(Trashbin): hello lint
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
Josh e53c124ecc chore(trashbin): drop unused use statements from SyncLivePhotosListener
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
Josh 5f658d9eae chore(Trashbin): add AbortedEventException use
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
Josh bd0784e056 refactor(Trashbin): switch to AbortedEventException usage in restore
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
Josh 23569000a8 chore(trashbin): switch SyncLivePhotosListener to AbortedEventException
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
Josh 4b2a77b1ff chore(trashbin): refactor deprecated abortOperation to use AbortedEventException
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-21 11:50:28 -04:00
mostafa 3d6d38a2fb fix(group): pass previous display name in GroupChangedEvent
Signed-off-by: mostafa <mostafakhaki00@gmail.com>
2026-03-17 09:16:30 +00:00
Jérôme Herbinet b964ca5394 feat(doc): activity_use_cached_mountpoints flag retroactivity notice
Signed-off-by: Jérôme Herbinet <33763786+Jerome-Herbinet@users.noreply.github.com>
2026-03-17 09:37:37 +01:00
Côme Chilliet cc99dd8fd9 fix: Fix user in Tags class, do not depend upon session
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-13 15:13:40 +01:00
Anna Larch a77040f41e chore(workflows): remove cypress cloud
and allow regular cypress tests to run on forks (no secret leaked)
add consistent gating
 for changes
remove empty matrix option
skip performance workflow on forks without it looking like it's failing

Signed-off-by: Anna Larch <anna@nextcloud.com>
2026-03-12 20:26:51 +01:00
John Molakvoæ (skjnldsv) 5fdebf923c fix(lib): templates core path detection
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2026-03-11 17:24:11 +01:00
Benjamin Frueh 4b203e5f7b fix: only skip template initialization when both skeletondirectory and templatedirectory are empty
Signed-off-by: Benjamin Frueh <benjamin.frueh@gmail.com>
2026-03-08 00:12:56 +01:00
1291 changed files with 18239 additions and 15825 deletions
+4 -4
View File
@@ -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 @blizzz
/apps/admin_audit/appinfo/info.xml @luka-nextcloud @samin-z
/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 @JuliaKirschenheuter @sorbaugh
/apps/settings/appinfo/info.xml @hweihwang @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 @JuliaKirschenheuter @sorbaugh
/apps/updatenotification/appinfo/info.xml @enjeck @sorbaugh
/apps/user_ldap/appinfo/info.xml @come-nc @blizzz
/apps/user_status/appinfo/info.xml @Antreesy @nickvergessen
/apps/weather_status/appinfo/info.xml @julien-nc @juliusknorr
/apps/webhook_listeners/appinfo/info.xml @come-nc @julien-nc
/apps/webhook_listeners/appinfo/info.xml @janepie @julien-nc
/apps/workflowengine/appinfo/info.xml @blizzz @juliusknorr
# Files frontend expertise
+7 -4
View File
@@ -20,7 +20,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
@@ -74,6 +74,9 @@ jobs:
autocheckers:
runs-on: ubuntu-latest-low
needs: changes
if: needs.changes.outputs.src != 'false'
strategy:
matrix:
php-versions: ['8.2']
@@ -88,7 +91,7 @@ jobs:
submodules: true
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
@@ -125,4 +128,4 @@ jobs:
steps:
- name: Summary status
run: if ${{ needs.autocheckers.result != 'success' || (needs.changes.outputs.src != 'false' && needs.autoloader.result != 'success') }}; then exit 1; fi
run: if ${{ needs.changes.outputs.src != 'false' && (needs.autocheckers.result != 'success' || needs.autoloader.result != 'success') }}; then exit 1; fi
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check requirement
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
continue-on-error: true
with:
+2 -2
View File
@@ -37,13 +37,13 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -124,7 +124,7 @@ jobs:
fallbackNpm: '^11.3'
- name: Set up node ${{ steps.package-engines-versions.outputs.nodeVersion }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.package-engines-versions.outputs.nodeVersion }}
cache: npm
+30 -19
View File
@@ -14,6 +14,7 @@ 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')
@@ -27,8 +28,25 @@ 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: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
if: steps.get-pr.outputs.head_repo != github.repository
run: |
echo 'Can not execute /update-3rdparty on forks'
exit 1
@@ -46,24 +64,17 @@ jobs:
ref: ${{ steps.comment-branch.outputs.head_ref }}
- name: Register server reference to fallback to master branch
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');
}
}
run: |
base_ref="${{ steps.get-pr.outputs.base_ref }}"
if [[ "$base_ref" == "main" || "$base_ref" == "master" ]]; then
echo "server_ref=master" >> "$GITHUB_ENV"
echo "Setting server_ref to master"
elif [[ "$base_ref" =~ ^stable[0-9]+$ ]]; then
echo "server_ref=$base_ref" >> "$GITHUB_ENV"
echo "Setting server_ref to $base_ref"
else
echo "Not based on master/main/stable*, so skipping pull 3rdparty command"
fi
- name: Setup git
run: |
+3 -14
View File
@@ -41,12 +41,6 @@ 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:
@@ -72,7 +66,7 @@ jobs:
fallbackNpm: '^11.3'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
@@ -160,7 +154,7 @@ jobs:
path: ./
- name: Set up node ${{ needs.init.outputs.nodeVersion }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ needs.init.outputs.nodeVersion }}
@@ -171,15 +165,11 @@ jobs:
run: ./node_modules/cypress/bin/cypress install
- name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
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 }}
@@ -188,7 +178,6 @@ 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 }}
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-files-external-ftp
+5 -5
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-files-external-s3
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
continue-on-error: true
with:
@@ -73,11 +73,11 @@ jobs:
- name: Set up sftpd
run: |
sudo mkdir /tmp/sftp
sudo chown -R 0777 /tmp/sftp
sudo chmod -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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-files-external-sftp
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
continue-on-error: true
with:
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-files-external-smb
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-files-external-webdav
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
timeout-minutes: 5
with:
php-version: 8.2
+57 -57
View File
@@ -19,7 +19,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@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 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 Python
uses: LizardByte/actions/actions/setup_python@70bb8d394d1c92f6113aeec6ae9cc959a5763d15 # v2026.227.200013
with:
python-version: '2.7'
- name: Set up Python
uses: LizardByte/actions/actions/setup_python@0affa4f7bcb27562658960eee840eff8ff844578 # v2026.328.161128
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:
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
+2 -3
View File
@@ -19,7 +19,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
@@ -95,7 +95,6 @@ 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
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
+5 -2
View File
@@ -25,7 +25,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
continue-on-error: true
with:
@@ -44,6 +44,9 @@ jobs:
lint:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src != 'false'
name: php-cs
steps:
@@ -53,7 +56,7 @@ jobs:
persist-credentials: false
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
timeout-minutes: 5
with:
php-version: 8.2
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
outputs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
+44 -1
View File
@@ -18,9 +18,37 @@ 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:
@@ -37,7 +65,7 @@ jobs:
fallbackNpm: '^11.3'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
@@ -51,3 +79,18 @@ 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
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
+3 -6
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
@@ -83,14 +83,11 @@ 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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./coverage/lcov.info,./coverage/legacy/lcov.info
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
fallbackNpm: '^11.3'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-azure
+3 -4
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
@@ -111,7 +111,6 @@ 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
@@ -123,7 +122,7 @@ jobs:
- name: Upload code coverage
if: ${{ !cancelled() && matrix.coverage }}
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-s3
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-swift
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
timeout-minutes: 5
with:
php-version: '8.2'
+8 -7
View File
@@ -15,7 +15,13 @@ jobs:
performance-testing:
runs-on: ubuntu-latest
if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
# Skip entirely on fork PRs so the job result is 'skipped' rather than
# 'failure'. The profiler action uses github.event.pull_request.head.repo.clone_url
# and GITHUB_TOKEN in ways that do not work reliably from forks, and a
# clean skip is far less confusing for contributors than a mid-run error.
if: >-
github.repository_owner != 'nextcloud-gmbh' &&
github.event.pull_request.head.repo.full_name == github.repository
permissions:
pull-requests: write
@@ -28,11 +34,6 @@ 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
@@ -42,7 +43,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
timeout-minutes: 5
with:
php-version: ${{ matrix.php-versions }}
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-mariadb
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.xml
flags: phpunit-memcached
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-mysql
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-mysql
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.nodb.xml
flags: phpunit-nodb
@@ -21,7 +21,7 @@ jobs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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
run: composer run test:db -- --log-junit junit.xml
- name: S3 logs
if: always()
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-oci
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-postgres
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
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@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
files: ./clover.db.xml
flags: phpunit-sqlite
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
ref: ${{ github.event.repository.default_branch }}
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.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
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.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
+65 -11
View File
@@ -21,10 +21,35 @@ 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
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
needs: changes
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
steps:
- name: Checkout
@@ -34,7 +59,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: '8.2'
@@ -56,7 +81,8 @@ jobs:
static-code-analysis-security:
runs-on: ubuntu-latest
if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
needs: changes
if: ${{ needs.changes.outputs.src != 'false' && github.repository_owner != 'nextcloud-gmbh' }}
permissions:
security-events: write
@@ -69,7 +95,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: '8.2'
@@ -88,14 +114,15 @@ jobs:
- name: Upload Security Analysis results to GitHub
if: always()
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3
with:
sarif_file: results.sarif
static-code-analysis-ocp:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
needs: changes
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
steps:
- name: Checkout
@@ -105,7 +132,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: '8.2'
@@ -127,7 +154,8 @@ jobs:
static-code-analysis-ncu:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
needs: changes
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
steps:
- name: Checkout
@@ -137,7 +165,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
timeout-minutes: 5
with:
php-version: '8.2'
@@ -155,7 +183,8 @@ jobs:
static-code-analysis-strict:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
needs: changes
if: ${{ needs.changes.outputs.src != 'false' && github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}
steps:
- name: Checkout
@@ -165,7 +194,7 @@ jobs:
submodules: true
- name: Set up php
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 #v2.36.0
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f #v2.37.0
with:
php-version: '8.2'
extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip
@@ -178,3 +207,28 @@ 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/${{ matrix.branches }}-update-min-supported-desktop-version"
branch: "automated/noid/update-min-supported-desktop-version"
title: "chore: Update minimum supported desktop version to ${{ steps.extract-version.outputs.VERSION }}"
base: "master"
body: |
+5 -4
View File
@@ -17,16 +17,17 @@ 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/files
!/apps/encryption
!/apps/federation
!/apps/federatedfilesharing
!/apps/sharebymail
!/apps/encryption
!/apps/files
!/apps/files_external
!/apps/files_reminders
!/apps/files_sharing
@@ -38,9 +39,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
+6
View File
@@ -8,6 +8,12 @@ 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
+7
View File
@@ -0,0 +1,7 @@
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);");
+5
View File
@@ -0,0 +1,5 @@
{ "translations": {
"Auditing / Logging" : "নিরীক্ষা",
"Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "নেক্সটক্লাউডের নিরীক্ষামূলক সক্ষমতা প্রদান করে যেমন লগিং ফাইল অ্যাক্সেস বা অন্য কোনো জরুরী পদক্ষেপসমূহ"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+16
View File
@@ -0,0 +1,16 @@
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"
+22
View File
@@ -0,0 +1,22 @@
<?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>
+22
View File
@@ -0,0 +1,22 @@
<?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();
+13
View File
@@ -0,0 +1,13 @@
{
"config" : {
"vendor-dir": ".",
"optimize-autoloader": true,
"classmap-authoritative": true,
"autoloader-suffix": "Appstore"
},
"autoload" : {
"psr-4": {
"OCA\\Appstore\\": "../lib/"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"_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"
}
@@ -0,0 +1,579 @@
<?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);
}
}
@@ -0,0 +1,396 @@
<?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;
}
}
+21
View File
@@ -0,0 +1,21 @@
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.
@@ -0,0 +1,16 @@
<?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',
);
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
);
@@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
'OCA\\Appstore\\' => array($baseDir . '/../lib'),
);
@@ -0,0 +1,37 @@
<?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;
}
}
@@ -0,0 +1,42 @@
<?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);
}
}
@@ -0,0 +1,5 @@
{
"packages": [],
"dev": false,
"dev-package-names": []
}
@@ -0,0 +1,23 @@
<?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,
),
),
);

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 321 B

View File
+30
View File
@@ -0,0 +1,30 @@
<?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 {
}
}
@@ -0,0 +1,440 @@
<?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;
}
}
@@ -0,0 +1,181 @@
<?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
}
}
}
}
@@ -0,0 +1,110 @@
<?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;
}
}
@@ -6,8 +6,9 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Settings\Search;
namespace OCA\Appstore\Search;
use OCA\Appstore\AppInfo\Application;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IUser;
@@ -24,7 +25,7 @@ class AppSearch implements IProvider {
}
public function getId(): string {
return 'settings_apps';
return Application::APP_ID;
}
public function getName(): string {
@@ -32,7 +33,7 @@ class AppSearch implements IProvider {
}
public function getOrder(string $route, array $routeParameters): int {
return $route === 'settings.AppSettings.viewApps' ? -50 : 100;
return $route === 'appstore.AppSettings.viewApps' ? -50 : 100;
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
+104
View File
@@ -0,0 +1,104 @@
{
"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": []
}
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
+709
View File
@@ -0,0 +1,709 @@
{
"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": []
}
+2
View File
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
+632
View File
@@ -0,0 +1,632 @@
{
"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": []
}
+2
View File
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
+61
View File
@@ -0,0 +1,61 @@
<!--
- 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>
+112
View File
@@ -0,0 +1,112 @@
/*!
* 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
@@ -27,7 +27,16 @@ export interface IAppstoreAppRelease {
}
}
export interface IAppstoreApp {
export interface IAppstoreAppData extends Record<string, unknown> {
ratingOverall: number
ratingNumOverall: number
ratingRecent: number
ratingNumRecent: number
releases: IAppstoreAppRelease[]
}
export interface IAppstoreAppResponse {
id: string
name: string
summary: string
@@ -38,24 +47,36 @@ export interface IAppstoreApp {
version: string
category: string | string[]
preview?: string
icon?: string
screenshot?: string
app_api: boolean
/**
* Groups this app is limited to.
* (only available if app is already installed)
*/
groups?: string[]
score: number
ratingNumThresholdReached: boolean
app_api: false
active: boolean
internal: boolean
removable: boolean
installed: boolean
canInstall: boolean
canUnInstall: boolean
isCompatible: boolean
needsDownload: boolean
missingDependencies?: string[]
update?: string
appstoreData: Record<string, never>
appstoreData?: IAppstoreAppData
releases?: IAppstoreAppRelease[]
}
export interface IAppstoreApp extends IAppstoreAppResponse {
loading?: boolean
}
export interface IComputeDevice {
id: string
label: string
@@ -81,10 +102,10 @@ export interface IDeployDaemon {
export interface IExAppStatus {
action: string
deploy: number
deploy_start_time: number
error: string
deploy_start_time?: number
error?: string
init: number
init_start_time: number
init_start_time?: number
type: string
}
@@ -111,8 +132,9 @@ export interface IAppstoreExAppRelease extends IAppstoreAppRelease {
}
export interface IAppstoreExApp extends IAppstoreApp {
app_api: true
daemon: IDeployDaemon | null | undefined
status: IExAppStatus | Record<string, never>
error: string
error?: string
releases: IAppstoreExAppRelease[]
}
+64
View File
@@ -0,0 +1,64 @@
<!--
- 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>
+66
View File
@@ -0,0 +1,66 @@
<!--
- 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>
@@ -2,19 +2,9 @@
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<span
v-if="isSupported || isFeatured"
class="app-level-badge"
:class="{ 'app-level-badge--supported': isSupported }"
:title="badgeTitle">
<NcIconSvgWrapper :path="badgeIcon" :size="20" inline />
{{ badgeText }}
</span>
</template>
<script setup lang="ts">
import { mdiCheck, mdiStarShootingOutline } from '@mdi/js'
import { mdiStar, mdiStarShootingOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { computed } from 'vue'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
@@ -28,15 +18,27 @@ const props = defineProps<{
const isSupported = computed(() => props.level === 300)
const isFeatured = computed(() => props.level === 200)
const badgeIcon = computed(() => isSupported.value ? mdiStarShootingOutline : mdiCheck)
const badgeText = computed(() => isSupported.value ? t('settings', 'Supported') : t('settings', 'Featured'))
const badgeIcon = computed(() => isSupported.value
? mdiStarShootingOutline
: mdiStar)
const badgeText = computed(() => isSupported.value ? t('appstore', 'Supported') : t('appstore', 'Featured'))
const badgeTitle = computed(() => isSupported.value
? t('settings', 'This app is supported via your current Nextcloud subscription.')
: t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.'))
? t('appstore', 'This app is supported via your current Nextcloud subscription.')
: t('appstore', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.'))
</script>
<style scoped lang="scss">
.app-level-badge {
<template>
<span
v-if="isSupported || isFeatured"
:class="[ $style.appLevelBadge, { [$style.appLevelBadge__supported]: isSupported } ]"
:title="badgeTitle">
<NcIconSvgWrapper :path="badgeIcon" :size="20" inline />
{{ badgeText }}
</span>
</template>
<style module>
.appLevelBadge {
color: var(--color-text-maxcontrast);
background-color: transparent;
border: 1px solid var(--color-text-maxcontrast);
@@ -44,14 +46,14 @@ const badgeTitle = computed(() => isSupported.value
display: flex;
flex-direction: row;
gap: 6px;
gap: var(--default-grid-baseline);
padding: 3px 6px;
width: fit-content;
}
&--supported {
background-color: var(--color-success);
border-color: var(--color-border-success);
color: var(--color-success-text);
}
.appLevelBadge__supported {
background-color: var(--color-success);
border-color: var(--color-border-success);
color: var(--color-success-text);
}
</style>
+83
View File
@@ -0,0 +1,83 @@
<!--
- 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>
@@ -19,36 +19,36 @@
}">
<template v-if="useListView">
<div v-if="showUpdateAll" class="apps-list__toolbar">
{{ n('settings', '%n app has an update available', '%n apps have an update available', counter) }}
{{ n('appstore', '%n app has an update available', '%n apps have an update available', counter) }}
<NcButton
v-if="showUpdateAll"
id="app-list-update-all"
variant="primary"
@click="updateAll">
{{ n('settings', 'Update', 'Update all', counter) }}
{{ n('appstore', 'Update', 'Update all', counter) }}
</NcButton>
</div>
<div v-if="!showUpdateAll" class="apps-list__toolbar">
{{ t('settings', 'All apps are up-to-date.') }}
{{ t('appstore', 'All apps are up-to-date.') }}
</div>
<TransitionGroup name="apps-list" tag="table" class="apps-list__list-container">
<tr key="app-list-view-header">
<th>
<span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Icon') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Name') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Name') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Version') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Version') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Level') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Level') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Actions') }}</span>
</th>
</tr>
<AppItem
@@ -64,30 +64,30 @@
class="apps-list__list-container">
<tr key="app-list-view-header">
<th id="app-table-col-icon">
<span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Icon') }}</span>
</th>
<th id="app-table-col-name">
<span class="hidden-visually">{{ t('settings', 'Name') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Name') }}</span>
</th>
<th id="app-table-col-version">
<span class="hidden-visually">{{ t('settings', 'Version') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Version') }}</span>
</th>
<th id="app-table-col-level">
<span class="hidden-visually">{{ t('settings', 'Level') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Level') }}</span>
</th>
<th id="app-table-col-actions">
<span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Actions') }}</span>
</th>
</tr>
<template v-for="bundle in bundles">
<tr :key="bundle.id">
<template v-for="bundle in bundles" :key="bundle.id">
<tr>
<th :id="`app-table-rowgroup-${bundle.id}`" colspan="5" scope="rowgroup">
<div class="apps-list__bundle-heading">
<span class="apps-list__bundle-header">
{{ bundle.name }}
</span>
<NcButton variant="secondary" @click="toggleBundle(bundle.id)">
{{ t('settings', bundleToggleText(bundle.id)) }}
{{ t('appstore', bundleToggleText(bundle.id)) }}
</NcButton>
</div>
</th>
@@ -115,23 +115,23 @@
<div class="apps-list__list-container">
<table v-if="search !== '' && searchApps.length > 0" class="apps-list__list-container">
<caption class="apps-list__bundle-header">
{{ t('settings', 'Results from other categories') }}
{{ t('appstore', 'Results from other categories') }}
</caption>
<tr key="app-list-view-header">
<th>
<span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Icon') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Name') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Name') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Version') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Version') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Level') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Level') }}</span>
</th>
<th>
<span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
<span class="hidden-visually">{{ t('appstore', 'Actions') }}</span>
</th>
</tr>
<AppItem
@@ -145,7 +145,7 @@
<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
<div id="app-list-empty-icon" class="icon-settings-dark" />
<h2>{{ t('settings', 'No apps found for your version') }}</h2>
<h2>{{ t('appstore', 'No apps found for your version') }}</h2>
</div>
</div>
</template>
@@ -157,7 +157,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
import AppItem from './AppList/AppItem.vue'
import OfficeSuiteSwitcher from './AppList/OfficeSuiteSwitcher.vue'
import { getOfficeSuiteById, OFFICE_SUITES } from '../constants/OfficeSuites.js'
import logger from '../logger.ts'
import logger from '../utils/logger.ts'
import AppManagement from '../mixins/AppManagement.js'
import { useAppApiStore } from '../store/app-api-store.ts'
import { useAppsStore } from '../store/apps-store.ts'
@@ -314,9 +314,9 @@ export default {
bundleToggleText() {
return (id) => {
if (this.allBundlesEnabled(id)) {
return t('settings', 'Disable all')
return t('appstore', 'Disable all')
}
return t('settings', 'Download and enable all')
return t('appstore', 'Download and enable all')
}
},
},
@@ -0,0 +1,108 @@
<!--
- 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>
@@ -0,0 +1,27 @@
<!--
- 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>
@@ -53,7 +53,7 @@
id: app.id,
},
}"
:aria-label="t('settings', 'Show details for {appName} app', { appName: app.name })">
:aria-label="t('appstore', 'Show details for {appName} app', { appName: app.name })">
{{ app.name }}
</router-link>
</component>
@@ -92,7 +92,7 @@
:disabled="installing || isLoading || !defaultDeployDaemonAccessible || isManualInstall"
:title="updateButtonText"
@click.stop="update(app.id)">
{{ t('settings', 'Update to {update}', { update: app.update }) }}
{{ t('appstore', 'Update to {update}', { update: app.update }) }}
</NcButton>
<NcButton
v-if="app.canUnInstall"
@@ -100,7 +100,7 @@
variant="tertiary"
:disabled="installing || isLoading"
@click.stop="remove(app.id)">
{{ t('settings', 'Remove') }}
{{ t('appstore', 'Remove') }}
</NcButton>
<NcButton
v-if="app.active"
@@ -129,7 +129,7 @@
<DaemonSelectionDialog
v-if="app?.app_api && showSelectDaemonModal"
:show.sync="showSelectDaemonModal"
v-model:show="showSelectDaemonModal"
:app="app" />
</component>
</component>
@@ -139,13 +139,12 @@
import { mdiCogOutline } from '@mdi/js'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
import SvgFilterMixin from '../SvgFilterMixin.vue'
import DaemonSelectionDialog from '../DaemonSelectionDialog/DaemonSelectionDialog.vue'
import AppLevelBadge from './AppLevelBadge.vue'
import AppScore from './AppScore.vue'
import AppManagement from '../../mixins/AppManagement.js'
import { useAppApiStore } from '../../store/app-api-store.ts'
import { useAppsStore } from '../../store/apps-store.js'
import { useAppsStore } from '../../store/apps.ts'
import { useAppApiStore } from '../../store/exApps.ts'
export default {
name: 'AppItem',
@@ -157,7 +156,7 @@ export default {
DaemonSelectionDialog,
},
mixins: [AppManagement, SvgFilterMixin],
mixins: [AppManagement],
props: {
app: {
type: Object,
@@ -281,7 +280,6 @@ export default {
</script>
<style scoped lang="scss">
@use '../../../../../core/css/variables.scss' as variables;
@use 'sass:math';
.app-item {
@@ -364,7 +362,7 @@ export default {
}
/* Hide actions on a small screen. Click on app opens fill-screen sidebar with the buttons */
@media only screen and (max-width: math.div(variables.$breakpoint-mobile, 2)) {
@media only screen and (max-width: 512px) {
.app-actions {
display: none;
}
@@ -437,7 +435,7 @@ export default {
}
}
@media only screen and (max-width: variables.$breakpoint-mobile) {
@media only screen and (max-width: 1024px) {
width: 50%;
}
@@ -3,6 +3,53 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconCheckCircle from 'vue-material-design-icons/CheckCircle.vue'
import { OFFICE_SUITES } from '../../constants/OfficeSuites.ts'
import { shallowRef } from 'vue'
const { installedApps = [] } = defineProps<{
installedApps: { id: string, active: boolean }[],
}>()
const emit = defineEmits<{
'suite-selected': [suiteId: string | null]
}>()
const isAllInOne = loadState('settings', 'isAllInOne', false)
const selectedSuite = shallowRef<string | null>(getInitialSuite())
function getInitialSuite() {
for (const suite of OFFICE_SUITES) {
const app = installedApps.find((a) => a.id === suite.appId)
if (app && app.active) {
return suite.id
}
}
return null
}
function selectSuite(suiteId: string) {
if (selectedSuite.value === suiteId) {
// already selected keep selection; use the disable button to clear
return
}
selectedSuite.value = suiteId
emit('suite-selected', suiteId)
}
function disableSuites() {
if (!selectedSuite.value) {
return
}
selectedSuite.value = null
emit('suite-selected', null)
}
</script>
<template>
<div class="office-suite-switcher">
<div v-if="isAllInOne" class="office-suite-switcher__aio-message">
@@ -13,7 +60,7 @@
<p>{{ t('settings', 'Select your preferred office suite. Please note that installing requires manual server setup.') }}</p>
<div class="office-suite-cards">
<div
v-for="suite in officeSuites"
v-for="suite in OFFICE_SUITES"
:key="suite.id"
class="office-suite-card"
:class="{
@@ -56,71 +103,6 @@
</div>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconCheckCircle from 'vue-material-design-icons/CheckCircle.vue'
import { OFFICE_SUITES } from '../../constants/OfficeSuites.js'
export default {
name: 'OfficeSuiteSwitcher',
components: {
IconCheckCircle,
IconArrowRight,
},
props: {
installedApps: {
type: Array,
default: () => [],
},
},
emits: ['suite-selected'],
data() {
return {
isAllInOne: loadState('settings', 'isAllInOne', false),
selectedSuite: this.getInitialSuite(),
officeSuites: OFFICE_SUITES,
}
},
methods: {
t,
getInitialSuite() {
for (const suite of OFFICE_SUITES) {
const app = this.installedApps.find((a) => a.id === suite.appId)
if (app && app.active) {
return suite.id
}
}
return null
},
selectSuite(suiteId) {
if (this.selectedSuite === suiteId) {
// already selected keep selection; use the disable button to clear
return
}
this.selectedSuite = suiteId
this.$emit('suite-selected', suiteId)
},
disableSuites() {
if (this.selectedSuite === null) {
return
}
this.selectedSuite = null
this.$emit('suite-selected', null)
},
},
}
</script>
<style lang="scss" scoped>
.office-suite-switcher {
padding: 20px;
@@ -134,13 +116,13 @@ export default {
text-align: center;
}
p {
margin: 8px 0;
p {
margin: 8px 0;
&:first-child {
font-weight: 600;
}
}
&:first-child {
font-weight: 600;
}
}
}
.office-suite-cards {
@@ -53,7 +53,7 @@ export default defineComponent({
computed: {
title() {
const appScore = (this.score * 5).toFixed(1)
return t('settings', 'Community rating: {score}/5', { score: appScore })
return t('appstore', 'Community rating: {score}/5', { score: appScore })
},
fullStars() {
@@ -7,18 +7,18 @@
<NcAppSidebarTab
v-if="app?.daemon"
id="daemon"
:name="t('settings', 'Daemon')"
:name="t('appstore', 'Daemon')"
:order="3">
<template #icon>
<NcIconSvgWrapper :path="mdiFileChart" :size="24" />
</template>
<div class="daemon">
<h4>{{ t('settings', 'Deploy Daemon') }}</h4>
<p><b>{{ t('settings', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p>
<p><b>{{ t('settings', 'Name') }}</b>: {{ app?.daemon.name }}</p>
<p><b>{{ t('settings', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p>
<p><b>{{ t('settings', 'GPUs support') }}</b>: {{ gpuSupport }}</p>
<p><b>{{ t('settings', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p>
<h4>{{ t('appstore', 'Deploy Daemon') }}</h4>
<p><b>{{ t('appstore', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p>
<p><b>{{ t('appstore', 'Name') }}</b>: {{ app?.daemon.name }}</p>
<p><b>{{ t('appstore', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p>
<p><b>{{ t('appstore', 'GPUs support') }}</b>: {{ gpuSupport }}</p>
<p><b>{{ t('appstore', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p>
</div>
</NcAppSidebarTab>
</template>
@@ -27,6 +27,7 @@
import type { IAppstoreExApp } from '../../app-types.ts'
import { mdiFileChart } from '@mdi/js'
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
@@ -7,17 +7,17 @@
<NcDialog
:open="show"
size="normal"
:name="t('settings', 'Advanced deploy options')"
:name="t('appstore', 'Advanced deploy options')"
@update:open="$emit('update:show', $event)">
<div class="modal__content">
<p class="deploy-option__hint">
{{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}.
{{ configuredDeployOptions === null ? t('appstore', 'Edit ExApp deploy options before installation') : t('appstore', 'Configured ExApp deploy options. Can be set only during installation') }}.
<a v-if="deployOptionsDocsUrl" :href="deployOptionsDocsUrl">
{{ t('settings', 'Learn more') }}
{{ t('appstore', 'Learn more') }}
</a>
</p>
<h3 v-if="environmentVariables.length > 0 || (configuredDeployOptions !== null && configuredDeployOptions.environment_variables.length > 0)">
{{ t('settings', 'Environment variables') }}
{{ t('appstore', 'Environment variables') }}
</h3>
<template v-if="configuredDeployOptions === null">
<div
@@ -34,7 +34,7 @@
v-else-if="Object.keys(configuredDeployOptions).length > 0"
class="envs">
<legend class="deploy-option__hint">
{{ t('settings', 'ExApp container environment variables') }}
{{ t('appstore', 'ExApp container environment variables') }}
</legend>
<NcTextField
v-for="(value, key) in configuredDeployOptions.environment_variables"
@@ -46,28 +46,28 @@
</fieldset>
<template v-else>
<p class="deploy-option__hint">
{{ t('settings', 'No environment variables defined') }}
{{ t('appstore', 'No environment variables defined') }}
</p>
</template>
<h3>{{ t('settings', 'Mounts') }}</h3>
<h3>{{ t('appstore', 'Mounts') }}</h3>
<template v-if="configuredDeployOptions === null">
<p class="deploy-option__hint">
{{ t('settings', 'Define host folder mounts to bind to the ExApp container') }}
{{ t('appstore', 'Define host folder mounts to bind to the ExApp container') }}
</p>
<NcNoteCard type="info" :text="t('settings', 'Must exist on the Deploy daemon host prior to installing the ExApp')" />
<NcNoteCard type="info" :text="t('appstore', 'Must exist on the Deploy daemon host prior to installing the ExApp')" />
<div
v-for="mount in deployOptions.mounts"
:key="mount.hostPath"
class="deploy-option"
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
<NcTextField v-model="mount.hostPath" :label="t('settings', 'Host path')" />
<NcTextField v-model="mount.containerPath" :label="t('settings', 'Container path')" />
<NcTextField v-model="mount.hostPath" :label="t('appstore', 'Host path')" />
<NcTextField v-model="mount.containerPath" :label="t('appstore', 'Container path')" />
<NcCheckboxRadioSwitch v-model="mount.readonly">
{{ t('settings', 'Read-only') }}
{{ t('appstore', 'Read-only') }}
</NcCheckboxRadioSwitch>
<NcButton
:aria-label="t('settings', 'Remove mount')"
:aria-label="t('appstore', 'Remove mount')"
style="margin-top: 6px;"
@click="removeMount(mount)">
<template #icon>
@@ -77,73 +77,73 @@
</div>
<div v-if="addingMount" class="deploy-option">
<h4>
{{ t('settings', 'New mount') }}
{{ t('appstore', 'New mount') }}
</h4>
<div style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
<NcTextField
ref="newMountHostPath"
v-model="newMountPoint.hostPath"
:label="t('settings', 'Host path')"
:aria-label="t('settings', 'Enter path to host folder')" />
:label="t('appstore', 'Host path')"
:aria-label="t('appstore', 'Enter path to host folder')" />
<NcTextField
v-model="newMountPoint.containerPath"
:label="t('settings', 'Container path')"
:aria-label="t('settings', 'Enter path to container folder')" />
:label="t('appstore', 'Container path')"
:aria-label="t('appstore', 'Enter path to container folder')" />
<NcCheckboxRadioSwitch
v-model="newMountPoint.readonly"
:aria-label="t('settings', 'Toggle read-only mode')">
{{ t('settings', 'Read-only') }}
:aria-label="t('appstore', 'Toggle read-only mode')">
{{ t('appstore', 'Read-only') }}
</NcCheckboxRadioSwitch>
</div>
<div style="display: flex; align-items: center; margin-top: 4px;">
<NcButton
:aria-label="t('settings', 'Confirm adding new mount')"
:aria-label="t('appstore', 'Confirm adding new mount')"
@click="addMountPoint">
<template #icon>
<NcIconSvgWrapper :path="mdiCheck" />
</template>
{{ t('settings', 'Confirm') }}
{{ t('appstore', 'Confirm') }}
</NcButton>
<NcButton
:aria-label="t('settings', 'Cancel adding mount')"
:aria-label="t('appstore', 'Cancel adding mount')"
style="margin-left: 4px;"
@click="cancelAddMountPoint">
<template #icon>
<NcIconSvgWrapper :path="mdiClose" />
</template>
{{ t('settings', 'Cancel') }}
{{ t('appstore', 'Cancel') }}
</NcButton>
</div>
</div>
<NcButton
v-if="!addingMount"
:aria-label="t('settings', 'Add mount')"
:aria-label="t('appstore', 'Add mount')"
style="margin-top: 5px;"
@click="startAddingMount">
<template #icon>
<NcIconSvgWrapper :path="mdiPlus" />
</template>
{{ t('settings', 'Add mount') }}
{{ t('appstore', 'Add mount') }}
</NcButton>
</template>
<template v-else-if="configuredDeployOptions.mounts.length > 0">
<p class="deploy-option__hint">
{{ t('settings', 'ExApp container mounts') }}
{{ t('appstore', 'ExApp container mounts') }}
</p>
<div
v-for="mount in configuredDeployOptions.mounts"
:key="mount.hostPath"
class="deploy-option"
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
<NcTextField v-model="mount.hostPath" :label="t('settings', 'Host path')" readonly />
<NcTextField v-model="mount.containerPath" :label="t('settings', 'Container path')" readonly />
<NcTextField v-model="mount.hostPath" :label="t('appstore', 'Host path')" readonly />
<NcTextField v-model="mount.containerPath" :label="t('appstore', 'Container path')" readonly />
<NcCheckboxRadioSwitch v-model="mount.readonly" disabled>
{{ t('settings', 'Read-only') }}
{{ t('appstore', 'Read-only') }}
</NcCheckboxRadioSwitch>
</div>
</template>
<p v-else class="deploy-option__hint">
{{ t('settings', 'No mounts defined') }}
{{ t('appstore', 'No mounts defined') }}
</p>
</div>
@@ -165,6 +165,7 @@ import { mdiCheck, mdiClose, mdiDeleteOutline, mdiPlus } from '@mdi/js'
import axios from '@nextcloud/axios'
import { emit } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { computed, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
@@ -222,6 +223,8 @@ export default {
})
return {
t,
environmentVariables,
deployOptions,
store,
@@ -244,7 +247,7 @@ export default {
addingPortBinding: false,
configuredDeployOptions: null,
deployOptionsDocsUrl: loadState('settings', 'deployOptionsDocsUrl', null),
deployOptionsDocsUrl: loadState('appstore', 'deployOptionsDocsUrl', null),
}
},
@@ -6,7 +6,7 @@
<template>
<NcAppSidebarTab
id="desc"
:name="t('settings', 'Description')"
:name="t('appstore', 'Description')"
:order="0">
<template #icon>
<NcIconSvgWrapper :path="mdiTextShort" />
@@ -24,7 +24,7 @@ import { mdiTextShort } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import Markdown from '../Markdown.vue'
import MarkdownPreview from '../MarkdownPreview.vue'
defineProps<{
app: IAppstoreApp
@@ -6,7 +6,7 @@
<template>
<NcAppSidebarTab
id="details"
:name="t('settings', 'Details')"
:name="t('appstore', 'Details')"
:order="1">
<template #icon>
<NcIconSvgWrapper :path="mdiTextBoxOutline" />
@@ -21,15 +21,15 @@
:value="app.id"
class="groups-enable__checkbox checkbox"
@change="setGroupLimit">
<label :for="`groups_enable_${app.id}`">{{ t('settings', 'Limit to groups') }}</label>
<label :for="`groups_enable_${app.id}`">{{ t('appstore', 'Limit to groups') }}</label>
<input
type="hidden"
class="group_select"
:title="t('settings', 'All')"
:title="t('appstore', 'All')"
value="">
<br>
<label for="limitToGroups">
<span>{{ t('settings', 'Limit app usage to groups') }}</span>
<span>{{ t('appstore', 'Limit app usage to groups') }}</span>
</label>
<NcSelect
v-if="isLimitedToGroups(app)"
@@ -43,7 +43,7 @@
@option:selected="addGroupLimitation"
@option:deselected="removeGroupLimitation"
@search="asyncFindGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
<span slot="noResult">{{ t('appstore', 'No results') }}</span>
</NcSelect>
</div>
<div class="app-details__actions-manage">
@@ -51,14 +51,14 @@
v-if="app.update"
class="update primary"
type="button"
:value="t('settings', 'Update to {version}', { version: app.update })"
:value="t('appstore', 'Update to {version}', { version: app.update })"
:disabled="installing || isLoading || isManualInstall"
@click="update(app.id)">
<input
v-if="app.canUnInstall"
class="uninstall"
type="button"
:value="t('settings', 'Remove')"
:value="t('appstore', 'Remove')"
:disabled="installing || isLoading"
@click="remove(app.id, removeData)">
<input
@@ -88,36 +88,36 @@
@click="forceEnable(app.id)">
<NcButton
v-if="app?.app_api && (app.canInstall || app.isCompatible)"
:aria-label="t('settings', 'Advanced deploy options')"
:aria-label="t('appstore', 'Advanced deploy options')"
variant="secondary"
@click="() => showDeployOptionsModal = true">
<template #icon>
<NcIconSvgWrapper :path="mdiToyBrickPlusOutline" />
</template>
{{ t('settings', 'Deploy options') }}
{{ t('appstore', 'Deploy options') }}
</NcButton>
</div>
<p v-if="!defaultDeployDaemonAccessible" class="warning">
{{ t('settings', 'Default Deploy daemon is not accessible') }}
{{ t('appstore', 'Default Deploy daemon is not accessible') }}
</p>
<NcCheckboxRadioSwitch
v-if="app.canUnInstall"
:model-value="removeData"
:disabled="installing || isLoading || !defaultDeployDaemonAccessible"
@update:modelValue="toggleRemoveData">
{{ t('settings', 'Delete data on remove') }}
{{ t('appstore', 'Delete data on remove') }}
</NcCheckboxRadioSwitch>
</div>
<ul class="app-details__dependencies">
<li v-if="app.missingMinOwnCloudVersion">
{{ t('settings', 'This app has no minimum {productName} version assigned. This will be an error in the future.', { productName }) }}
{{ t('appstore', 'This app has no minimum {productName} version assigned. This will be an error in the future.', { productName }) }}
</li>
<li v-if="app.missingMaxOwnCloudVersion">
{{ t('settings', 'This app has no maximum {productName} version assigned. This will be an error in the future.', { productName }) }}
{{ t('appstore', 'This app has no maximum {productName} version assigned. This will be an error in the future.', { productName }) }}
</li>
<li v-if="!app.canInstall">
{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
{{ t('appstore', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
<ul class="missing-dependencies">
<li v-for="(dep, index) in app.missingDependencies" :key="index">
{{ dep }}
@@ -128,14 +128,14 @@
<div v-if="lastModified && !app.shipped" class="app-details__section">
<h4>
{{ t('settings', 'Latest updated') }}
{{ t('appstore', 'Latest updated') }}
</h4>
<NcDateTime :timestamp="lastModified" />
</div>
<div class="app-details__section">
<h4>
{{ t('settings', 'Author') }}
{{ t('appstore', 'Author') }}
</h4>
<p class="app-details__authors">
{{ appAuthors }}
@@ -144,7 +144,7 @@
<div class="app-details__section">
<h4>
{{ t('settings', 'Categories') }}
{{ t('appstore', 'Categories') }}
</h4>
<p>
{{ appCategories }}
@@ -152,8 +152,8 @@
</div>
<div v-if="externalResources.length > 0" class="app-details__section">
<h4>{{ t('settings', 'Resources') }}</h4>
<ul class="app-details__documentation" :aria-label="t('settings', 'Documentation')">
<h4>{{ t('appstore', 'Resources') }}</h4>
<ul class="app-details__documentation" :aria-label="t('appstore', 'Documentation')">
<li v-for="resource of externalResources" :key="resource.id">
<a
class="appslink"
@@ -167,13 +167,13 @@
</div>
<div class="app-details__section">
<h4>{{ t('settings', 'Interact') }}</h4>
<h4>{{ t('appstore', 'Interact') }}</h4>
<div class="app-details__interact">
<NcButton
:disabled="!app.bugs"
:href="app.bugs ?? '#'"
:aria-label="t('settings', 'Report a bug')"
:title="t('settings', 'Report a bug')">
:aria-label="t('appstore', 'Report a bug')"
:title="t('appstore', 'Report a bug')">
<template #icon>
<NcIconSvgWrapper :path="mdiBugOutline" />
</template>
@@ -181,8 +181,8 @@
<NcButton
:disabled="!app.bugs"
:href="app.bugs ?? '#'"
:aria-label="t('settings', 'Request feature')"
:title="t('settings', 'Request feature')">
:aria-label="t('appstore', 'Request feature')"
:title="t('appstore', 'Request feature')">
<template #icon>
<NcIconSvgWrapper :path="mdiFeatureSearchOutline" />
</template>
@@ -190,8 +190,8 @@
<NcButton
v-if="app.appstoreData?.discussion"
:href="app.appstoreData.discussion"
:aria-label="t('settings', 'Ask questions or discuss')"
:title="t('settings', 'Ask questions or discuss')">
:aria-label="t('appstore', 'Ask questions or discuss')"
:title="t('appstore', 'Ask questions or discuss')">
<template #icon>
<NcIconSvgWrapper :path="mdiTooltipQuestionOutline" />
</template>
@@ -199,8 +199,8 @@
<NcButton
v-if="!app.internal"
:href="rateAppUrl"
:aria-label="t('settings', 'Rate the app')"
:title="t('settings', 'Rate')">
:aria-label="t('appstore', 'Rate the app')"
:title="t('appstore', 'Rate')">
<template #icon>
<NcIconSvgWrapper :path="mdiStar" />
</template>
@@ -335,14 +335,14 @@ export default {
resources.push({
id: 'appstore',
href: this.appstoreUrl,
label: t('settings', 'View in store'),
label: t('appstore', 'View in store'),
})
}
if (this.app.website) {
resources.push({
id: 'website',
href: this.app.website,
label: t('settings', 'Visit website'),
label: t('appstore', 'Visit website'),
})
}
if (this.app.documentation) {
@@ -350,21 +350,21 @@ export default {
resources.push({
id: 'doc-user',
href: this.app.documentation.user,
label: t('settings', 'Usage documentation'),
label: t('appstore', 'Usage documentation'),
})
}
if (this.app.documentation.admin) {
resources.push({
id: 'doc-admin',
href: this.app.documentation.admin,
label: t('settings', 'Admin documentation'),
label: t('appstore', 'Admin documentation'),
})
}
if (this.app.documentation.developer) {
resources.push({
id: 'doc-developer',
href: this.app.documentation.developer,
label: t('settings', 'Developer documentation'),
label: t('appstore', 'Developer documentation'),
})
}
}
@@ -6,14 +6,14 @@
<NcAppSidebarTab
v-if="hasChangelog"
id="changelog"
:name="t('settings', 'Changelog')"
:name="t('appstore', 'Changelog')"
:order="2">
<template #icon>
<NcIconSvgWrapper :path="mdiClockFast" :size="24" />
</template>
<div v-for="release in app.releases" :key="release.version" class="app-sidebar-tabs__release">
<h2>{{ release.version }}</h2>
<Markdown
<MarkdownPreview
class="app-sidebar-tabs__release-text"
:text="createChangelogFromRelease(release)" />
</div>
@@ -28,7 +28,7 @@ import { getLanguage, translate as t } from '@nextcloud/l10n'
import { computed } from 'vue'
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import Markdown from '../Markdown.vue'
import MarkdownPreview from '../MarkdownPreview.vue'
const props = defineProps<{ app: IAppstoreApp }>()
@@ -0,0 +1,50 @@
<!--
- 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>
@@ -0,0 +1,149 @@
<!--
- 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>
@@ -0,0 +1,59 @@
<!--
- 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>

Some files were not shown because too many files have changed in this diff Show More