Compare commits

..

52 Commits

Author SHA1 Message Date
Nextcloud bot
d76f1841d3 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-09 00:49:34 +00:00
Nextcloud bot
16a9f0861a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-07 00:31:53 +00:00
Nextcloud bot
0802911d13 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-04 00:31:17 +00:00
Nextcloud bot
70b4d62e54 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-03 00:33:31 +00:00
Nextcloud bot
154ed8bf84 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-02 00:32:23 +00:00
Nextcloud bot
c3a5833d5a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-10-01 00:32:03 +00:00
Nextcloud bot
0dfba6f1c8 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-30 00:32:02 +00:00
Luka Trovic
c3df4b112f Merge pull request #7282 from nextcloud/release/1.16.0
Release/1.16.0
2025-09-29 10:57:04 +02:00
Luka Trovic
703b6d3062 release 1.16.0
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-29 10:39:35 +02:00
Nextcloud bot
4bb06aa7f5 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-28 00:31:37 +00:00
Nextcloud bot
7e995ab110 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-27 00:32:14 +00:00
Luka Trovic
a2046e9a2e Merge pull request #7276 from nextcloud/backport/7154/stable32
[stable32] fix: parse arguments to CardService.reorder correctly to int
2025-09-26 18:34:30 +02:00
Viktor Diezel
0262b9ab76 fix: parse arguments to CardService.reorder correctly to int
Signed-off-by: Viktor Diezel <viktor.diezel@posteo.de>
2025-09-26 13:28:25 +00:00
Luka Trovic
b999366fb4 Merge pull request #7273 from nextcloud/backport/7261/stable32
[stable32] fix: use text cursor for card title on dashboard
2025-09-26 12:02:22 +02:00
grnd-alt
9921c5446c fix: use text cursor for card title on dashboard
Signed-off-by: grnd-alt <github@belakkaf.net>

[skip ci]
2025-09-26 05:42:11 +00:00
Nextcloud bot
4c6e9b086a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-26 00:32:18 +00:00
Luka Trovic
ad9c1eb9af Merge pull request #7271 from nextcloud/32-bump-nextcloud-vue-8.31.0
Chore(deps): Bump @nextcloud/vue from 8.27.0 to 8.31.0
2025-09-25 22:59:45 +02:00
Luka Trovic
da0f407121 Chore(deps): Bump @nextcloud/vue from 8.27.0 to 8.31.0
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-25 18:44:01 +02:00
Luka Trovic
5d59047cbd Merge pull request #7270 from nextcloud/backport/7255/stable32
[stable32] fix: missing push notifications
2025-09-25 11:40:50 +02:00
Luka Trovic
c3c19bcc09 chore: update base query count
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-25 09:16:50 +00:00
Luka Trovic
c078e3c241 fix: missing push notifications
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-25 09:16:50 +00:00
Nextcloud bot
95cc40cc38 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-25 00:31:42 +00:00
Luka Trovic
649f995c6b Merge pull request #7268 from nextcloud/backport/7266/stable32
[stable32] fix: redirect to cleaner URL if RewriteBase is enabled
2025-09-24 21:19:40 +02:00
Luka Trovic
9ba7652212 fix: redirect to cleaner URL if RewriteBase is enabled
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-24 18:28:37 +00:00
Nextcloud bot
4eea72af13 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-24 00:31:54 +00:00
Nextcloud bot
b06ac63dbd fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-23 00:31:52 +00:00
Nextcloud bot
1ccfb6a13a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-22 00:31:32 +00:00
Luka Trovic
86a4d5b2c8 Merge pull request #7214 from nextcloud/automated/noid/stable32-update-nextcloud-ocp
[stable32] Update nextcloud/ocp dependency
2025-09-19 19:10:20 +02:00
nextcloud-command
3bc7d9dccf chore(dev-deps): Bump nextcloud/ocp package
Signed-off-by: GitHub <noreply@github.com>
2025-09-19 19:03:45 +02:00
Nextcloud bot
2a82bc6a63 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-19 00:32:03 +00:00
Nextcloud bot
af03e841b1 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-18 00:31:45 +00:00
Nextcloud bot
b01c545fd0 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-17 00:31:45 +00:00
Luka Trovic
a0ee1684b1 Merge pull request #7241 from nextcloud/backport/7238/stable32
[stable32] fix(darkmode): Fix activity icon colors
2025-09-16 22:09:20 +02:00
Joas Schilling
b35279e801 fix(darkmode): Fix activity icon colors
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-09-16 21:40:59 +02:00
Luka Trovic
a2c03cebb5 Merge pull request #7239 from nextcloud/backport/7237/stable32
[stable32] fix: Fix colors from due dates and done
2025-09-16 21:39:43 +02:00
Andy Scherzinger
b391fa9edc Merge pull request #7211 from nextcloud/backport/7210/stable32
[stable32] Migrate delete icon to Material Symbol outline variant
2025-09-16 21:38:50 +02:00
Joas Schilling
824540bf98 fix: Fix colors from due dates and done
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-09-16 21:19:42 +02:00
Andy Scherzinger
22b8dbec80 style(icon): Migrate delete icon to Material Symbol outline variant
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-09-16 21:19:30 +02:00
Luka Trovic
b36bd37dd6 fix: set server version to stable32 for cypress
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2025-09-16 21:17:47 +02:00
Nextcloud bot
351b4f4165 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-16 00:31:51 +00:00
Nextcloud bot
7fb7558d9e fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-15 00:31:02 +00:00
Nextcloud bot
be9a6f29c6 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-14 00:30:55 +00:00
Nextcloud bot
a0bc896123 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-13 00:31:25 +00:00
Andy Scherzinger
f06e391e36 Merge pull request #7203 from nextcloud/update-stable32-target-versions
chore(CI): Adjust testing matrix for Nextcloud 32 on stable32
2025-09-12 18:27:53 +02:00
Nextcloud bot
ce0e6a4acc fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-12 00:31:50 +00:00
Nextcloud bot
997249fc7a fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-11 00:31:54 +00:00
Nextcloud bot
e48e0aa7ce fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-09 00:33:41 +00:00
Nextcloud bot
961053230d fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-08 00:32:15 +00:00
Nextcloud bot
16f0fa8c0c fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-07 00:33:32 +00:00
Nextcloud bot
daf6aeb650 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-06 00:31:28 +00:00
Joas Schilling
6861730a61 chore(CI): Adjust testing matrix for Nextcloud 32 on stable32
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-09-05 18:29:28 +02:00
Nextcloud bot
839d789f65 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-09-05 00:32:04 +00:00
122 changed files with 3901 additions and 4777 deletions

2
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,2 @@
# App maintainers
* @luka-nextcloud @grnd-alt @elzody

120
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@@ -0,0 +1,120 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
Thanks for reporting issues back!
Guidelines for submitting issues:
* Please search the existing issues first, it's likely that your issue was already reported or even fixed.
* SECURITY: Report any potential security bug to us via our HackerOne page (https://hackerone.com/nextcloud) following our security policy (https://nextcloud.com/security/) instead of filing an issue in our bug tracker.
* The issues in other components should be reported in their respective repositories: You will find them in our GitHub Organization (https://github.com/nextcloud/)
* You can also use the Issue Template app to prefill most of the required information: https://apps.nextcloud.com/apps/issuetemplate
-->
<!--- Please keep this note for other contributors -->
### How to use GitHub
* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue.
* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
* Subscribe to receive notifications on status change and new comments.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Client details:**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- Device: [e.g. iPhone6, desktop]
<details>
<summary>Server details</summary>
<!--
You can use the Issue Template application to prefill most of the required information: https://apps.nextcloud.com/apps/issuetemplate
-->
**Operating system**:
**Web server:**
**Database:**
**PHP version:**
**Nextcloud version:** (see Nextcloud admin page)
**Where did you install Nextcloud from:**
**Signing status:**
```
Login as admin user into your Nextcloud and access
http://example.com/index.php/settings/integrity/failed
paste the results here.
```
**List of activated apps:**
```
If you have access to your command line run e.g.:
sudo -u www-data php occ app:list
from within your Nextcloud installation folder
```
**Nextcloud configuration:**
```
If you have access to your command line run e.g.:
sudo -u www-data php occ config:list system
from within your Nextcloud installation folder
or
Insert your config.php content here
Make sure to remove all sensitive content such as passwords. (e.g. database password, passwordsalt, secret, smtp password, …)
```
**Are you using an external user-backend, if yes which one:** LDAP/ActiveDirectory/Webdav/...
</details>
<details>
<summary>Logs</summary>
#### Nextcloud log (data/nextcloud.log)
```
Insert your Nextcloud log here
```
#### Browser log
```
Insert your browser log here, this could for example include:
a) The javascript console log
b) The network log
c) ...
```
</details>

View File

@@ -0,0 +1,39 @@
---
name: Feature request
about: Suggest an idea for this project
---
<!--
Thanks for reporting issues back!
Guidelines for submitting issues:
* Please search the existing issues first, it's likely that your issue was already reported or even fixed.
* SECURITY: Report any potential security bug to us via our HackerOne page (https://hackerone.com/nextcloud) following our security policy (https://nextcloud.com/security/) instead of filing an issue in our bug tracker.
* The issues in other components should be reported in their respective repositories: You will find them in our GitHub Organization (https://github.com/nextcloud/)
* You can also use the Issue Template app to prefill most of the required information: https://apps.nextcloud.com/apps/issuetemplate
-->
<!--- Please keep this note for other contributors -->
### How to use GitHub
* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue.
* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
* Subscribe to receive notifications on status change and new comments.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

84
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
target-branch: "main"
schedule:
interval: weekly
day: saturday
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- luka-nextcloud
- package-ecosystem: npm
target-branch: stable31
versioning-strategy: lockfile-only
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:15"
timezone: Europe/Paris
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 30
labels:
- 3. to review
- dependencies
- package-ecosystem: npm
target-branch: stable30
versioning-strategy: lockfile-only
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:30"
timezone: Europe/Paris
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 30
labels:
- 3. to review
- dependencies
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:45"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- luka-nextcloud
- package-ecosystem: composer
directory: "/tests/integration"
schedule:
interval: weekly
day: saturday
time: "04:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- luka-nextcloud
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: saturday
time: "04:15"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- luka-nextcloud

17
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,17 @@
* Resolves: # <!-- related github issue -->
* Target version: main
### Summary
### TODO
- [ ] ...
### Checklist
- [ ] Code is properly formatted
- [ ] Sign-off message is added to all commits
- [ ] Tests (unit, integration, api and/or acceptance) are included
- [ ] Documentation (manuals or wiki) has been updated or is not required

View File

@@ -2,11 +2,10 @@ name: Package build
on:
push:
tags:
- 'mod*'
permissions:
contents: write
branches:
- main
- master
- stable*
jobs:
build:
@@ -31,23 +30,14 @@ jobs:
tools: composer
- name: install dependencies
run: |
wget https://github.com/ChristophWurst/krankerl/releases/download/v0.13.3/krankerl_0.13.3_amd64.deb
sudo dpkg -i krankerl_0.13.3_amd64.deb
wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb
sudo dpkg -i krankerl_0.14.0_amd64.deb
- name: package
run: |
uname -a
RUST_BACKTRACE=1 krankerl --version
RUST_BACKTRACE=1 krankerl package
- uses: https://data.forgejo.org/forgejo/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: Deck app tarball
path: build/artifacts/deck.tar.gz
- name: Attach tarball to release
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # v2
id: attach_to_release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/artifacts/deck.tar.gz
asset_name: deck.tar.gz
tag: ${{ github.ref }}
overwrite: true

View File

@@ -0,0 +1,191 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Build and publish app release
on:
release:
types: [published]
permissions:
contents: write
jobs:
build_and_publish:
runs-on: ubuntu-latest
# Only allowed to be run on nextcloud-releases repositories
if: ${{ github.repository_owner == 'nextcloud-releases' }}
steps:
- name: Check actor permission
uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0
with:
require: write
- name: Set app env
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
path: ${{ env.APP_NAME }}
- name: Get app version number
id: app-version
uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # master
with:
filename: ${{ env.APP_NAME }}/appinfo/info.xml
expression: "//info//version/text()"
- name: Validate app version against tag
run: |
[ "${{ env.APP_VERSION }}" = "v${{ fromJSON(steps.app-version.outputs.result).version }}" ]
- name: Get appinfo data
id: appinfo
uses: skjnldsv/xpath-action@f5b036e9d973f42c86324833fd00be90665fbf77 # master
with:
filename: ${{ env.APP_NAME }}/appinfo/info.xml
expression: "//info//dependencies//nextcloud/@min-version"
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
# Continue if no package.json
continue-on-error: true
with:
path: ${{ env.APP_NAME }}
fallbackNode: '^20'
fallbackNpm: '^10'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
# Skip if no package.json
if: ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.1.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
# Skip if no package.json
if: ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- name: Get php version
id: php-versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
with:
filename: ${{ env.APP_NAME }}/appinfo/info.xml
- name: Set up php ${{ steps.php-versions.outputs.php-min }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: ${{ steps.php-versions.outputs.php-min }}
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check composer.json
id: check_composer
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: "${{ env.APP_NAME }}/composer.json"
- name: Install composer dependencies
if: steps.check_composer.outputs.files_exists == 'true'
run: |
cd ${{ env.APP_NAME }}
composer install --no-dev
- name: Build ${{ env.APP_NAME }}
# Skip if no package.json
if: ${{ steps.versions.outputs.nodeVersion }}
env:
CYPRESS_INSTALL_BINARY: 0
run: |
cd ${{ env.APP_NAME }}
npm ci
npm run build --if-present
- name: Check Krankerl config
id: krankerl
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: ${{ env.APP_NAME }}/krankerl.toml
- name: Install Krankerl
if: steps.krankerl.outputs.files_exists == 'true'
run: |
wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb
sudo dpkg -i krankerl_0.14.0_amd64.deb
- name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl
if: steps.krankerl.outputs.files_exists == 'true'
run: |
cd ${{ env.APP_NAME }}
krankerl package
- name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with makefile
if: steps.krankerl.outputs.files_exists != 'true'
run: |
cd ${{ env.APP_NAME }}
make appstore
- name: Checkout server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
continue-on-error: true
id: server-checkout
run: |
NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}'
wget --quiet https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip
unzip latest-$NCVERSION.zip
- name: Checkout server master fallback
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
if: ${{ steps.server-checkout.outcome != 'success' }}
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
path: nextcloud
- name: Sign app
run: |
# Extracting release
cd ${{ env.APP_NAME }}/build/artifacts
tar -xvf ${{ env.APP_NAME }}.tar.gz
cd ../../../
# Setting up keys
echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key
wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt"
# Signing
php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}
# Rebuilding archive
cd ${{ env.APP_NAME }}/build/artifacts
tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }}
- name: Attach tarball to github release
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # v2
id: attach_to_release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz
asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz
tag: ${{ github.ref }}
overwrite: true
- name: Upload app to Nextcloud appstore
uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1
with:
app_name: ${{ env.APP_NAME }}
appstore_token: ${{ secrets.APPSTORE_TOKEN }}
download_url: ${{ steps.attach_to_release.outputs.browser_download_url }}
app_private_key: ${{ secrets.APP_PRIVATE_KEY }}

153
.github/workflows/cypress-e2e.yml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Cypress
on:
pull_request:
push:
branches:
- main
- stable*
env:
APP_NAME: deck
CYPRESS_baseUrl: http://localhost:8081/index.php
jobs:
cypress:
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
matrix:
node-version: [20.x]
# containers: [1, 2, 3]
php-versions: [ '8.2' ]
server-versions: [ 'stable32' ]
env:
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, zip, gd, apcu
key: cache-v1
services:
postgres:
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
- name: Register text Git reference
run: |
text_app_ref="$(if [ "${{ matrix.server-versions }}" = "master" ]; then echo -n "main"; else echo -n "${{ matrix.server-versions }}"; fi)"
echo "text_app_ref=$text_app_ref" >> $GITHUB_ENV
- name: Checkout server
uses: actions/checkout@v4.2.2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout ${{ env.APP_NAME }}
uses: actions/checkout@v4.2.2
with:
path: apps/${{ env.APP_NAME }}
- name: Checkout text
uses: actions/checkout@v4.2.2
with:
repository: nextcloud/text
ref: ${{ env.text_app_ref }}
path: apps/text
- name: Setup cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
key: ${{ env.key }}
- name: Cache extensions
uses: actions/cache@v4
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.34.1
with:
php-version: ${{ matrix.php-versions }}
extensions: ${{ env.extensions }}
ini-values:
apc.enable_cli=on
coverage: none
- name: Install composer dependencies
working-directory: apps/${{ env.APP_NAME }}
run: |
composer install --no-dev
- name: Set up Nextcloud
env:
DB_PORT: 4444
PHP_CLI_SERVER_WORKERS: 20
run: |
mkdir data
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
php occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
php occ background:cron
php -f index.php
php -S 0.0.0.0:8081 &
export OC_PASS=1234561
php occ user:add --password-from-env user1
php occ user:add --password-from-env user2
php occ app:enable deck
php occ app:list
curl -v http://localhost:8081/index.php/login
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run dev
record: false
parallel: false
browser: chrome
wait-on: '${{ env.CYPRESS_baseUrl }}'
working-directory: 'apps/${{ env.APP_NAME }}'
config: defaultCommandTimeout=10000,video=false
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
npm_package_name: ${{ env.APP_NAME }}
- name: Upload test failure screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: Upload screenshots
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
retention-days: 5
- name: Upload nextcloud logs
uses: actions/upload-artifact@v4
if: failure()
with:
name: Upload nextcloud log
path: data/nextcloud.log
retention-days: 5

View File

@@ -0,0 +1,49 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Dependabot
on:
pull_request_target: # zizmor: ignore[dangerous-triggers]
branches:
- main
- master
- stable*
permissions:
contents: read
concurrency:
group: dependabot-approve-merge-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
auto-approve-merge:
if: github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'renovate[bot]'
runs-on: ubuntu-latest-low
permissions:
# for hmarr/auto-approve-action to approve PRs
pull-requests: write
steps:
- name: Disabled on forks
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
run: |
echo 'Can not approve PRs from forks'
exit 1
# GitHub actions bot approve
- uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@45fc124d949b19b6b8bf6645b6c9d55f4f9ac61a # v2
with:
target: minor
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}

36
.github/workflows/fixup.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Block fixup and squash commits
on:
pull_request:
types: [opened, ready_for_review, reopened, synchronize]
permissions:
contents: read
concurrency:
group: fixup-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
commit-message-check:
if: github.event.pull_request.draft == false
permissions:
pull-requests: write
name: Block fixup and squash commits
runs-on: ubuntu-latest-low
steps:
- name: Run check
uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

165
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,165 @@
name: Integration tests
on:
pull_request:
paths:
- '.github/workflows/integration.yml'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
push:
branches:
- main
- master
- stable*
env:
APP_NAME: deck
jobs:
integration:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ['8.1']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['stable32']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
services:
postgres:
image: postgres:14
ports:
- 4445:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
mysql:
image: mariadb:10.5
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout server
uses: actions/checkout@v4.2.2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
cd build/integration && composer require --dev phpunit/phpunit:~9
- name: Checkout app
uses: actions/checkout@v4.2.2
with:
path: apps/${{ env.APP_NAME }}
- name: Checkout activity
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: nextcloud/activity
ref: ${{ matrix.server-versions }}
path: apps/activity
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.34.1
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, apcu, gd
ini-values:
apc.enable_cli=on
coverage: none
- name: Set up dependencies
working-directory: apps/${{ env.APP_NAME }}
run: composer i --no-dev
- name: Set up Nextcloud
run: |
if [ "${{ matrix.databases }}" = "mysql" ]; then
export DB_PORT=4444
elif [ "${{ matrix.databases }}" = "pgsql" ]; then
export DB_PORT=4445
fi
mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
./occ config:system:set memcache.distributed --value="\\OC\\Memcache\\APCu"
cat config/config.php
./occ user:list
./occ app:enable --force ${{ env.APP_NAME }}
./occ config:system:set query_log_file --value "$PWD/query.log"
php -S localhost:8080 &
- name: Run behat
working-directory: apps/${{ env.APP_NAME }}/tests/integration
run: ./run.sh
- name: Print log
if: always()
run: cat data/nextcloud.log
- name: Query count
if: ${{ matrix.databases == 'mysql' }}
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
let myOutput = ''
let myError = ''
const options = {}
options.listeners = {
stdout: (data) => {
myOutput += data.toString()
},
stderr: (data) => {
myError += data.toString()
}
}
await exec.exec(`/bin/bash -c "cat query.log | wc -l"`, [], options)
msg = myOutput
const queryCount = parseInt(myOutput, 10)
myOutput = ''
await exec.exec('cat', ['apps/${{ env.APP_NAME }}/tests/integration/base-query-count.txt'], options)
const baseCount = parseInt(myOutput, 10)
const absoluteIncrease = queryCount - baseCount
const relativeIncrease = baseCount <= 0 ? 100 : (parseInt((absoluteIncrease / baseCount * 10000), 10) / 100)
if (absoluteIncrease >= 100 || relativeIncrease > 5) {
const comment = `🐢 Performance warning.\nIt looks like the query count of the integration tests increased with this PR.\nDatabase query count is now ` + queryCount + ' was ' + baseCount + ' (+' + relativeIncrease + '%)\nPlease check your code again. If you added a new test this can be expected and the base value in tests/integration/base-query-count.txt can be increased.'
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
}
if (queryCount < 100) {
const comment = `🐈 Performance messuring seems broken. Failed to get query count.`
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
}

100
.github/workflows/lint-eslint.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint eslint
on: pull_request
permissions:
contents: read
concurrency:
group: lint-eslint-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest-low
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- 'src/**'
- 'appinfo/info.xml'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- '.eslintrc.*'
- '.eslintignore'
- '**.js'
- '**.ts'
- '**.vue'
lint:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src != 'false'
name: NPM lint
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^10'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.1.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_DOWNLOAD: true
run: npm ci
- name: Lint
run: npm run lint
summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: [changes, lint]
if: always()
# This is the summary, we just avoid to rename it so that branch protection rules still match
name: eslint
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi

52
.github/workflows/lint-php-cs.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint php-cs
on: pull_request
permissions:
contents: read
concurrency:
group: lint-php-cs-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
name: php-cs
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get php version
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
- name: Set up php${{ steps.versions.outputs.php-min }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: ${{ steps.versions.outputs.php-min }}
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
composer remove nextcloud/ocp --dev
composer i
- name: Lint
run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )

75
.github/workflows/lint-php.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint php
on: pull_request
permissions:
contents: read
concurrency:
group: lint-php-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
matrix:
runs-on: ubuntu-latest-low
outputs:
php-versions: ${{ steps.versions.outputs.php-versions }}
steps:
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.0.0
php-lint:
runs-on: ubuntu-latest
needs: matrix
strategy:
matrix:
php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}}
name: php-lint
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: ${{ matrix.php-versions }}
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Lint
run: composer run lint
summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: php-lint
if: always()
name: php-lint-summary
steps:
- name: Summary status
run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi

53
.github/workflows/lint-stylelint.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Lint stylelint
on: pull_request
permissions:
contents: read
concurrency:
group: lint-stylelint-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
name: stylelint
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^10'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.1.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
run: npm ci
- name: Lint
run: npm run stylelint

34
.github/workflows/nodejs.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v4.2.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
run: npm i -g npm@7
- name: install dependencies
run: |
npm ci
- name: build
env:
RELATIVE_CI_KEY: ${{ secrets.RELATIVE_CI_KEY }}
RELATIVE_CI_SLUG: nextcloud/deck
run: |
mkdir -p js
npm run build --if-present -- --profile --json | tail -n +6 > js/webpack-stats.json
npx relative-ci-agent

81
.github/workflows/npm-audit-fix.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Npm audit fix and compile
on:
workflow_dispatch:
schedule:
# At 2:30 on Sundays
- cron: '30 2 * * 0'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branches: ['main', 'master', 'stable31', 'stable30']
name: npm-audit-fix-${{ matrix.branches }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
ref: ${{ matrix.branches }}
continue-on-error: true
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^10'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.1.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- name: Fix npm audit
id: npm-audit
uses: nextcloud-libraries/npm-audit-action@1b1728b2b4a7a78d69de65608efcf4db0e3e42d0 # v0.2.0
- name: Run npm ci and npm run build
if: steps.checkout.outcome == 'success'
env:
CYPRESS_INSTALL_BINARY: 0
run: |
npm ci
npm run build --if-present
- name: Create Pull Request
if: steps.checkout.outcome == 'success'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
commit-message: 'fix(deps): Fix npm audit'
committer: GitHub <noreply@github.com>
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
signoff: true
branch: automated/noid/${{ matrix.branches }}-fix-npm-audit
title: '[${{ matrix.branches }}] Fix npm audit'
body: ${{ steps.npm-audit.outputs.markdown }}
labels: |
dependencies
3. to review

200
.github/workflows/phpunit-mysql.yml vendored Normal file
View File

@@ -0,0 +1,200 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit MySQL
on: pull_request
permissions:
contents: read
concurrency:
group: phpunit-mysql-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
matrix:
runs-on: ubuntu-latest-low
outputs:
matrix: ${{ steps.versions.outputs.sparse-matrix }}
steps:
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
with:
matrix: '{"mysql-versions": ["8.4"]}'
changes:
runs-on: ubuntu-latest-low
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
phpunit-mysql:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src != 'false'
strategy:
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
name: MySQL ${{ matrix.mysql-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }}
services:
mysql:
image: ghcr.io/nextcloud/continuous-integration-mysql-${{ matrix.mysql-versions }}:latest
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10
steps:
- name: Set app env
if: ${{ env.APP_NAME == '' }}
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Checkout server
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
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, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql
coverage: none
ini-file: development
# Temporary workaround for missing pcntl_* in PHP 8.3
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable ONLY_FULL_GROUP_BY MySQL option
run: |
echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword
echo 'SELECT @@sql_mode;' | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up dependencies
# Only run if phpunit config file exists
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev
composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit script is defined
id: check_phpunit
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:unit ' | wc -l | grep 1
- name: PHPUnit
# Only run if phpunit config file exists
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
- name: Skipped
# Fail the action when neither unit nor integration tests ran
if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
run: |
echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: [changes, phpunit-mysql]
if: always()
name: phpunit-mysql-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-mysql.result != 'success' }}; then exit 1; fi

198
.github/workflows/phpunit-pgsql.yml vendored Normal file
View File

@@ -0,0 +1,198 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit PostgreSQL
on: pull_request
permissions:
contents: read
concurrency:
group: phpunit-pgsql-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
matrix:
runs-on: ubuntu-latest-low
outputs:
php-version: ${{ steps.versions.outputs.php-available-list }}
server-max: ${{ steps.versions.outputs.branches-max-list }}
steps:
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
changes:
runs-on: ubuntu-latest-low
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
phpunit-pgsql:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src != 'false'
strategy:
matrix:
php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }}
server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }}
name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }}
services:
postgres:
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Set app env
if: ${{ env.APP_NAME == '' }}
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Checkout server
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
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, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql
coverage: none
ini-file: development
# Temporary workaround for missing pcntl_* in PHP 8.3
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up dependencies
# Only run if phpunit config file exists
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev
composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit script is defined
id: check_phpunit
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:unit ' | wc -l | grep 1
- name: PHPUnit
# Only run if phpunit config file exists
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
- name: Skipped
# Fail the action when neither unit nor integration tests ran
if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
run: |
echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: [changes, phpunit-pgsql]
if: always()
name: phpunit-pgsql-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-pgsql.result != 'success' }}; then exit 1; fi

187
.github/workflows/phpunit-sqlite.yml vendored Normal file
View File

@@ -0,0 +1,187 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit SQLite
on: pull_request
permissions:
contents: read
concurrency:
group: phpunit-sqlite-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
matrix:
runs-on: ubuntu-latest-low
outputs:
php-version: ${{ steps.versions.outputs.php-available-list }}
server-max: ${{ steps.versions.outputs.branches-max-list }}
steps:
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get version matrix
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
changes:
runs-on: ubuntu-latest-low
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.changes.outputs.src}}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
phpunit-sqlite:
runs-on: ubuntu-latest
needs: [changes, matrix]
if: needs.changes.outputs.src != 'false'
strategy:
matrix:
php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }}
server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }}
name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }}
steps:
- name: Set app env
if: ${{ env.APP_NAME == '' }}
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Checkout server
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
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, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
# Temporary workaround for missing pcntl_* in PHP 8.3
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up dependencies
# Only run if phpunit config file exists
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev
composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit script is defined
id: check_phpunit
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:unit ' | wc -l | grep 1
- name: PHPUnit
# Only run if phpunit config file exists
if: steps.check_phpunit.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:unit
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
- name: Skipped
# Fail the action when neither unit nor integration tests ran
if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
run: |
echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
exit 1
summary:
permissions:
contents: none
runs-on: ubuntu-latest-low
needs: [changes, phpunit-sqlite]
if: always()
name: phpunit-sqlite-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi

55
.github/workflows/pr-feedback.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-FileCopyrightText: 2023 Marcel Klehr <mklehr@gmx.net>
# SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com>
# SPDX-FileCopyrightText: 2023 Daniel Kesselberg <mail@danielkesselberg.de>
# SPDX-FileCopyrightText: 2023 Florian Steffens <florian.steffens@nextcloud.com>
# SPDX-License-Identifier: MIT
name: 'Ask for feedback on PRs'
on:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
pull-requests: write
jobs:
pr-feedback:
if: ${{ github.repository_owner == 'nextcloud' }}
runs-on: ubuntu-latest
steps:
- name: The get-github-handles-from-website action
uses: marcelklehr/get-github-handles-from-website-action@06b2239db0a48fe1484ba0bfd966a3ab81a08308 # v1.0.1
id: scrape
with:
website: 'https://nextcloud.com/team/'
- name: Get blocklist
id: blocklist
run: |
blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -)
echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT"
- uses: nextcloud/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 # main
with:
feedback-message: |
Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.
We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.
Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6
Thank you for contributing to Nextcloud and we hope to hear from you soon!
(If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).)
days-before-feedback: 14
start-date: '2024-04-30'
exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}'
exempt-bots: true

59
.github/workflows/psalm.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Static analysis
on: pull_request
concurrency:
group: psalm-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
static-analysis:
runs-on: ubuntu-latest
name: static-psalm-analysis
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get php version
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
- name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml
run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml
- name: Set up php${{ steps.versions.outputs.php-min }}
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: ${{ steps.versions.outputs.php-min }}
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
# Temporary workaround for missing pcntl_* in PHP 8.3
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
composer remove nextcloud/ocp --dev
composer i
- name: Install nextcloud/ocp
run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies
- name: Run coding standards check
run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github

27
.github/workflows/reuse.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: CC0-1.0
name: REUSE Compliance Check
on: [pull_request]
permissions:
contents: read
jobs:
reuse-compliance-check:
runs-on: ubuntu-latest-low
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: REUSE Compliance Check
uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0

View File

@@ -0,0 +1,58 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Auto approve nextcloud/ocp
on:
pull_request_target: # zizmor: ignore[dangerous-triggers]
branches:
- main
- master
- stable*
permissions:
contents: read
concurrency:
group: update-nextcloud-ocp-approve-merge-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
auto-approve-merge:
if: github.actor == 'nextcloud-command'
runs-on: ubuntu-latest-low
permissions:
# for hmarr/auto-approve-action to approve PRs
pull-requests: write
# for alexwilson/enable-github-automerge-action to approve PRs
contents: write
steps:
- name: Disabled on forks
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
run: |
echo 'Can not approve PRs from forks'
exit 1
- uses: mdecoleman/pr-branch-name@55795d86b4566d300d237883103f052125cc7508 # v3.0.0
id: branchname
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# GitHub actions bot approve
- uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2
if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Enable GitHub auto merge
- name: Auto merge
uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # v2.0.0
if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,123 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Update nextcloud/ocp
on:
workflow_dispatch:
schedule:
- cron: "5 2 * * 0"
permissions:
contents: read
jobs:
update-nextcloud-ocp:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branches: ['main', 'master', 'stable31', 'stable30']
name: update-nextcloud-ocp-${{ matrix.branches }}
steps:
- id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
ref: ${{ matrix.branches }}
submodules: true
continue-on-error: true
- name: Set up php8.2
if: steps.checkout.outcome == 'success'
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: 8.2
# 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, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Read codeowners
if: steps.checkout.outcome == 'success'
id: codeowners
run: |
grep '/appinfo/info.xml' .github/CODEOWNERS | cut -f 2- -d ' ' | xargs | awk '{ print "codeowners="$0 }' >> $GITHUB_OUTPUT
continue-on-error: true
- name: Composer install
if: steps.checkout.outcome == 'success'
run: composer install
- name: Composer update nextcloud/ocp
id: update_branch
if: ${{ steps.checkout.outcome == 'success' && matrix.branches != 'main' }}
run: composer require --dev 'nextcloud/ocp:dev-${{ matrix.branches }}'
- name: Raise on issue on failure
uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0
if: ${{ steps.checkout.outcome == 'success' && failure() && steps.update_branch.conclusion == 'failure' }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: 'Failed to update nextcloud/ocp package on branch ${{ matrix.branches }}'
body: 'Please check the output of the GitHub action and manually resolve the issues<br>${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}<br>${{ steps.codeowners.outputs.codeowners }}'
- name: Composer update nextcloud/ocp
id: update_main
if: ${{ steps.checkout.outcome == 'success' && matrix.branches == 'main' }}
run: composer require --dev nextcloud/ocp:dev-master
- name: Raise on issue on failure
uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0
if: ${{ steps.checkout.outcome == 'success' && failure() && steps.update_main.conclusion == 'failure' }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: 'Failed to update nextcloud/ocp package on branch ${{ matrix.branches }}'
body: 'Please check the output of the GitHub action and manually resolve the issues<br>${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}<br>${{ steps.codeowners.outputs.codeowners }}'
- name: Reset checkout 3rdparty
if: steps.checkout.outcome == 'success'
run: |
git clean -f 3rdparty
git checkout 3rdparty
continue-on-error: true
- name: Reset checkout vendor
if: steps.checkout.outcome == 'success'
run: |
git clean -f vendor
git checkout vendor
continue-on-error: true
- name: Reset checkout vendor-bin
if: steps.checkout.outcome == 'success'
run: |
git clean -f vendor-bin
git checkout vendor-bin
continue-on-error: true
- name: Create Pull Request
if: steps.checkout.outcome == 'success'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
commit-message: 'chore(dev-deps): Bump nextcloud/ocp package'
committer: GitHub <noreply@github.com>
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
signoff: true
branch: 'automated/noid/${{ matrix.branches }}-update-nextcloud-ocp'
title: '[${{ matrix.branches }}] Update nextcloud/ocp dependency'
body: |
Auto-generated update of [nextcloud/ocp](https://github.com/nextcloud-deps/ocp/) dependency
labels: |
dependencies
3. to review

View File

@@ -6,7 +6,6 @@
- Adrian Missy <adrian.missy@onewavestudios.com>
- Alexandru Puiu <alexpuiu20@yahoo.com>
- Arne Bartelt <arne.bartelt@gmail.com>
- Chandi Langecker <git@chandi.it>
- Christoph Wurst <christoph@winzerhof-wurst.at>
- Gary Kim <gary@garykim.dev>

View File

@@ -5,6 +5,24 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.16.0
### Added
- feat: update default content @luka-nextcloud [#6740](https://github.com/nextcloud/deck/pull/6740)
- feat: add board import and export @luka-nextcloud [#6872](https://github.com/nextcloud/deck/pull/6872)
- feat: use outline icons @luka-nextcloud [#7114](https://github.com/nextcloud/deck/pull/7114)
- Add OCC commands for global calendar feature opt-in and opt-out in Deck @Fledermaus-20 [#7080](https://github.com/nextcloud/deck/pull/7080)
- [stable32] Migrate delete icon to Material Symbol outline variant @backportbot [#7211](https://github.com/nextcloud/deck/pull/7211)
- Chore(deps): Bump @nextcloud/vue from 8.27.0 to 8.31.0 @luka-nextcloud [#7271](https://github.com/nextcloud/deck/pull/7271)
### Fixed
- CSV export fixes @gidan80 [#6800](https://github.com/nextcloud/deck/pull/6800)
- [stable32] fix: Fix colors from due dates and done @backportbot [#7239](https://github.com/nextcloud/deck/pull/7239)
- [stable32] fix(darkmode): Fix activity icon colors @backportbot [#7241](https://github.com/nextcloud/deck/pull/7241)
- [stable32] fix: redirect to cleaner URL if RewriteBase is enabled @backportbot [#7268](https://github.com/nextcloud/deck/pull/7268)
- [stable32] fix: missing push notifications @backportbot [#7270](https://github.com/nextcloud/deck/pull/7270)
- [stable32] fix: use text cursor for card title on dashboard @backportbot [#7273](https://github.com/nextcloud/deck/pull/7273)
- [stable32] fix: parse arguments to CardService.reorder correctly to int @backportbot [#7276](https://github.com/nextcloud/deck/pull/7276)
## 1.16.0-beta.1
### Added

View File

@@ -24,7 +24,7 @@ Deck is a kanban style organization tool aimed at personal planning and project
### Mobile apps
- [Nextcloud Deck app for Android](https://github.com/stefan-niedermann/nextcloud-deck) - It is available in [F-Droid](https://f-droid.org/de/packages/it.niedermann.nextcloud.deck/) and the [Google Play Store](https://play.google.com/store/apps/details?id=it.niedermann.nextcloud.deck.play)
- Nextcloud Deck app for iOS - It is available in [Apple App store](https://apps.apple.com/de/app/next-deck/id6752478755)
- [Nextcloud Deck app for iOS](https://github.com/StCyr/deck-react-native) - It is available in [Apple App store](https://apps.apple.com/ml/app/nextcloud-deck/id1570892788)
### 3rd-Party Integrations

View File

@@ -20,7 +20,7 @@
- 🚀 Get your project organized
</description>
<version>2.0.0-dev.0</version>
<version>1.16.0</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
@@ -42,7 +42,7 @@
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="33" max-version="33"/>
<nextcloud min-version="32" max-version="32"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>

View File

@@ -9,35 +9,31 @@
}
],
"require": {
"justinrainbow/json-schema": "^6.0",
"bamarni/composer-bin-plugin": "^1.8"
"justinrainbow/json-schema": "^6.0"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"phpunit/phpunit": "^9",
"nextcloud/coding-standard": "^1.1",
"nextcloud/ocp": "dev-master"
"nextcloud/ocp": "dev-stable32",
"psalm/phar": "^5.13"
},
"config": {
"optimize-autoloader": true,
"allow-plugins": {
"composer/package-versions-deprecated": true,
"bamarni/composer-bin-plugin": true
"composer/package-versions-deprecated": true
},
"platform": {
"php": "8.1"
}
},
"scripts": {
"post-install-cmd": [
"@composer bin all install --ansi"
],
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm",
"psalm:update-baseline": "psalm --threads=$(nproc) --no-cache --update-baseline",
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType",
"psalm": "psalm.phar",
"psalm:update-baseline": "psalm.phar --update-baseline",
"psalm:fix": "psalm.phar --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType",
"test": [
"@test:unit",
"@test:integration"

109
composer.lock generated
View File

@@ -4,65 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "263f9ff9e6a13d50ab09bc9f4e06b749",
"content-hash": "a802f567c811977afdf6203368ae43e9",
"packages": [
{
"name": "bamarni/composer-bin-plugin",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/bamarni/composer-bin-plugin.git",
"reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
"reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.0",
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"composer/composer": "^2.0",
"ext-json": "*",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.5",
"symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
},
"type": "composer-plugin",
"extra": {
"class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin"
},
"autoload": {
"psr-4": {
"Bamarni\\Composer\\Bin\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "No conflicts for your bin dependencies",
"keywords": [
"composer",
"conflict",
"dependency",
"executable",
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2"
},
"time": "2022-10-31T08:38:03+00:00"
},
{
"name": "justinrainbow/json-schema",
"version": "6.4.2",
@@ -433,16 +376,16 @@
},
{
"name": "nextcloud/ocp",
"version": "dev-master",
"version": "dev-stable32",
"source": {
"type": "git",
"url": "https://github.com/nextcloud-deps/ocp.git",
"reference": "9a2e6c0bf6f2d87e1db8d18063a5bedf85040bb2"
"reference": "5f0db0fc0e5f09a4c8ac06ebc5dedefd21d53603"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/9a2e6c0bf6f2d87e1db8d18063a5bedf85040bb2",
"reference": "9a2e6c0bf6f2d87e1db8d18063a5bedf85040bb2",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/5f0db0fc0e5f09a4c8ac06ebc5dedefd21d53603",
"reference": "5f0db0fc0e5f09a4c8ac06ebc5dedefd21d53603",
"shasum": ""
},
"require": {
@@ -452,11 +395,10 @@
"psr/event-dispatcher": "^1.0",
"psr/log": "^3.0.2"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "33.0.0-dev"
"dev-stable32": "32.0.0-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -476,9 +418,9 @@
"description": "Composer package containing Nextcloud's public OCP API and the unstable NCU API",
"support": {
"issues": "https://github.com/nextcloud-deps/ocp/issues",
"source": "https://github.com/nextcloud-deps/ocp/tree/master"
"source": "https://github.com/nextcloud-deps/ocp/tree/stable32"
},
"time": "2025-09-27T00:45:05+00:00"
"time": "2025-09-10T00:46:52+00:00"
},
{
"name": "nikic/php-parser",
@@ -1130,6 +1072,41 @@
],
"time": "2024-12-05T13:48:26+00:00"
},
{
"name": "psalm/phar",
"version": "5.26.1",
"source": {
"type": "git",
"url": "https://github.com/psalm/phar.git",
"reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/psalm/phar/zipball/8a38e7ad04499a0ccd2c506fd1da6fc01fff4547",
"reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"vimeo/psalm": "*"
},
"bin": [
"psalm.phar"
],
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Composer-based Psalm Phar",
"support": {
"issues": "https://github.com/psalm/phar/issues",
"source": "https://github.com/psalm/phar/tree/5.26.1"
},
"time": "2024-09-09T16:22:43+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",

View File

@@ -14,7 +14,6 @@
input[type=submit].icon-confirm {
border-color: var(--color-border-maxcontrast) !important;
border-style: solid;
border-left: none;
}

View File

@@ -6,7 +6,7 @@ The REST API provides access for authenticated users to their data inside the De
# Prerequisites
- All requests require a `OCS-APIRequest` HTTP header to be set to `true` and a `Content-Type` of `application/json`. This does not apply to the endpoint for uploading attachments, which consumes `multipart/form-data`.
- All requests require a `OCS-APIRequest` HTTP header to be set to `true` and a `Content-Type` of `application/json`.
- The API is located at https://nextcloud.local/index.php/apps/deck/api/v1.0
- All request parameters are required, unless otherwise specified
@@ -733,7 +733,6 @@ The board list endpoint supports setting an `If-Modified-Since` header to limit
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------- |
| labelId | Integer | The label id to assign to the card |
#### Response
##### 200 Success
@@ -998,12 +997,10 @@ The request can fail with a bad request response for the following reasons:
#### Request data
The request is performed as `multipart/form-data`.
| Parameter | Type | Description |
| --------- | ------- | ----------------------------------------------------------------------------------------------- |
| type | String | The type of the attachement. Use `file` or `deck_file`. |
| file | Binary | File data to add as an attachment together with the `filename` parameter according to RFC 7578. |
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------------- |
| type | String | The type of the attachement |
| file | Binary | File data to add as an attachment |
- Prior to Deck version v1.3.0 (API v1.0), attachments were stored within deck. For this type of attachments `deck_file` was used as the default type of attachments
- Starting with Deck version 1.3.0 (API v1.1) files are stored within the users regular Nextcloud files and the type `file` has been introduced for that
@@ -1025,13 +1022,12 @@ The request is performed as `multipart/form-data`.
#### Request data
The request is performed as `multipart/form-data`.
| Parameter | Type | Description |
| --------- | ------- | ----------------------------------------------------------------------------------------------- |
| type | String | The type of the attachement. For now only `deck_file` is supported as an attachment type. |
| file | Binary | File data to add as an attachment together with the `filename` parameter according to RFC 7578. |
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------------- |
| type | String | The type of the attachement |
| file | Binary | File data to add as an attachment |
For now only `deck_file` is supported as an attachment type.
#### Response

View File

@@ -516,7 +516,7 @@ class ActivityManager {
];
}
private function findDetailsForCard(int $cardId, ?string $subject = null): array {
private function findDetailsForCard($cardId, $subject = null) {
$card = $this->cardMapper->find($cardId);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardMapper->find($stack->getBoardId());

View File

@@ -7,9 +7,6 @@
namespace OCA\Deck\Activity;
/**
* @psalm-api SettingComment
*/
class SettingComment extends SettingBase {
/**

View File

@@ -6,13 +6,9 @@
*/
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Service\AttachmentService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
@@ -25,52 +21,72 @@ class AttachmentApiController extends ApiController {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function getAll(string $apiVersion): DataResponse {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function getAll($apiVersion) {
$attachment = $this->attachmentService->findAll($this->request->getParam('cardId'), true);
if ($apiVersion === '1.0') {
$attachment = array_filter($attachment, fn (Attachment $attachment): bool => $attachment->getType() === 'deck_file');
$attachment = array_filter($attachment, function ($attachment) {
return $attachment->getType() === 'deck_file';
});
}
return new DataResponse($attachment, HTTP::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function display(int $cardId, int $attachmentId, string $type = 'deck_file') {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function display($cardId, $attachmentId, $type = 'deck_file') {
return $this->attachmentService->display($cardId, $attachmentId, $type);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function create(int $cardId, string $type, string $data): DataResponse {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function create($cardId, $type, $data) {
$attachment = $this->attachmentService->create($cardId, $type, $data);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function update(int $cardId, int $attachmentId, string $data, string $type = 'deck_file'): DataResponse {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function update($cardId, $attachmentId, $data, $type = 'deck_file') {
$attachment = $this->attachmentService->update($cardId, $attachmentId, $data, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): DataResponse {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function delete($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->delete($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): DataResponse {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*/
public function restore($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->restore($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}

View File

@@ -7,13 +7,8 @@
namespace OCA\Deck\Controller;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Service\AttachmentService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
class AttachmentController extends Controller {
@@ -25,66 +20,74 @@ class AttachmentController extends Controller {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
public function getAll(int $cardId): array {
/**
* @NoAdminRequired
*/
public function getAll($cardId) {
return $this->attachmentService->findAll($cardId, true);
}
/**
* @param $cardId
* @param $attachmentId
* @NoCSRFRequired
* @NoAdminRequired
* @return \OCP\AppFramework\Http\Response
* @throws \OCA\Deck\NotFoundException
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function display(int $cardId, string $attachmentId): Response {
['type' => $type, 'attachmentId' => $attachmentId] = $this->extractTypeAndAttachmentId($attachmentId);
return $this->attachmentService->display($cardId, $attachmentId, $type);
}
#[NoAdminRequired]
public function create(int $cardId): Attachment {
return $this->attachmentService->create(
$cardId,
$this->request->getParam('type'),
$this->request->getParam('data') ?? '',
);
}
#[NoAdminRequired]
public function update(int $cardId, string $attachmentId): Attachment {
['type' => $type, 'attachmentId' => $attachmentId] = $this->extractTypeAndAttachmentId($attachmentId);
return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data') ?? '', $type);
}
#[NoAdminRequired]
public function delete(int $cardId, string $attachmentId): Attachment {
['type' => $type, 'attachmentId' => $attachmentId] = $this->extractTypeAndAttachmentId($attachmentId);
return $this->attachmentService->delete($cardId, $attachmentId, $type);
}
#[NoAdminRequired]
public function restore(int $cardId, string $attachmentId): Attachment {
['type' => $type, 'attachmentId' => $attachmentId] = $this->extractTypeAndAttachmentId($attachmentId);
return $this->attachmentService->restore($cardId, $attachmentId, $type);
}
/**
* @return array{type: string, attachmentId: int}
* @throws BadRequestException
*/
private function extractTypeAndAttachmentId(string $attachmentId): array {
public function display($cardId, $attachmentId) {
if (!str_contains($attachmentId, ':')) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = [...explode(':', $attachmentId), '', ''];
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->display($cardId, $attachmentId, $type);
}
if ($type === '' || !is_numeric($attachmentId)) {
throw new BadRequestException('Invalid attachment id');
/**
* @NoAdminRequired
*/
public function create($cardId) {
return $this->attachmentService->create(
$cardId,
$this->request->getParam('type'),
$this->request->getParam('data')
);
}
/**
* @NoAdminRequired
*/
public function update($cardId, $attachmentId) {
if (!str_contains($attachmentId, ':')) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data'), $type);
}
return [
'type' => $type,
'attachmentId' => (int)$attachmentId,
];
/**
* @NoAdminRequired
*/
public function delete($cardId, $attachmentId) {
if (!str_contains($attachmentId, ':')) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->delete($cardId, $attachmentId, $type);
}
/**
* @NoAdminRequired
*/
public function restore($cardId, $attachmentId) {
if (!str_contains($attachmentId, ':')) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->restore($cardId, $attachmentId, $type);
}
}

View File

@@ -12,13 +12,10 @@ use OCA\Deck\Service\BoardService;
use OCA\Deck\StatusException;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use function Sabre\HTTP\parseDate;
use OCP\IRequest;
use Sabre\HTTP\Util;
/**
* Class BoardApiController
@@ -39,18 +36,21 @@ class BoardApiController extends ApiController {
}
/**
* Return all the boards that the current user has access to.
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Return all of the boards that the current user has access to.
*
* @param bool $details
* @throws StatusException
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function index(bool $details = false): DataResponse {
public function index(bool $details = false) {
$modified = $this->request->getHeader('If-Modified-Since');
if ($modified === '') {
if ($modified === null || $modified === '') {
$boards = $this->boardService->findAll(0, $details === true);
} else {
$date = parseDate($modified);
$date = Util::parseHTTPDate($modified);
if (!$date) {
throw new StatusException('Invalid If-Modified-Since header provided.');
}
@@ -64,12 +64,14 @@ class BoardApiController extends ApiController {
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*
* Return the board specified by $this->request->getParam('boardId').
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function get(): DataResponse {
public function get() {
$board = $this->boardService->find($this->request->getParam('boardId'));
$response = new DataResponse($board, HTTP::STATUS_OK);
$response->setETag($board->getEtag());
@@ -77,53 +79,68 @@ class BoardApiController extends ApiController {
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $color
*
* Create a board with the specified title and color.
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function create(string $title, string $color): DataResponse {
public function create($title, $color) {
$board = $this->boardService->create($title, $this->userId, $color);
return new DataResponse($board, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $color
* @params $archived
*
* Update a board with the specified boardId, title and color, and archived state.
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function update(string $title, string $color, bool $archived = false): DataResponse {
public function update($title, $color, $archived = false) {
$board = $this->boardService->update($this->request->getParam('boardId'), $title, $color, $archived);
return new DataResponse($board, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*
* Delete the board specified by $boardId. Return the board that was deleted.
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function delete(): DataResponse {
public function delete() {
$board = $this->boardService->delete($this->request->getParam('boardId'));
return new DataResponse($board, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*
* Undo the deletion of the board specified by $boardId.
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function undoDelete(): DataResponse {
public function undoDelete() {
$board = $this->boardService->deleteUndo($this->request->getParam('boardId'));
return new DataResponse($board, HTTP::STATUS_OK);
}
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function addAcl(int $boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage) {
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage) {
$acl = $this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage);
return new DataResponse($acl, HTTP::STATUS_OK);
}

View File

@@ -14,7 +14,6 @@ use OCA\Deck\Service\Importer\BoardImportService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IL10N;
use OCP\IRequest;
@@ -32,38 +31,68 @@ class BoardController extends ApiController {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
/**
* @NoAdminRequired
*/
public function index() {
return $this->boardService->findAll();
}
#[NoAdminRequired]
public function read(int $boardId): Board {
/**
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
public function read(int $boardId) {
return $this->boardService->find($boardId);
}
#[NoAdminRequired]
public function create(string $title, string $color): Board {
/**
* @NoAdminRequired
* @param $title
* @param $color
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $color) {
return $this->boardService->create($title, $this->userId, $color);
}
#[NoAdminRequired]
public function update(int $id, string $title, string $color, bool $archived): Board {
/**
* @NoAdminRequired
* @param $id
* @param $title
* @param $color
* @param $archived
* @return \OCP\AppFramework\Db\Entity
*/
public function update($id, $title, $color, $archived) {
return $this->boardService->update($id, $title, $color, $archived);
}
#[NoAdminRequired]
public function delete(int $boardId): Board {
/**
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
public function delete($boardId) {
return $this->boardService->delete($boardId);
}
#[NoAdminRequired]
public function deleteUndo(int $boardId): Board {
/**
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
public function deleteUndo($boardId) {
return $this->boardService->deleteUndo($boardId);
}
#[NoAdminRequired]
public function getUserPermissions(int $boardId): array {
/**
* @NoAdminRequired
* @param $boardId
* @return array|bool
* @internal param $userId
*/
public function getUserPermissions($boardId) {
$permissions = $this->permissionService->getPermissions($boardId);
return [
'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ],
@@ -74,10 +103,16 @@ class BoardController extends ApiController {
}
/**
* @NoAdminRequired
* @param $boardId
* @param $type
* @param $participant
* @param $permissionEdit
* @param $permissionShare
* @param $permissionManage
* @return \OCP\AppFramework\Db\Entity
*/
#[NoAdminRequired]
public function addAcl(int $boardId, int $type, $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage): Acl {
public function addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage) {
return $this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage);
}

View File

@@ -9,9 +9,6 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Service\Importer\BoardImportService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
@@ -26,9 +23,11 @@ class BoardImportApiController extends OCSController {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function import(string $system, array $config, array $data): DataResponse {
$this->boardImportService->setSystem($system);
$config = json_decode(json_encode($config));
@@ -39,17 +38,21 @@ class BoardImportApiController extends OCSController {
return new DataResponse($this->boardImportService->getBoard(), Http::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function getAllowedSystems(): DataResponse {
$allowedSystems = $this->boardImportService->getAllowedImportSystems();
return new DataResponse($allowedSystems, Http::STATUS_OK);
}
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function getConfigSchema(string $name): DataResponse {
$this->boardImportService->setSystem($name);
$this->boardImportService->validateSystem();

View File

@@ -12,9 +12,6 @@ use OCA\Deck\Service\AssignmentService;
use OCA\Deck\Service\CardService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
@@ -30,7 +27,7 @@ class CardApiController extends ApiController {
* @param IRequest $request
* @param CardService $cardService
* @param AssignmentService $assignmentService
* @param string $userId
* @param $userId
*/
public function __construct(
string $appName,
@@ -83,102 +80,112 @@ class CardApiController extends ApiController {
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
*
* Update a card
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function update(string $title, $type, string $owner, string $description = '', int $order = 0, $duedate = null, $archived = null): DataResponse {
public function update($title, $type, $owner, $description = '', $order = 0, $duedate = null, $archived = null) {
$done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null;
$card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Delete a specific card.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function delete(): DataResponse {
public function delete() {
$card = $this->cardService->delete($this->request->getParam('cardId'));
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Assign a label to a card.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function assignLabel(int $labelId): DataResponse {
public function assignLabel($labelId) {
$card = $this->cardService->assignLabel($this->request->getParam('cardId'), $labelId);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Assign a label to a card.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function removeLabel(int $labelId): DataResponse {
public function removeLabel($labelId) {
$card = $this->cardService->removeLabel($this->request->getParam('cardId'), $labelId);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Assign a user to a card
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function assignUser(int $cardId, string $userId, int $type = 0): DataResponse {
public function assignUser($cardId, $userId, $type = 0) {
$card = $this->assignmentService->assignUser($cardId, $userId, $type);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Unassign a user from a card
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function unassignUser(int $cardId, string $userId, int $type = 0): DataResponse {
public function unassignUser($cardId, $userId, $type = 0) {
$card = $this->assignmentService->unassignUser($cardId, $userId, $type);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Archive card
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function archive(int $cardId): DataResponse {
public function archive($cardId) {
$card = $this->cardService->archive($cardId);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Unarchive card
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function unarchive(int $cardId): DataResponse {
public function unarchive($cardId) {
$card = $this->cardService->unarchive($cardId);
return new DataResponse($card, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Reorder cards
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function reorder(int $stackId, int $order): DataResponse {
$card = $this->cardService->reorder((int)$this->request->getParam('cardId'), $stackId, $order);
public function reorder($stackId, $order) {
$card = $this->cardService->reorder((int)$this->request->getParam('cardId'), (int)$stackId, (int)$order);
return new DataResponse($card, HTTP::STATUS_OK);
}
}

View File

@@ -7,12 +7,9 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Card;
use OCA\Deck\Service\AssignmentService;
use OCA\Deck\Service\CardService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\IRequest;
class CardController extends Controller {
@@ -26,26 +23,45 @@ class CardController extends Controller {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
public function read(int $cardId): Card {
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function read($cardId) {
return $this->cardService->find($cardId);
}
/**
* @return Card[]
* @NoAdminRequired
* @param $cardId
* @param $stackId
* @param $order
* @return array
*/
#[NoAdminRequired]
public function reorder(int $cardId, int $stackId, int $order): array {
return $this->cardService->reorder($cardId, $stackId, $order);
public function reorder($cardId, $stackId, $order) {
return $this->cardService->reorder((int)$cardId, (int)$stackId, (int)$order);
}
#[NoAdminRequired]
public function rename(int $cardId, string $title): Card {
/**
* @NoAdminRequired
* @param $cardId
* @param $title
* @return \OCP\AppFramework\Db\Entity
*/
public function rename($cardId, $title) {
return $this->cardService->rename($cardId, $title);
}
#[NoAdminRequired]
public function create(string $title, int $stackId, string $type = 'plain', int $order = 999, string $description = '', $duedate = null, array $labels = [], array $users = []): Card {
/**
* @NoAdminRequired
* @param $title
* @param $stackId
* @param $type
* @param int $order
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $stackId, $type = 'plain', $order = 999, string $description = '', $duedate = null, $labels = [], $users = []) {
$card = $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description, $duedate);
foreach ($labels as $label) {
@@ -60,68 +76,113 @@ class CardController extends Controller {
}
/**
* @NoAdminRequired
* @param $id
* @param $title
* @param $stackId
* @param $type
* @param $order
* @param $description
* @param $duedate
* @param $deletedAt
* @return \OCP\AppFramework\Db\Entity
*/
#[NoAdminRequired]
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt): Card {
public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt) {
return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt);
}
#[NoAdminRequired]
public function clone(int $cardId, ?int $targetStackId = null): Card {
/**
* @NoAdminRequired
* @param $cardId
* @param $targetStackId
* @return \OCP\AppFramework\Db\Entity
*/
public function clone(int $cardId, ?int $targetStackId = null) {
return $this->cardService->cloneCard($cardId, $targetStackId);
}
#[NoAdminRequired]
public function delete(int $cardId): Card {
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function delete($cardId) {
return $this->cardService->delete($cardId);
}
/**
* @return Card[]
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
#[NoAdminRequired]
public function deleted(int $boardId): array {
public function deleted($boardId) {
return $this->cardService->fetchDeleted($boardId);
}
#[NoAdminRequired]
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function archive($cardId) {
return $this->cardService->archive($cardId);
}
#[NoAdminRequired]
public function unarchive(int $cardId): Card {
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function unarchive($cardId) {
return $this->cardService->unarchive($cardId);
}
#[NoAdminRequired]
public function done(int $cardId): Card {
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function done(int $cardId) {
return $this->cardService->done($cardId);
}
#[NoAdminRequired]
public function undone(int $cardId): Card {
/**
* @NoAdminRequired
* @param $cardId
* @return \OCP\AppFramework\Db\Entity
*/
public function undone(int $cardId) {
return $this->cardService->undone($cardId);
}
#[NoAdminRequired]
public function assignLabel(int $cardId, int $labelId): void {
/**
* @NoAdminRequired
* @param $cardId
* @param $labelId
*/
public function assignLabel($cardId, $labelId) {
$this->cardService->assignLabel($cardId, $labelId);
}
#[NoAdminRequired]
public function removeLabel(int $cardId, int $labelId): void {
/**
* @NoAdminRequired
* @param $cardId
* @param $labelId
*/
public function removeLabel($cardId, $labelId) {
$this->cardService->removeLabel($cardId, $labelId);
}
#[NoAdminRequired]
public function assignUser(int $cardId, string $userId, int $type = 0): Assignment {
/**
* @NoAdminRequired
*/
public function assignUser($cardId, $userId, $type = 0) {
return $this->assignmentService->assignUser($cardId, $userId, $type);
}
#[NoAdminRequired]
public function unassignUser(int $cardId, string $userId, int $type = 0): Assignment {
/**
* @NoAdminRequired
*/
public function unassignUser($cardId, $userId, $type = 0) {
return $this->assignmentService->unassignUser($cardId, $userId, $type);
}
}

View File

@@ -9,15 +9,11 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Service\CommentService;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
/**
* @psalm-api
*/
class CommentsApiController extends OCSController {
public function __construct(
string $appName,
@@ -31,33 +27,33 @@ class CommentsApiController extends OCSController {
}
/**
* @NoAdminRequired
* @throws StatusException
*/
#[NoAdminRequired]
public function list(int $cardId, int $limit = 20, int $offset = 0): DataResponse {
public function list(string $cardId, int $limit = 20, int $offset = 0): DataResponse {
return $this->commentService->list($cardId, $limit, $offset);
}
/**
* @NoAdminRequired
* @throws StatusException
*/
#[NoAdminRequired]
public function create(int $cardId, string $message, int $parentId = 0): DataResponse {
return $this->commentService->create($cardId, $message, $parentId);
}
/**
* @NoAdminRequired
* @throws StatusException
*/
#[NoAdminRequired]
public function update(int $cardId, int $commentId, string $message): DataResponse {
return $this->commentService->update($cardId, $commentId, $message);
}
/**
* @NoAdminRequired
* @throws StatusException
*/
#[NoAdminRequired]
public function delete(int $cardId, int $commentId): DataResponse {
return $this->commentService->delete($cardId, $commentId);
}

View File

@@ -8,8 +8,6 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Service\ConfigService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\OCSController;
@@ -24,15 +22,19 @@ class ConfigController extends OCSController {
parent::__construct($AppName, $request);
}
#[NoAdminRequired]
#[NoCSRFRequired]
/**
* @NoCSRFRequired
* @NoAdminRequired
*/
public function get(): DataResponse {
return new DataResponse($this->configService->getAll());
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function setValue(string $key, mixed $value): DataResponse|NotFoundResponse {
/**
* @NoCSRFRequired
* @NoAdminRequired
*/
public function setValue(string $key, $value) {
$result = $this->configService->set($key, $value);
if ($result === null) {
return new NotFoundResponse();

View File

@@ -10,9 +10,6 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Service\LabelService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
@@ -29,50 +26,59 @@ class LabelApiController extends ApiController {
$appName,
IRequest $request,
private LabelService $labelService,
private $userId,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Get a specific label.
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function get(): DataResponse {
public function get() {
$label = $this->labelService->find($this->request->getParam('labelId'));
return new DataResponse($label, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $color
* Create a new label
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function create(string $title, string $color): DataResponse {
public function create($title, $color) {
$label = $this->labelService->create($title, $color, $this->request->getParam('boardId'));
return new DataResponse($label, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $color
* Update a specific label
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function update(string $title, string $color): DataResponse {
public function update($title, $color) {
$label = $this->labelService->update($this->request->getParam('labelId'), $title, $color);
return new DataResponse($label, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Delete a specific label
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[CORS]
public function delete(): DataResponse {
public function delete() {
$label = $this->labelService->delete($this->request->getParam('labelId'));
return new DataResponse($label, HTTP::STATUS_OK);
}

View File

@@ -7,10 +7,8 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Label;
use OCA\Deck\Service\LabelService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\IRequest;
class LabelController extends Controller {
@@ -22,18 +20,34 @@ class LabelController extends Controller {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
public function create(string $title, string $color, int $boardId): Label {
/**
* @NoAdminRequired
* @param $title
* @param $color
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $color, $boardId) {
return $this->labelService->create($title, $color, $boardId);
}
#[NoAdminRequired]
public function update(int $id, string $title, string $color): Label {
/**
* @NoAdminRequired
* @param $id
* @param $title
* @param $color
* @return \OCP\AppFramework\Db\Entity
*/
public function update($id, $title, $color) {
return $this->labelService->update($id, $title, $color);
}
#[NoAdminRequired]
public function delete(int $labelId): Label {
/**
* @NoAdminRequired
* @param $labelId
* @return \OCP\AppFramework\Db\Entity
*/
public function delete($labelId) {
return $this->labelService->delete($labelId);
}
}

View File

@@ -10,7 +10,6 @@ declare(strict_types=1);
namespace OCA\Deck\Controller;
use OCA\Deck\Service\OverviewService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
@@ -25,7 +24,9 @@ class OverviewApiController extends OCSController {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
/**
* @NoAdminRequired
*/
public function upcomingCards(): DataResponse {
return new DataResponse($this->dashboardService->findUpcomingCards($this->userId));
}

View File

@@ -13,7 +13,6 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Db\Card;
use OCA\Deck\Model\CardDetails;
use OCA\Deck\Service\SearchService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
@@ -27,7 +26,9 @@ class SearchController extends OCSController {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
/**
* @NoAdminRequired
*/
public function search(string $term, ?int $limit = null, ?int $cursor = null): DataResponse {
$cards = $this->searchService->searchCards($term, $limit, $cursor);
return new DataResponse(array_map(function (Card $card) {

View File

@@ -7,16 +7,14 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\StackService;
use OCA\Deck\StatusException;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use function Sabre\HTTP\parseDate;
use Sabre\HTTP\Util;
/**
* Class StackApiController
@@ -31,21 +29,23 @@ class StackApiController extends ApiController {
$appName,
IRequest $request,
private StackService $stackService,
private BoardService $boardService,
) {
parent::__construct($appName, $request);
}
/**
* Return all the stacks in the specified board.
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Return all of the stacks in the specified board.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function index(): DataResponse {
public function index() {
$since = 0;
$modified = $this->request->getHeader('If-Modified-Since');
if ($modified !== '') {
$date = parseDate($modified);
if ($modified !== null && $modified !== '') {
$date = Util::parseHTTPDate($modified);
if (!$date) {
throw new StatusException('Invalid If-Modified-Since header provided.');
}
@@ -56,12 +56,13 @@ class StackApiController extends ApiController {
}
/**
* Return all the stacks in the specified board.
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Return all of the stacks in the specified board.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function get(): DataResponse {
public function get() {
$stack = $this->stackService->find($this->request->getParam('stackId'));
$response = new DataResponse($stack, HTTP::STATUS_OK);
$response->setETag($stack->getETag());
@@ -69,45 +70,55 @@ class StackApiController extends ApiController {
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $order
*
* Create a stack with the specified title and order.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function create(string $title, int $order): DataResponse {
public function create($title, $order) {
$stack = $this->stackService->create($title, $this->request->getParam('boardId'), $order);
return new DataResponse($stack, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @params $title
* @params $order
*
* Update a stack by the specified stackId and boardId with the values that were put.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function update(string $title, int $order) {
public function update($title, $order) {
$stack = $this->stackService->update($this->request->getParam('stackId'), $title, $this->request->getParam('boardId'), $order, 0);
return new DataResponse($stack, HTTP::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* Delete the stack specified by $this->request->getParam('stackId').
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function delete(): DataResponse {
public function delete() {
$stack = $this->stackService->delete($this->request->getParam('stackId'));
return new DataResponse($stack, HTTP::STATUS_OK);
}
/**
* Get the stacks that have been archived.
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* get the stacks that have been archived.
*/
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function getArchived(): DataResponse {
public function getArchived() {
$stacks = $this->stackService->findAllArchived($this->request->getParam('boardId'));
return new DataResponse($stacks, HTTP::STATUS_OK);
}

View File

@@ -7,12 +7,10 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Stack;
use OCA\Deck\Service\StackService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\IRequest;
class StackController extends Controller {
@@ -20,54 +18,78 @@ class StackController extends Controller {
string $appName,
IRequest $request,
private StackService $stackService,
private $userId,
) {
parent::__construct($appName, $request);
}
/**
* @return Stack[]
* @NoAdminRequired
* @param $boardId
* @return array
*/
#[NoAdminRequired]
public function index(int $boardId): array {
public function index($boardId) {
return $this->stackService->findAll($boardId);
}
/**
* @return Stack[]
* @NoAdminRequired
* @param $boardId
* @return array
*/
#[NoAdminRequired]
public function archived(int $boardId): array {
public function archived($boardId) {
return $this->stackService->findAllArchived($boardId);
}
#[NoAdminRequired]
public function create(string $title, int $boardId, int $order = 999): Stack {
/**
* @NoAdminRequired
* @param $title
* @param $boardId
* @param int $order
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $boardId, $order = 999) {
return $this->stackService->create($title, $boardId, $order);
}
#[NoAdminRequired]
public function update(int $id, string $title, int $boardId, int $order, ?int $deletedAt = null): Stack {
/**
* @NoAdminRequired
* @param $id
* @param $title
* @param $boardId
* @param $order
* @param $deletedAt
* @return \OCP\AppFramework\Db\Entity
*/
public function update($id, $title, $boardId, $order, $deletedAt) {
return $this->stackService->update($id, $title, $boardId, $order, $deletedAt);
}
/**
* @return array<int, Stack>
* @NoAdminRequired
* @param $stackId
* @param $order
* @return array
*/
#[NoAdminRequired]
public function reorder(int $stackId, int $order): array {
return $this->stackService->reorder($stackId, $order);
public function reorder($stackId, $order) {
return $this->stackService->reorder((int)$stackId, (int)$order);
}
#[NoAdminRequired]
public function delete(int $stackId): Stack {
/**
* @NoAdminRequired
* @param $stackId
* @return \OCP\AppFramework\Db\Entity
*/
public function delete($stackId) {
return $this->stackService->delete($stackId);
}
/**
* @return Stack[]
* @NoAdminRequired
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
*/
#[NoAdminRequired]
public function deleted(int $boardId): array {
public function deleted($boardId) {
return $this->stackService->fetchDeleted($boardId);
}
}

View File

@@ -7,20 +7,6 @@
namespace OCA\Deck\Db;
/**
* @method int getBoardId()
* @method bool isPermissionEdit()
* @method void setPermissionEdit(bool $permissionEdit)
* @method bool isPermissionShare()
* @method void setPermissionShare(bool $permissionShare)
* @method bool isPermissionManage()
* @method void setPermissionManage(bool $permissionManage)
* @method int getType()
* @method void setType(int $type)
* @method bool isOwner()
* @method void setOwner(int $owner)
*
*/
class Acl extends RelationalEntity {
public const PERMISSION_READ = 0;
public const PERMISSION_EDIT = 1;
@@ -51,13 +37,17 @@ class Acl extends RelationalEntity {
$this->addResolvable('participant');
}
public function getPermission(int $permission): bool {
return match ($permission) {
self::PERMISSION_READ => true,
self::PERMISSION_EDIT => $this->getPermissionEdit(),
self::PERMISSION_SHARE => $this->getPermissionShare(),
self::PERMISSION_MANAGE => $this->getPermissionManage(),
default => false,
};
public function getPermission($permission) {
switch ($permission) {
case self::PERMISSION_READ:
return true;
case self::PERMISSION_EDIT:
return $this->getPermissionEdit();
case self::PERMISSION_SHARE:
return $this->getPermissionShare();
case self::PERMISSION_MANAGE:
return $this->getPermissionManage();
}
return false;
}
}

View File

@@ -19,10 +19,13 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Acl[]
* @throws \OCP\DB\Exception
*/
public function findAll(int $boardId, ?int $limit = null, ?int $offset = null) {
public function findAll($boardId, $limit = null, $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
->from('deck_board_acl')
@@ -48,9 +51,12 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $userId
* @param numeric $id
* @return bool
* @throws \OCP\DB\Exception
*/
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
$aclId = $id;
$qb = $this->db->getQueryBuilder();
$qb->select('acl.id')
@@ -62,7 +68,11 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
return count($qb->executeQuery()->fetchAll()) > 0;
}
public function findBoardId(int $id): ?int {
/**
* @param numeric $id
* @return int|null
*/
public function findBoardId($id): ?int {
try {
$entity = $this->find($id);
return $entity->getBoardId();
@@ -77,7 +87,7 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
* @return Acl[]
* @throws \OCP\DB\Exception
*/
public function findByParticipant(int $type, string $participant): array {
public function findByParticipant($type, $participant): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')

View File

@@ -77,41 +77,26 @@ class AssignmentMapper extends DeckMapper implements IPermissionMapper {
}
public function deleteByParticipantOnBoard(string $participant, int $boardId, $type = Assignment::TYPE_USER) {
// Step 1: Get all card IDs for the board that have assignments for this participant
// This avoids MySQL Error 1093 by separating the SELECT from the DELETE operation
$qb = $this->db->getQueryBuilder();
$cardIdQuery = $this->db->getQueryBuilder();
$cardIdQuery->select('a.card_id')
->from('deck_assigned_users', 'a')
->innerJoin('a', 'deck_cards', 'c', 'c.id = a.card_id')
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
->where($cardIdQuery->expr()->eq('a.participant', $cardIdQuery->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($cardIdQuery->expr()->eq('s.board_id', $cardIdQuery->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($cardIdQuery->expr()->eq('a.type', $cardIdQuery->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
$result = $cardIdQuery->executeQuery();
$cardIds = [];
while ($row = $result->fetch()) {
$cardIds[] = $row['card_id'];
}
$result->closeCursor();
// Step 2: If we have card IDs, delete the assignments
if (!empty($cardIds)) {
$deleteQuery = $this->db->getQueryBuilder();
$deleteQuery->delete('deck_assigned_users')
->where($deleteQuery->expr()->eq('participant', $deleteQuery->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($deleteQuery->expr()->eq('type', $deleteQuery->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
->andWhere($deleteQuery->expr()->in('card_id', $deleteQuery->createNamedParameter($cardIds, IQueryBuilder::PARAM_INT_ARRAY)));
$deleteQuery->executeStatement();
}
->where($cardIdQuery->expr()->eq('a.participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($cardIdQuery->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($cardIdQuery->expr()->eq('a.type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
$qb->delete('deck_assigned_users')
->where($qb->expr()->in('card_id', $qb->createFunction($cardIdQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
$qb->executeStatement();
}
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
return $this->cardMapper->isOwner($userId, $id);
}
public function findBoardId(int $id): ?int {
public function findBoardId($id): ?int {
return $this->cardMapper->findBoardId($id);
}
@@ -123,9 +108,6 @@ class AssignmentMapper extends DeckMapper implements IPermissionMapper {
* @throws NotFoundException
*/
public function insert(Entity $entity): Entity {
if (!($entity instanceof Assignment)) {
throw new \LogicException('Trying to insert a ' . get_class($entity) . ' in the assignment mapper');
}
$origin = $this->getOrigin($entity);
if ($origin === null) {
throw new NotFoundException('No origin found for assignment');
@@ -144,7 +126,7 @@ class AssignmentMapper extends DeckMapper implements IPermissionMapper {
});
}
public function isUserAssigned(int $cardId, string $userId): bool {
public function isUserAssigned($cardId, $userId): bool {
$assignments = $this->findAll($cardId);
foreach ($assignments as $assignment) {
$origin = $this->getOrigin($assignment);

View File

@@ -36,11 +36,13 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param int $id
* @return Attachment
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
public function find(int $id): Attachment {
public function find($id) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -50,11 +52,14 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param int $cardId
* @param string $data
* @return Attachment
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
public function findByData(int $cardId, string $data): Attachment {
public function findByData($cardId, $data) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -65,10 +70,11 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param $cardId
* @return Entity[]
* @throws \OCP\DB\Exception
*/
public function findAll(int $cardId): array {
public function findAll($cardId) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -80,9 +86,11 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @return Attachment[]
* @param null $cardId
* @param bool $withOffset
* @return array
*/
public function findToDelete(?int $cardId = null, bool $withOffset = true): array {
public function findToDelete($cardId = null, $withOffset = true) {
// add buffer of 5 min
$timeLimit = time() - (60 * 5);
$qb = $this->db->getQueryBuilder();
@@ -104,8 +112,12 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
/**
* Check if $userId is owner of Entity with $id
*
* @param $userId string userId
* @param $id int|string unique entity identifier
* @return boolean
*/
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
try {
$attachment = $this->find($id);
return $this->cardMapper->isOwner($userId, $attachment->getCardId());
@@ -118,10 +130,10 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
/**
* Query boardId for Entity of given $id
*
* @param $id int unique entity identifier
* @param $id int|string unique entity identifier
* @return int|null id of Board
*/
public function findBoardId(int $id): ?int {
public function findBoardId($id): ?int {
try {
$attachment = $this->find($id);
} catch (\Exception $e) {

View File

@@ -10,20 +10,10 @@ namespace OCA\Deck\Db;
/**
* @method int getId()
* @method string getTitle()
* @method void setTitle(string $title)
* @method int getShared()
* @method void setShared(int $shared)
* @method bool isArchived()
* @method bool getArchived()
* @method void setArchived(bool $archived)
* @method int getDeletedAt()
* @method void setDeletedAt(int $deletedAt)
* @method int getLastModified()
* @method void setLastModified(int $lastModified)
* @method string getOwner()
* @method void setOwner(string $owner)
* @method string getColor()
* @method void setColor(string $color)
*/
class Board extends RelationalEntity {
protected $title;

View File

@@ -469,16 +469,16 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
return parent::delete($entity);
}
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
$board = $this->find($id);
return ($board->getOwner() === $userId);
}
public function findBoardId(int $id): ?int {
public function findBoardId($id): ?int {
return $id;
}
public function mapAcl(Acl &$acl): void {
public function mapAcl(Acl &$acl) {
$acl->resolveRelation('participant', function ($participant) use (&$acl) {
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
if ($this->userManager->userExists($acl->getParticipant())) {

View File

@@ -15,18 +15,13 @@ use Sabre\VObject\Component\VCalendar;
/**
* @method string getTitle()
* @method void setTitle(string $title)
* @method string getDescription()
* @method string getDescriptionPrev()
* @method int getStackId()
* @method void setStackId(int $stackId)
* @method int getOrder()
* @method void setOrder(int $order)
* @method int getLastModified()
* @method int getCreatedAt()
* @method bool getArchived()
* @method string getType()
* @method void setType(string $type)
* @method int getDeletedAt()
* @method void setDeletedAt(int $deletedAt)
* @method bool getNotified()
@@ -73,8 +68,8 @@ class Card extends RelationalEntity {
protected $createdAt;
protected $labels;
protected $assignedUsers;
protected array $attachments = [];
protected int $attachmentCount = 0;
protected $attachments;
protected $attachmentCount;
protected $owner;
protected $order;
protected $archived = false;

View File

@@ -86,15 +86,16 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$updatedFields = $entity->getUpdatedFields();
if (isset($updatedFields['duedate']) && $updatedFields['duedate']) {
try {
/** @var Card $existing */
$existing = $this->find($entity->getId());
if ($entity->getDueDate() !== $existing->getDueDate()) {
if ($existing && $entity->getDuedate() !== $existing->getDuedate()) {
$entity->setNotified(false);
}
// remove pending notifications
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setObject('card', (string)$entity->getId());
->setObject('card', $entity->getId());
$this->notificationManager->markProcessed($notification);
} catch (Exception $e) {
}
@@ -130,11 +131,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $card;
}
/**
* @return Card[]
* @throws \OCP\DB\Exception
*/
public function findAll($stackId, ?int $limit = null, int $offset = 0, int $since = -1) {
public function findAll($stackId, $limit = null, $offset = null, $since = -1) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
@@ -149,32 +146,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @param int[] $stackIds
* @return array<int, null|Card[]>
* @throws \OCP\DB\Exception
*/
public function findAllForStacks(array $stackIds, ?int $limit = null, int $offset = 0, int $since = -1): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
->where($qb->expr()->in('stack_id', $qb->createNamedParameter($stackIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
->setFirstResult($offset)
->orderBy('order')
->addOrderBy('id');
$rawCards = $this->findEntities($qb);
$cards = array_fill_keys($stackIds, null);
foreach ($rawCards as $card) {
$cards[$card->getStackId()][] = $card;
}
return $cards;
}
public function queryCardsByBoard(int $boardId): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
@@ -193,10 +164,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $qb;
}
/**
* @return Card[]
*/
public function findToDelete(int $timeLimit, ?int $limit = null): array {
public function findToDelete($timeLimit, $limit = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'owner', 'archived', 'deleted_at', 'last_modified')
->from('deck_cards')
@@ -207,10 +175,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @return Card[]
*/
public function findDeleted(int $boardId, ?int $limit = null, int $offset = 0): array {
public function findDeleted($boardId, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoard($boardId);
$qb->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
@@ -220,10 +185,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @return Card[]
*/
public function findCalendarEntries(int $boardId, ?int $limit = null, $offset = 0): array {
public function findCalendarEntries($boardId, $limit = null, $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
->from('deck_cards', 'c')
@@ -278,11 +240,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @param int[] $boardIds
* @return Card[]
*/
public function findAllWithDue(array $boardIds): array {
public function findAllWithDue(array $boardIds) {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
->from('deck_cards', 'c')
@@ -299,11 +257,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @param int[] $boardIds
* @return Card[]
*/
public function findToMeOrNotAssignedCards(array $boardIds, string $username): array {
public function findToMeOrNotAssignedCards(array $boardIds, string $username) {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
->from('deck_cards', 'c')
@@ -325,10 +279,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @return Card[]
*/
public function findOverdue(): array {
public function findOverdue() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'duedate', 'notified')
->from('deck_cards')
@@ -340,9 +291,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @return Card[]
*/
public function findUnexposedDescriptionChances() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'duedate', 'notified', 'description_prev', 'last_editor', 'description')
@@ -351,9 +299,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
/**
* @return Card[]
*/
public function search(array $boardIds, SearchQuery $query, ?int $limit = null, ?int $offset = null): array {
$qb = $this->queryCardsByBoards($boardIds);
$this->extendQueryByFilter($qb, $query);
@@ -388,7 +333,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$qb->andWhere($qb->expr()->lt('c.last_modified', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
$result = $qb->executeQuery();
$result = $qb->execute();
$entities = [];
while ($row = $result->fetch()) {
$entities[] = Card::fromRow($row);
@@ -431,7 +376,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$qb->andWhere($qb->expr()->lt('comments.id', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
$result = $qb->executeQuery();
$result = $qb->execute();
$entities = $result->fetchAll();
$result->closeCursor();
return $entities;
@@ -527,7 +472,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
});
$groups = $this->groupManager->search($assignment->getValue());
foreach ($searchUsers as $user) {
$groups = array_merge($groups, $this->groupManager->getUserGroups($user));
$groups = array_merge($groups, $this->groupManager->getUserIdGroups($user->getUID()));
}
$assignmentSearches = [];
@@ -580,7 +525,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$result = $qb->executeQuery();
$result = $qb->execute();
$all = $result->fetchAll();
$result->closeCursor();
return $all;
@@ -592,32 +537,32 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return parent::delete($entity);
}
public function deleteByStack($stackId): void {
public function deleteByStack($stackId) {
$cards = $this->findAllByStack($stackId);
foreach ($cards as $card) {
$this->delete($card);
}
}
public function assignLabel(int $card, int $label): void {
public function assignLabel($card, $label) {
$qb = $this->db->getQueryBuilder();
$qb->insert('deck_assigned_labels')
->values([
'label_id' => $qb->createNamedParameter($label, IQueryBuilder::PARAM_INT),
'card_id' => $qb->createNamedParameter($card, IQueryBuilder::PARAM_INT),
]);
$qb->executeStatement();
$qb->execute();
}
public function removeLabel(int $card, int $label): void {
public function removeLabel($card, $label) {
$qb = $this->db->getQueryBuilder();
$qb->delete('deck_assigned_labels')
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($card, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('label_id', $qb->createNamedParameter($label, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();
$qb->execute();
}
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('c.id')
->from($this->getTableName(), 'c')
@@ -629,7 +574,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return count($qb->executeQuery()->fetchAll()) > 0;
}
public function findBoardId(int $id): ?int {
public function findBoardId($id): ?int {
$result = $this->cache->get('findBoardId:' . $id);
if ($result === null) {
try {
@@ -659,11 +604,13 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
public function transferOwnership(string $ownerId, string $newOwnerId, ?int $boardId = null): void {
$qb = $this->db->getQueryBuilder();
$qb->update($this->getTableName())
->set('owner', $qb->createNamedParameter($newOwnerId, IQueryBuilder::PARAM_STR))
->where('owner', $qb->createNamedParameter($ownerId, IQueryBuilder::PARAM_STR))
->executeStatement();
$params = [
'owner' => $ownerId,
'newOwner' => $newOwnerId
];
$sql = "UPDATE `*PREFIX*{$this->tableName}` SET `owner` = :newOwner WHERE `owner` = :owner";
$stmt = $this->db->executeQuery($sql, $params);
$stmt->closeCursor();
}
public function remapCardOwner(int $boardId, string $userId, string $newUserId): void {

View File

@@ -19,11 +19,12 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
abstract class DeckMapper extends QBMapper {
/**
* @param $id
* @return T
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws \OCP\AppFramework\Db\DoesNotExistException
*/
public function find(int $id): Entity {
public function find($id) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -36,7 +37,7 @@ abstract class DeckMapper extends QBMapper {
* Helper function to split passed array into chunks of 1000 elements and
* call a given callback for fetching query results
*
* Can be useful to limit to 1000 results per query for oracle compatibility
* Can be useful to limit to 1000 results per query for oracle compatiblity
* but still iterate over all results
*/
public function chunkQuery(array $ids, callable $callback): Generator {

View File

@@ -8,25 +8,22 @@
namespace OCA\Deck\Db;
/**
*
*/
interface IPermissionMapper {
/**
* Check if $userId is owner of Entity with $id
*
* @param $userId string userId
* @param $id int unique entity identifier
* @param $id int|string unique entity identifier
* @return boolean
*/
public function isOwner(string $userId, int $id): bool;
public function isOwner($userId, $id): bool;
/**
* Query boardId for Entity of given $id
*
* @param $id int unique entity identifier
* @return ?int id of Board
* @param $id int|string unique entity identifier
* @return int|null id of Board
*/
public function findBoardId(int $id): ?int;
public function findBoardId($id): ?int;
}

View File

@@ -8,16 +8,7 @@
namespace OCA\Deck\Db;
/**
* @method string getTitle()
* @method void setTitle(string $title)
* @method string getColor()
* @method void setColor(string $color)
* @method int getBoardId()
* @method void setBoardId(int $boardId)
* @method int getCardId()
* @method void setCardId(int $cardId)
* @method int getLastModified()
* @method void setLastModified(int $lastModified)
* @method getTitle(): string
*/
class Label extends RelationalEntity {
protected $title;
@@ -33,7 +24,7 @@ class Label extends RelationalEntity {
$this->addType('lastModified', 'integer');
}
public function getETag(): string {
public function getETag() {
return md5((string)$this->getLastModified());
}
}

View File

@@ -20,10 +20,13 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Label[]
* @throws \OCP\DB\Exception
*/
public function findAll(int $boardId, ?int $limit = null, int $offset = 0): array {
public function findAll($boardId, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -41,10 +44,13 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $cardId
* @param int|null $limit
* @param int|null $offset
* @return Label[]
* @throws \OCP\DB\Exception
*/
public function findAssignedLabelsForCard(int $cardId, ?int $limit = null, int $offset = 0): array {
public function findAssignedLabelsForCard($cardId, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('l.*', 'card_id')
->from($this->getTableName(), 'l')
@@ -57,7 +63,7 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function findAssignedLabelsForCards(array $cardIds, ?int $limit = null, int $offset = 0): array {
public function findAssignedLabelsForCards($cardIds, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('l.*', 'card_id')
->from($this->getTableName(), 'l')
@@ -71,10 +77,13 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Label[]
* @throws \OCP\DB\Exception
*/
public function findAssignedLabelsForBoard(int $boardId, ?int $limit = null, int $offset = 0): array {
public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('l.id as id', 'l.title as title', 'l.color as color')
->selectAlias('c.id', 'card_id')
@@ -104,10 +113,11 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @return array<int, list<Label>>
* @param numeric $boardId
* @return array
* @throws \OCP\DB\Exception
*/
public function getAssignedLabelsForBoard(int $boardId): array {
public function getAssignedLabelsForBoard($boardId) {
$labels = $this->findAssignedLabelsForBoard($boardId);
$result = [];
foreach ($labels as $label) {
@@ -120,9 +130,11 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $labelId
* @return void
* @throws \OCP\DB\Exception
*/
public function deleteLabelAssignments(int $labelId): void {
public function deleteLabelAssignments($labelId) {
$qb = $this->db->getQueryBuilder();
$qb->delete('deck_assigned_labels')
->where($qb->expr()->eq('label_id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)));
@@ -130,9 +142,11 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $cardId
* @return void
* @throws \OCP\DB\Exception
*/
public function deleteLabelAssignmentsForCard(int $cardId): void {
public function deleteLabelAssignmentsForCard($cardId) {
$qb = $this->db->getQueryBuilder();
$qb->delete('deck_assigned_labels')
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
@@ -140,25 +154,33 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param string $userId
* @param numeric $labelId
* @return bool
* @throws \OCP\DB\Exception
*/
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $labelId): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('l.id')
->from($this->getTableName(), 'l')
->innerJoin('l', 'deck_boards', 'b', 'l.board_id = b.id')
->where($qb->expr()->eq('l.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->where($qb->expr()->eq('l.id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
return count($qb->executeQuery()->fetchAll()) > 0;
}
public function findBoardId(int $id): ?int {
/**
* @param numeric $id
* @return int|null
*/
public function findBoardId($id): ?int {
try {
$entity = $this->find($id);
return $entity->getBoardId();
} catch (DoesNotExistException|MultipleObjectsReturnedException) {
return null;
} catch (DoesNotExistException $e) {
} catch (MultipleObjectsReturnedException $e) {
}
return null;
}
}

View File

@@ -11,17 +11,10 @@ use Sabre\VObject\Component\VCalendar;
/**
* @method int getId()
* @method string getTitle()
* @method void setTitle(string $title)
* @method int getBoardId()
* @method void setBoardId(int $boardId)
* @method int getDeletedAt()
* @method void setDeletedAt(int $deletedAt)
* @method int getLastModified()
* @method void setLastModified(int $lastModified)
* @method \int getOrder()
* @method void setOrder(int $order)
* @method Card[] getCards()
* @method int getOrder()
*/
class Stack extends RelationalEntity {
protected $title;

View File

@@ -35,11 +35,13 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
/**
* @param numeric $id
* @return Stack
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
public function find(int $id): Stack {
public function find($id): Stack {
if (isset($this->stackCache[(string)$id])) {
return $this->stackCache[(string)$id];
}
@@ -54,9 +56,11 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param $cardId
* @return Stack|null
* @throws \OCP\DB\Exception
*/
public function findStackFromCardId(int $cardId): ?Stack {
public function findStackFromCardId($cardId): ?Stack {
$qb = $this->db->getQueryBuilder();
$qb->select('s.*')
->from($this->getTableName(), 's')
@@ -72,10 +76,13 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Stack[]
* @throws \OCP\DB\Exception
*/
public function findAll(int $boardId, ?int $limit = null, int $offset = 0): array {
public function findAll($boardId, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -88,9 +95,13 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Stack[]
* @throws \OCP\DB\Exception
*/
public function findDeleted(int $boardId, ?int $limit = null, int $offset = 0): array {
public function findDeleted($boardId, $limit = null, $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
@@ -116,9 +127,12 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $userId
* @param numeric $stackId
* @return bool
* @throws \OCP\DB\Exception
*/
public function isOwner(string $userId, int $id): bool {
public function isOwner($userId, $id): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('s.id')
->from($this->getTableName(), 's')
@@ -130,9 +144,11 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
}
/**
* @param numeric $id
* @return int|null
* @throws \OCP\DB\Exception
*/
public function findBoardId(int $id): ?int {
public function findBoardId($id): ?int {
$result = $this->cache->get('findBoardId:' . $id);
if ($result !== null) {
return $result !== false ? $result : null;
@@ -149,10 +165,6 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
return $result !== false ? $result : null;
}
/**
* @return array<Stack>
* @throws \OCP\DB\Exception
*/
public function findToDelete(): array {
// add buffer of 5 min
$timeLimit = time() - (60 * 5);

View File

@@ -35,10 +35,6 @@ class BeforeTemplateRenderedListener implements IEventListener {
Util::addStyle('deck', 'deck');
$pathInfo = $this->request->getPathInfo();
if (!$pathInfo) {
return;
}
if (str_starts_with($pathInfo, '/apps/calendar')) {
Util::addScript('deck', 'deck-calendar');
}

View File

@@ -18,10 +18,14 @@ use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<Event|AclDeletedEvent|AclCreatedEvent> */
class ResourceListener implements IEventListener {
public function __construct(
private readonly IManager $resourceManager,
private readonly ResourceProviderCard $resourceProviderCard,
) {
/** @var IManager */
private $resourceManager;
/** @var ResourceProviderCard */
private $resourceProviderCard;
public function __construct(IManager $resourceManager, ResourceProviderCard $resourceProviderCard) {
$this->resourceManager = $resourceManager;
$this->resourceProviderCard = $resourceProviderCard;
}
public function handle(Event $event): void {
@@ -34,10 +38,10 @@ class ResourceListener implements IEventListener {
$this->resourceManager->invalidateAccessCacheForProvider($this->resourceProviderCard);
try {
$resource = $this->resourceManager->getResourceForUser(ResourceProvider::RESOURCE_TYPE, (string)$boardId, null);
$resource = $this->resourceManager->getResourceForUser(ResourceProvider::RESOURCE_TYPE, $boardId, null);
$this->resourceManager->invalidateAccessCacheForResource($resource);
} catch (ResourceException $e) {
// If there is no resource we don't need to invalidate anything, but this should not happen anyway
// If there is no resource we don't need to invalidate anything, but this should not happen anyways
}
}
}

View File

@@ -11,7 +11,6 @@ use OCA\Deck\Db\Board;
class BoardSummary extends Board {
private Board $board;
/** @psalm-suppress ConstructorSignatureMismatch */
public function __construct(Board $board) {
parent::__construct();
$this->board = $board;
@@ -28,7 +27,7 @@ class BoardSummary extends Board {
return $this->board->getter($name);
}
public function __call($methodName, $args) {
return $this->board->__call($methodName, $args);
public function __call($name, $arguments) {
return $this->board->__call($name, $arguments);
}
}

View File

@@ -15,7 +15,6 @@ class CardDetails extends Card {
private ?Board $board;
private ?Reference $referenceData = null;
/** @psalm-suppress ConstructorSignatureMismatch */
public function __construct(Card $card, ?Board $board = null) {
parent::__construct();
$this->card = $card;

View File

@@ -95,7 +95,7 @@ class NotificationHelper {
$shouldNotify = $notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL;
if ($user->getUID() === $board->getOwner() && count($board->getAcl() ?? []) === 0) {
if ($user->getUID() === $board->getOwner() && count($board->getAcl()) === 0) {
// Notify if all or assigned is configured for unshared boards
$shouldNotify = true;
} elseif ($notificationSetting === ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED && $this->assignmentMapper->isUserAssigned($card->getId(), $user->getUID())) {

View File

@@ -64,7 +64,6 @@ class BoardReferenceProvider implements IReferenceProvider {
$reference = new Reference($referenceText);
$reference->setTitle($this->l10n->t('Deck board') . ': ' . $board['title']);
$ownerDisplayName = $board['owner']['displayname'] ?? $board['owner']['uid'] ?? '???';
// TRANSLATORS Owned by {boardOwnerName}
$reference->setDescription($this->l10n->t('Owned by %1$s', [$ownerDisplayName]));
$imageUrl = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath(Application::APP_ID, 'deck-dark.svg')

View File

@@ -90,7 +90,6 @@ class CommentReferenceProvider implements IReferenceProvider {
$reference->setTitle($comment['message']);
$boardOwnerDisplayName = $board['owner']['displayname'] ?? $board['owner']['uid'] ?? '???';
$reference->setDescription(
// TRANSLATORS From {userName}, in {boardTitle}/{stackTitle}, owned by {boardOwnerName}
$this->l10n->t('From %1$s, in %2$s/%3$s, owned by %4$s', [
$comment['actorDisplayName'],
$board['title'],

View File

@@ -21,6 +21,7 @@ use OCA\Deck\NotFoundException;
use OCA\Deck\Notification\NotificationHelper;
use OCA\Deck\Validators\AssignmentServiceValidator;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\EventDispatcher\IEventDispatcher;
@@ -91,12 +92,15 @@ class AssignmentService {
}
/**
* @param $cardId
* @param $userId
* @return bool|null|Entity
* @throws BadRequestException
* @throws NoPermissionException
* @throws MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function assignUser(int $cardId, string $userId, int $type = Assignment::TYPE_USER): Assignment {
public function assignUser($cardId, $userId, int $type = Assignment::TYPE_USER) {
$this->assignmentServiceValidator->check(compact('cardId', 'userId'));
if ($type !== Assignment::TYPE_USER && $type !== Assignment::TYPE_GROUP) {
@@ -140,13 +144,16 @@ class AssignmentService {
}
/**
* @param $cardId
* @param $userId
* @return Entity
* @throws BadRequestException
* @throws NotFoundException
* @throws NoPermissionException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function unassignUser(int $cardId, string $userId, int $type = 0): Assignment {
public function unassignUser($cardId, $userId, $type = 0) {
$this->assignmentServiceValidator->check(compact('cardId', 'userId'));
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);

View File

@@ -25,7 +25,6 @@ use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http\Response;
use OCP\IL10N;
use OCP\IUserManager;
use Psr\Container\ContainerExceptionInterface;
class AttachmentService {
private $attachmentMapper;
@@ -81,16 +80,20 @@ class AttachmentService {
}
/**
* @throws ContainerExceptionInterface
* @param string $type
* @param string $class
* @throws \OCP\AppFramework\QueryException
*/
public function registerAttachmentService(string $type, string $class): void {
$this->services[$type] = $this->application->getContainer()->get($class);
public function registerAttachmentService($type, $class) {
$this->services[$type] = $this->application->getContainer()->query($class);
}
/**
* @param string $type
* @return IAttachmentService
* @throws InvalidAttachmentType
*/
public function getService(string $type): IAttachmentService {
public function getService($type) {
if (isset($this->services[$type])) {
return $this->services[$type];
}
@@ -98,11 +101,16 @@ class AttachmentService {
}
/**
* @return Attachment[]
* @param $cardId
* @return array
* @throws \OCA\Deck\NoPermissionException
* @throws BadRequestException
*/
public function findAll(int $cardId, bool $withDeleted = false): array {
public function findAll($cardId, $withDeleted = false) {
if (is_numeric($cardId) === false) {
throw new BadRequestException('card id must be a number');
}
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$attachments = $this->attachmentMapper->findAll($cardId);
@@ -114,7 +122,7 @@ class AttachmentService {
/** @var IAttachmentService $service */
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
$attachments = array_merge($attachments, $service->listAttachments($cardId));
$attachments = array_merge($attachments, $service->listAttachments((int)$cardId));
}
}
@@ -132,40 +140,49 @@ class AttachmentService {
}
/**
* @param $cardId
* @return int|mixed
* @throws BadRequestException
* @throws InvalidAttachmentType
* @throws \OCP\DB\Exception
*/
public function count(int $cardId): int {
$count = $this->attachmentCacheHelper->getAttachmentCount($cardId);
public function count($cardId) {
if (is_numeric($cardId) === false) {
throw new BadRequestException('card id must be a number');
}
$count = $this->attachmentCacheHelper->getAttachmentCount((int)$cardId);
if ($count === null) {
$count = count($this->attachmentMapper->findAll($cardId));
foreach (array_keys($this->services) as $attachmentType) {
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
$count += $service->getAttachmentCount($cardId);
$count += $service->getAttachmentCount((int)$cardId);
}
}
$this->attachmentCacheHelper->setAttachmentCount($cardId, $count);
$this->attachmentCacheHelper->setAttachmentCount((int)$cardId, $count);
}
return $count;
}
/**
* @param $cardId
* @param $type
* @param $data
* @return Attachment|\OCP\AppFramework\Db\Entity
* @throws NoPermissionException
* @throws StatusException
* @throws BadRequestException
*/
public function create(int $cardId, string $type, string $data) {
public function create($cardId, $type, $data) {
$this->attachmentServiceValidator->check(compact('cardId', 'type'));
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
$this->attachmentCacheHelper->clearAttachmentCount((int)$cardId);
$attachment = new Attachment();
$attachment->setCardId($cardId);
$attachment->setType($type);
@@ -201,10 +218,12 @@ class AttachmentService {
/**
* Display the attachment
*
* @param $attachmentId
* @return Response
* @throws NoPermissionException
* @throws NotFoundException
*/
public function display(int $cardId, int $attachmentId, string $type = 'deck_file'): Response {
public function display($cardId, $attachmentId, $type = 'deck_file') {
try {
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
@@ -238,10 +257,13 @@ class AttachmentService {
/**
* Update an attachment with custom data
*
* @param $attachmentId
* @param $data
* @return mixed
* @throws BadRequestException
* @throws NoPermissionException
*/
public function update(int $cardId, int $attachmentId, string $data, string $type = 'deck_file'): Attachment {
public function update($cardId, $attachmentId, $data, $type = 'deck_file') {
$this->attachmentServiceValidator->check(compact('cardId', 'type', 'data'));
try {
@@ -362,6 +384,8 @@ class AttachmentService {
}
/**
* @param Attachment $attachment
* @return Attachment
* @throws \ReflectionException
*/
private function addCreator(Attachment $attachment): Attachment {

View File

@@ -75,6 +75,8 @@ class BoardService {
/**
* Set a different user than the current one, e.g. when no user is available in occ
*
* @param string $userId
*/
public function setUserId(string $userId): void {
$this->userId = $userId;
@@ -115,18 +117,22 @@ class BoardService {
}
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
/** @var Board $board */
$board = $this->boardMapper->find($boardId, true, true, $allowDeleted);
[$board] = $this->enrichBoards([$board], $fullDetails);
return $board;
}
/**
* @param $mapper
* @param $id
* @return bool
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function isArchived($mapper, int $id): bool {
public function isArchived($mapper, $id) {
$this->boardServiceValidator->check(compact('id'));
try {
@@ -145,12 +151,15 @@ class BoardService {
}
/**
* @param $mapper
* @param $id
* @return bool
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function isDeleted($mapper, int $id): bool {
public function isDeleted($mapper, $id) {
$this->boardServiceValidator->check(compact('mapper', 'id'));
try {
@@ -170,9 +179,13 @@ class BoardService {
/**
* @param $title
* @param $userId
* @param $color
* @return \OCP\AppFramework\Db\Entity
* @throws BadRequestException
*/
public function create(string $title, string $userId, string $color): Board {
public function create($title, $userId, $color) {
$this->boardServiceValidator->check(compact('title', 'userId', 'color'));
if (!$this->permissionService->canCreate()) {
@@ -183,8 +196,7 @@ class BoardService {
$board->setTitle($title);
$board->setOwner($userId);
$board->setColor($color);
/** @var Board $board */
$board = $this->boardMapper->insert($board);
$new_board = $this->boardMapper->insert($board);
// create new labels
$default_labels = [
@@ -198,31 +210,33 @@ class BoardService {
$label = new Label();
$label->setColor($labelColor);
$label->setTitle($labelTitle);
$label->setBoardId($board->getId());
$label->setBoardId($new_board->getId());
$labels[] = $this->labelMapper->insert($label);
}
$board->setLabels($labels);
$this->boardMapper->mapOwner($board);
$permissions = $this->permissionService->matchPermissions($board);
$board->setPermissions([
$new_board->setLabels($labels);
$this->boardMapper->mapOwner($new_board);
$permissions = $this->permissionService->matchPermissions($new_board);
$new_board->setPermissions([
'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false,
'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false,
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $board, ActivityManager::SUBJECT_BOARD_CREATE, [], $userId);
$this->changeHelper->boardChanged($board->getId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $new_board, ActivityManager::SUBJECT_BOARD_CREATE, [], $userId);
$this->changeHelper->boardChanged($new_board->getId());
return $board;
return $new_board;
}
/**
* @param $id
* @return Board
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete(int $id): Board {
public function delete($id) {
$this->boardServiceValidator->check(compact('id'));
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
@@ -239,11 +253,13 @@ class BoardService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function deleteUndo(int $id): Board {
public function deleteUndo($id) {
$this->boardServiceValidator->check(compact('id'));
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
@@ -257,12 +273,14 @@ class BoardService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function deleteForce(int $id): Board {
public function deleteForce($id) {
$this->boardServiceValidator->check(compact('id'));
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
@@ -273,12 +291,17 @@ class BoardService {
}
/**
* @param $id
* @param $title
* @param $color
* @param $archived
* @return \OCP\AppFramework\Db\Entity
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update(int $id, string $title, string $color, bool $archived): Board {
public function update($id, $title, $color, $archived) {
$this->boardServiceValidator->check(compact('id', 'title', 'color', 'archived'));
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
@@ -297,7 +320,7 @@ class BoardService {
return $board;
}
private function applyPermissions(int $boardId, bool $edit, bool $share, bool $manage, ?Acl $oldAcl = null): array {
private function applyPermissions($boardId, $edit, $share, $manage, $oldAcl = null) {
try {
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_MANAGE);
} catch (NoPermissionException $e) {
@@ -309,7 +332,7 @@ class BoardService {
return [$edit, $share, $manage];
}
public function enrichWithBoardSettings(Board $board): void {
public function enrichWithBoardSettings(Board $board) {
$globalCalendarConfig = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
$settings = [
'notify-due' => $this->config->getUserValue($this->userId, Application::APP_ID, 'board:' . $board->getId() . ':notify-due', ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED),
@@ -318,7 +341,7 @@ class BoardService {
$board->setSettings($settings);
}
public function enrichWithActiveSessions(Board $board): void {
public function enrichWithActiveSessions(Board $board) {
$sessions = $this->sessionMapper->findAllActive($board->getId());
$board->setActiveSessions(array_values(
@@ -331,11 +354,17 @@ class BoardService {
}
/**
* @param Acl::PERMISSION_TYPE_* $type
* @param $boardId
* @param $type
* @param $participant
* @param $edit
* @param $share
* @param $manage
* @return \OCP\AppFramework\Db\Entity
* @throws BadRequestException
* @throws NoPermissionException
*/
public function addAcl(int $boardId, int $type, $participant, bool $edit, bool $share, bool $manage): Acl {
public function addAcl($boardId, $type, $participant, $edit, $share, $manage) {
$this->boardServiceValidator->check(compact('boardId', 'type', 'participant', 'edit', 'share', 'manage'));
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_SHARE);
@@ -371,12 +400,17 @@ class BoardService {
}
/**
* @param $id
* @param $edit
* @param $share
* @param $manage
* @return \OCP\AppFramework\Db\Entity
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function updateAcl(int $id, bool $edit, bool $share, bool $manage): Acl {
public function updateAcl($id, $edit, $share, $manage) {
$this->boardServiceValidator->check(compact('id', 'edit', 'share', 'manage'));
$this->permissionService->checkPermission($this->aclMapper, $id, Acl::PERMISSION_SHARE);
@@ -388,12 +422,12 @@ class BoardService {
$acl->setPermissionShare($share);
$acl->setPermissionManage($manage);
$this->boardMapper->mapAcl($acl);
$acl = $this->aclMapper->update($acl);
$board = $this->aclMapper->update($acl);
$this->changeHelper->boardChanged($acl->getBoardId());
$this->eventDispatcher->dispatchTyped(new AclUpdatedEvent($acl));
return $acl;
return $board;
}
/**
@@ -545,23 +579,27 @@ class BoardService {
}
/**
* @param $id
* @return Board
* @throws DoesNotExistException
* @throws NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function export(int $id): Board {
public function export($id) : Board {
if (is_numeric($id) === false) {
throw new BadRequestException('board id must be a number');
}
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ);
$board = $this->boardMapper->find($id);
$board = $this->boardMapper->find((int)$id);
$this->enrichWithCards($board);
$this->enrichWithLabels($board);
return $board;
}
/**
* @param Board[] $boards
* @return Board[]
*/
/** @param Board[] $boards */
private function enrichBoards(array $boards, bool $fullDetails = true): array {
$result = [];
foreach ($boards as $board) {
@@ -677,8 +715,8 @@ class BoardService {
}
}
private function enrichWithStacks(Board $board): void {
$stacks = $this->stackMapper->findAll($board->getId());
private function enrichWithStacks($board, $since = -1) {
$stacks = $this->stackMapper->findAll($board->getId(), null, null, $since);
if (\count($stacks) === 0) {
return;
@@ -687,8 +725,8 @@ class BoardService {
$board->setStacks($stacks);
}
private function enrichWithLabels(Board $board): void {
$labels = $this->labelMapper->findAll($board->getId());
private function enrichWithLabels($board, $since = -1) {
$labels = $this->labelMapper->findAll($board->getId(), null, null, $since);
if (\count($labels) === 0) {
return;
@@ -697,7 +735,7 @@ class BoardService {
$board->setLabels($labels);
}
private function enrichWithUsers(Board $board): void {
private function enrichWithUsers($board, $since = -1) {
$boardUsers = $this->permissionService->findUsers($board->getId());
if ($boardUsers === null || \count($boardUsers) === 0) {
return;
@@ -708,7 +746,7 @@ class BoardService {
/**
* Clean a given board data from the Cache
*/
private function clearBoardFromCache(Board $board): void {
private function clearBoardFromCache(Board $board) {
$boardId = $board->getId();
$boardOwnerId = $board->getOwner();
@@ -717,7 +755,7 @@ class BoardService {
unset($this->boardsCachePartial[$boardId]);
}
private function enrichWithCards(Board $board): void {
private function enrichWithCards($board) {
$stacks = $this->stackMapper->findAll($board->getId());
foreach ($stacks as $stack) {
$cards = $this->cardMapper->findAllByStack($stack->getId());

View File

@@ -64,14 +64,10 @@ class CardService {
) {
}
/**
* @param Card[] $cards
* @return CardDetails[]
*/
public function enrichCards(array $cards): array {
public function enrichCards($cards) {
$user = $this->userManager->get($this->userId);
$cardIds = array_map(function (Card $card) use ($user): int {
$cardIds = array_map(function (Card $card) use ($user) {
// Everything done in here might be heavy as it is executed for every card
$cardId = $card->getId();
$this->cardMapper->mapOwner($card);
@@ -124,8 +120,7 @@ class CardService {
);
}
/** @return Card[] */
public function fetchDeleted($boardId): array {
public function fetchDeleted($boardId) {
$this->cardServiceValidator->check(compact('boardId'));
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
$cards = $this->cardMapper->findDeleted($boardId);
@@ -134,12 +129,13 @@ class CardService {
}
/**
* @return \OCA\Deck\Db\RelationalEntity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function find(int $cardId): Card {
public function find(int $cardId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$card = $this->cardMapper->find($cardId);
[$card] = $this->enrichCards([$card]);
@@ -156,10 +152,7 @@ class CardService {
return $card;
}
/**
* @return Card[]
*/
public function findCalendarEntries(int $boardId): array {
public function findCalendarEntries($boardId) {
try {
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
} catch (NoPermissionException $e) {
@@ -170,13 +163,20 @@ class CardService {
}
/**
* @param $title
* @param $stackId
* @param $type
* @param integer $order
* @param $description
* @param $owner
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadrequestException
*/
public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null): Card {
public function create($title, $stackId, $type, $order, $owner, $description = '', $duedate = null) {
$this->cardServiceValidator->check(compact('title', 'stackId', 'type', 'order', 'owner'));
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT);
@@ -203,13 +203,19 @@ class CardService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete(int $id): Card {
public function delete($id) {
if (is_numeric($id) === false) {
throw new BadRequestException('card id must be a number');
}
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
if ($this->boardService->isArchived($this->cardMapper, $id)) {
throw new StatusException('Operation not allowed. This board is archived.');
@@ -227,13 +233,25 @@ class CardService {
}
/**
* @param $id
* @param $title
* @param $stackId
* @param $type
* @param $owner
* @param $description
* @param $order
* @param $duedate
* @param $deletedAt
* @param $archived
* @param $done
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update(int $id, string $title, int $stackId, string $type, string $owner, string $description = '', int $order = 0, ?string $duedate = null, ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null): Card {
public function update($id, $title, $stackId, $type, $owner, $description = '', $order = 0, $duedate = null, $deletedAt = null, $archived = null, ?OptionalNullableValue $done = null) {
$this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order'));
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT, allowDeletedCard: true);
@@ -380,13 +398,16 @@ class CardService {
}
/**
* @param $id
* @param $title
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function rename(int $id, string $title): Card {
public function rename($id, $title) {
$this->cardServiceValidator->check(compact('id', 'title'));
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
@@ -408,14 +429,17 @@ class CardService {
}
/**
* @return list<Card>
* @param $id
* @param $stackId
* @param $order
* @return array
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function reorder(int $id, int $stackId, int $order): array {
public function reorder(int $id, int $stackId, int $order) {
$this->cardServiceValidator->check(compact('id', 'stackId', 'order'));
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
@@ -464,13 +488,15 @@ class CardService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function archive(int $id): Card {
public function archive($id) {
$this->cardServiceValidator->check(compact('id'));
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT);
@@ -491,13 +517,15 @@ class CardService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function unarchive(int $id): Card {
public function unarchive($id) {
$this->cardServiceValidator->check(compact('id'));
@@ -518,6 +546,8 @@ class CardService {
}
/**
* @param $id
* @return \OCA\Deck\Db\Card
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
@@ -569,13 +599,15 @@ class CardService {
}
/**
* @param $cardId
* @param $labelId
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function assignLabel(int $cardId, int $labelId): Card {
public function assignLabel($cardId, $labelId) {
$this->cardServiceValidator->check(compact('cardId', 'labelId'));
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
@@ -597,16 +629,18 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $card;
}
/**
* @param $cardId
* @param $labelId
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function removeLabel(int $cardId, int $labelId): Card {
public function removeLabel($cardId, $labelId) {
$this->cardServiceValidator->check(compact('cardId', 'labelId'));
@@ -626,7 +660,6 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $card;
}
public function getCardUrl(int $cardId): string {

View File

@@ -66,7 +66,7 @@ class CirclesService {
$circlesManager->startSession($federatedUser);
$circle = $circlesManager->getCircle($circleId);
$member = $circle->getInitiator();
$isUserInCircle = $member->getLevel() >= Member::LEVEL_MEMBER;
$isUserInCircle = $member !== null && $member->getLevel() >= Member::LEVEL_MEMBER;
if (!isset($this->userCircleCache[$circleId])) {
$this->userCircleCache[$circleId] = [];

View File

@@ -22,6 +22,8 @@ use OCP\IUserManager;
use OutOfBoundsException;
use Psr\Log\LoggerInterface;
use function is_numeric;
class CommentService {
public function __construct(
@@ -34,9 +36,12 @@ class CommentService {
) {
}
public function list(int $cardId, int $limit = 20, int $offset = 0): DataResponse {
public function list(string $cardId, int $limit = 20, int $offset = 0): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$comments = $this->commentsManager->getForObject(Application::COMMENT_ENTITY_TYPE, (string)$cardId, $limit, $offset);
$comments = $this->commentsManager->getForObject(Application::COMMENT_ENTITY_TYPE, $cardId, $limit, $offset);
$result = [];
foreach ($comments as $comment) {
$formattedComment = $this->formatComment($comment);
@@ -91,13 +96,13 @@ class CommentService {
* @throws BadRequestException
* @throws NotFoundException|NoPermissionException
*/
public function create(int $cardId, string $message, int $replyTo = 0): DataResponse {
public function create(int $cardId, string $message, string $replyTo = '0'): DataResponse {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
// Check if parent is a comment on the same card
if ($replyTo !== 0) {
if ($replyTo !== '0') {
try {
$comment = $this->commentsManager->get((string)$replyTo);
$comment = $this->commentsManager->get($replyTo);
if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || (int)$comment->getObjectId() !== $cardId) {
throw new CommentNotFoundException();
}
@@ -110,7 +115,7 @@ class CommentService {
$comment = $this->commentsManager->create('users', $this->userId, Application::COMMENT_ENTITY_TYPE, (string)$cardId);
$comment->setMessage($message);
$comment->setVerb('comment');
$comment->setParentId((string)$replyTo);
$comment->setParentId($replyTo);
$this->commentsManager->save($comment);
return new DataResponse($this->formatComment($comment, true));
} catch (\InvalidArgumentException $e) {
@@ -123,8 +128,14 @@ class CommentService {
}
}
public function update(int $cardId, int $commentId, string $message): DataResponse {
$comment = $this->get($cardId, $commentId);
public function update(string $cardId, string $commentId, string $message): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
if (!is_numeric($commentId)) {
throw new BadRequestException('A valid comment id must be provided');
}
$comment = $this->get((int)$cardId, (int)$commentId);
if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) {
throw new NoPermissionException('Only authors are allowed to edit their comment.');
}
@@ -134,12 +145,18 @@ class CommentService {
return new DataResponse($this->formatComment($comment));
}
public function delete(int $cardId, int $commentId): DataResponse {
public function delete(string $cardId, string $commentId): DataResponse {
if (!is_numeric($cardId)) {
throw new BadRequestException('A valid card id must be provided');
}
if (!is_numeric($commentId)) {
throw new BadRequestException('A valid comment id must be provided');
}
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
try {
$comment = $this->commentsManager->get((string)$commentId);
if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || (int)$comment->getObjectId() !== $cardId) {
$comment = $this->commentsManager->get($commentId);
if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) {
throw new CommentNotFoundException();
}
} catch (CommentNotFoundException $e) {
@@ -148,11 +165,11 @@ class CommentService {
if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) {
throw new NoPermissionException('Only authors are allowed to edit their comment.');
}
$this->commentsManager->delete((string)$commentId);
$this->commentsManager->delete($commentId);
return new DataResponse([]);
}
private function formatComment(IComment $comment, bool $addReplyTo = false): array {
private function formatComment(IComment $comment, $addReplyTo = false): array {
$actorDisplayName = $this->userManager->getDisplayName($comment->getActorId()) ?? $comment->getActorId();
$formattedComment = [

View File

@@ -48,8 +48,7 @@ class ConfigService {
}
public function getAll(): array {
$userId = $this->getUserId();
if ($userId === null) {
if ($this->getUserId() === null) {
return [];
}
@@ -58,7 +57,7 @@ class ConfigService {
'cardDetailsInModal' => $this->isCardDetailsInModal(),
'cardIdBadge' => $this->isCardIdBadgeEnabled()
];
if ($this->groupManager->isAdmin($userId)) {
if ($this->groupManager->isAdmin($this->getUserId())) {
$data['groupLimit'] = $this->get('groupLimit');
}
return $data;
@@ -96,48 +95,44 @@ class ConfigService {
}
public function isCalendarEnabled(?int $boardId = null): bool {
$userId = $this->getUserId();
if ($userId === null) {
if ($this->getUserId() === null) {
return false;
}
$appConfigState = $this->config->getAppValue(Application::APP_ID, 'calendar', 'yes') === 'yes';
$defaultState = (bool)$this->config->getUserValue($userId, Application::APP_ID, 'calendar', $appConfigState);
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'calendar', $appConfigState);
if ($boardId === null) {
return $defaultState;
}
return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'board:' . $boardId . ':calendar', $defaultState);
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'board:' . $boardId . ':calendar', $defaultState);
}
public function isCardDetailsInModal(?int $boardId = null): bool {
$userId = $this->getUserId();
if ($userId === null) {
if ($this->getUserId() === null) {
return false;
}
$defaultState = (bool)$this->config->getUserValue($userId, Application::APP_ID, 'cardDetailsInModal', true);
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', true);
if ($boardId === null) {
return $defaultState;
}
return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'board:' . $boardId . ':cardDetailsInModal', $defaultState);
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'board:' . $boardId . ':cardDetailsInModal', $defaultState);
}
public function isCardIdBadgeEnabled(): bool {
$userId = $this->getUserId();
if ($userId === null) {
if ($this->getUserId() === null) {
return false;
}
$appConfigState = $this->config->getAppValue(Application::APP_ID, 'cardIdBadge', 'yes') === 'no';
$defaultState = (bool)$this->config->getUserValue($userId, Application::APP_ID, 'cardIdBadge', $appConfigState);
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', $appConfigState);
return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'cardIdBadge', $defaultState);
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', $defaultState);
}
public function set($key, $value) {
$userId = $this->getUserId();
if ($userId === null) {
if ($this->getUserId() === null) {
throw new NoPermissionException('Must be logged in to set user config');
}
@@ -145,21 +140,21 @@ class ConfigService {
[$scope] = explode(':', $key, 2);
switch ($scope) {
case 'groupLimit':
if (!$this->groupManager->isAdmin($userId)) {
if (!$this->groupManager->isAdmin($this->getUserId())) {
throw new NoPermissionException('You must be admin to set the group limit');
}
$result = $this->setGroupLimit($value);
break;
case 'calendar':
$this->config->setUserValue($userId, Application::APP_ID, 'calendar', (string)$value);
$this->config->setUserValue($this->getUserId(), Application::APP_ID, 'calendar', (string)$value);
$result = $value;
break;
case 'cardDetailsInModal':
$this->config->setUserValue($userId, Application::APP_ID, 'cardDetailsInModal', (string)$value);
$this->config->setUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', (string)$value);
$result = $value;
break;
case 'cardIdBadge':
$this->config->setUserValue($userId, Application::APP_ID, 'cardIdBadge', (string)$value);
$this->config->setUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', (string)$value);
$result = $value;
break;
case 'board':
@@ -167,7 +162,7 @@ class ConfigService {
if ($boardConfigKey === 'notify-due' && !in_array($value, [self::SETTING_BOARD_NOTIFICATION_DUE_ALL, self::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED, self::SETTING_BOARD_NOTIFICATION_DUE_OFF], true)) {
throw new BadRequestException('Board notification option must be one of: off, assigned, all');
}
$this->config->setUserValue($userId, Application::APP_ID, $key, (string)$value);
$this->config->setUserValue($this->getUserId(), Application::APP_ID, $key, (string)$value);
$result = $value;
}
return $result;

View File

@@ -124,15 +124,18 @@ class FullTextSearchService {
/**
* @param int $cardId
*
* @return Board
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function getBoardFromCardId(int $cardId): Board {
$boardId = $this->cardMapper->findBoardId($cardId);
if ($boardId === null) {
throw new DoesNotExistException("Board '$cardId' does not exist");
}
return $this->boardMapper->find($boardId);
$boardId = (int)$this->cardMapper->findBoardId($cardId);
/** @var Board $board */
$board = $this->boardMapper->find($boardId);
return $board;
}
@@ -142,7 +145,7 @@ class FullTextSearchService {
* @return Card[]
*/
private function getCardsFromStack(int $stackId): array {
return $this->cardMapper->findAll($stackId);
return $this->cardMapper->findAll($stackId, null, null);
}
@@ -152,7 +155,7 @@ class FullTextSearchService {
* @return Stack[]
*/
private function getStacksFromBoard(int $boardId): array {
return $this->stackMapper->findAll($boardId);
return $this->stackMapper->findAll($boardId, null, null);
}
@@ -162,6 +165,6 @@ class FullTextSearchService {
* @return Board[]
*/
private function getBoardsFromUser(string $userId): array {
return $this->boardMapper->findAllByUser($userId);
return $this->boardMapper->findAllByUser($userId, null, null, null);
}
}

View File

@@ -19,25 +19,29 @@ use OCP\Comments\IComment;
abstract class ABoardImportService {
/** @var string */
public static $name = '';
private BoardImportService $boardImportService;
protected bool $needValidateData = true;
/** @var BoardImportService */
private $boardImportService;
/** @var bool */
protected $needValidateData = true;
/** @var Stack[] */
protected array $stacks = [];
protected $stacks = [];
/** @var Label[] */
protected array $labels = [];
protected $labels = [];
/** @var Card[] */
protected array $cards = [];
protected $cards = [];
/** @var Acl[] */
protected array $acls = [];
protected $acls = [];
/** @var IComment[][] */
protected array $comments = [];
protected $comments = [];
/** @var Assignment[] */
protected array $assignments = [];
/** @var int[][] */
protected array $labelCardAssignments = [];
protected $assignments = [];
/** @var string[][] */
protected $labelCardAssignments = [];
/**
* Configure import service
*
* @return void
*/
abstract public function bootstrap(): void;
@@ -64,13 +68,10 @@ abstract class ABoardImportService {
abstract public function getCardAssignments(): array;
/**
* @return array<int, array<int, int>>
*/
abstract public function getCardLabelAssignment(): array;
/**
* @return array<int, array<string, IComment>>
* @return IComment[][]|array
*/
abstract public function getComments(): array;
@@ -97,16 +98,16 @@ abstract class ABoardImportService {
$this->acls[$code] = $acl;
}
public function updateComment(int $cardId, string $commentId, IComment $comment): void {
public function updateComment(string $cardId, string $commentId, IComment $comment): void {
$this->comments[$cardId][$commentId] = $comment;
}
public function updateCardAssignment(int $cardId, int $assignmentId, Entity $assignment): void {
public function updateCardAssignment(string $cardId, string $assignmentId, Entity $assignment): void {
$this->assignments[$cardId][$assignmentId] = $assignment;
}
public function updateCardLabelsAssignment(int $cardId, int $assignmentId, int $labelId): void {
$this->labelCardAssignments[$cardId][$assignmentId] = $labelId;
public function updateCardLabelsAssignment(string $cardId, string $assignmentId, string $assignment): void {
$this->labelCardAssignments[$cardId][$assignmentId] = $assignment;
}
public function setImportService(BoardImportService $service): void {

View File

@@ -209,15 +209,14 @@ class BoardImportService {
public function importBoard(): void {
$board = $this->getImportSystem()->getBoard();
if ($board === null) {
throw new \LogicException('Import board not found');
}
if (!$this->userManager->userExists($board->getOwner())) {
throw new \Exception('Target owner ' . $board->getOwner() . ' not found. Please provide a mapping through the import config.');
}
$this->boardMapper->insert($board);
$this->board = $board;
if ($board) {
$this->boardMapper->insert($board);
$this->board = $board;
}
}
public function getBoard(bool $reset = false): Board {
@@ -293,7 +292,12 @@ class BoardImportService {
}
}
public function assignCardToLabel(int $cardId, int $labelId): self {
/**
* @param mixed $cardId
* @param mixed $labelId
* @return self
*/
public function assignCardToLabel($cardId, $labelId): self {
$this->cardMapper->assignLabel(
$cardId,
$labelId
@@ -303,14 +307,14 @@ class BoardImportService {
public function assignCardsToLabels(): void {
$data = $this->getImportSystem()->getCardLabelAssignment();
foreach ($data as $cardId => $assignment) {
foreach ($assignment as $assignmentId => $labelId) {
foreach ($data as $cardId => $assignemnt) {
foreach ($assignemnt as $assignmentId => $labelId) {
try {
$this->assignCardToLabel(
(int)$cardId,
$cardId,
$labelId
);
$this->getImportSystem()->updateCardLabelsAssignment((int)$cardId, (int)$assignmentId, $labelId);
$this->getImportSystem()->updateCardLabelsAssignment($cardId, $assignmentId, $labelId);
} catch (\Exception $e) {
$this->addError('Failed to assign label ' . $labelId . ' to ' . $cardId, $e);
}
@@ -322,20 +326,20 @@ class BoardImportService {
$allComments = $this->getImportSystem()->getComments();
foreach ($allComments as $cardId => $comments) {
foreach ($comments as $commentId => $comment) {
$this->insertComment((int)$cardId, $comment);
$this->getImportSystem()->updateComment((int)$cardId, $commentId, $comment);
$this->insertComment($cardId, $comment);
$this->getImportSystem()->updateComment($cardId, $commentId, $comment);
}
}
}
private function insertComment(int $cardId, IComment $comment): void {
$comment->setObject('deckCard', (string)$cardId);
private function insertComment(string $cardId, IComment $comment): void {
$comment->setObject('deckCard', $cardId);
$comment->setVerb('comment');
// Check if parent is a comment on the same card
if ($comment->getParentId() !== '0') {
try {
$parent = $this->commentsManager->get($comment->getParentId());
if ($parent->getObjectType() !== Application::COMMENT_ENTITY_TYPE || (int)$parent->getObjectId() !== $cardId) {
if ($parent->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $parent->getObjectId() !== $cardId) {
throw new CommentNotFoundException();
}
} catch (CommentNotFoundException $e) {
@@ -358,7 +362,7 @@ class BoardImportService {
foreach ($assignments as $assignment) {
try {
$assignment = $this->assignmentMapper->insert($assignment);
$this->getImportSystem()->updateCardAssignment((int)$cardId, $assignment->getId(), $assignment);
$this->getImportSystem()->updateCardAssignment($cardId, (string)$assignment->getId(), $assignment);
$this->addOutput('Assignment ' . $assignment->getParticipant() . ' added');
} catch (NotFoundException $e) {
$this->addError('No origin or mapping found for card "' . $cardId . '" and ' . $assignment->getTypeString() . ' assignment "' . $assignment->getParticipant(), $e);

View File

@@ -16,7 +16,6 @@ use OCA\Deck\Db\Card;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Service\Importer\ABoardImportService;
use OCP\Comments\IComment;
use OCP\IUser;
use OCP\IUserManager;
@@ -119,7 +118,6 @@ class DeckJsonService extends ABoardImportService {
$comments[$this->cards[$sourceCard->id]->getId()][$commentOriginal->id] = $comment;
}
}
/** @var array<int, array<string, IComment>> */
return $comments;
}

View File

@@ -17,7 +17,7 @@ use Psr\Log\LoggerInterface;
class TrelloApiService extends TrelloJsonService {
/** @var string */
public static $name = 'Trello API';
protected bool $needValidateData = false;
protected $needValidateData = false;
/** @var IClient */
private $httpClient;
/** @var LoggerInterface */
@@ -176,7 +176,7 @@ class TrelloApiService extends TrelloJsonService {
if (empty($queryString['limit'])) {
return [];
}
if ((count($data) < $queryString['limit']) || (count($data) === 0)) {
if (count($data) < $queryString['limit']) {
return [];
}
$queryString['before'] = end($data)->id;

View File

@@ -159,7 +159,6 @@ class TrelloJsonService extends ABoardImportService {
$comments[$cardId][$commentId] = $comment;
}
}
/** @var array<int, array<string, IComment>> */
return $comments;
}

View File

@@ -43,24 +43,33 @@ class LabelService {
}
/**
* @param $labelId
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function find(int $labelId): Label {
public function find($labelId) {
if (is_numeric($labelId) === false) {
throw new BadRequestException('label id must be a number');
}
$this->permissionService->checkPermission($this->labelMapper, $labelId, Acl::PERMISSION_READ);
return $this->labelMapper->find($labelId);
}
/**
* @param $title
* @param $color
* @param $boardId
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function create(string $title, string $color, int $boardId): Label {
public function create($title, $color, $boardId) {
$this->labelServiceValidator->check(compact('title', 'color', 'boardId'));
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE);
@@ -97,13 +106,15 @@ class LabelService {
}
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete(int $id): Label {
public function delete($id) {
$this->labelServiceValidator->check(compact('id'));
$this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE);
@@ -116,13 +127,17 @@ class LabelService {
}
/**
* @param $id
* @param $title
* @param $color
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update(int $id, string $title, string $color): Label {
public function update($id, $title, $color) {
$this->labelServiceValidator->check(compact('title', 'color', 'id'));
$this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE);

View File

@@ -9,28 +9,53 @@ declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Model\CardDetails;
use OCP\Comments\ICommentsManager;
use OCP\IUserManager;
class OverviewService {
private CardService $cardService;
private BoardMapper $boardMapper;
private LabelMapper $labelMapper;
private CardMapper $cardMapper;
private AssignmentMapper $assignedUsersMapper;
private IUserManager $userManager;
private ICommentsManager $commentsManager;
private AttachmentService $attachmentService;
public function __construct(
private readonly CardService $cardService,
private readonly BoardMapper $boardMapper,
private readonly CardMapper $cardMapper,
CardService $cardService,
BoardMapper $boardMapper,
LabelMapper $labelMapper,
CardMapper $cardMapper,
AssignmentMapper $assignedUsersMapper,
IUserManager $userManager,
ICommentsManager $commentsManager,
AttachmentService $attachmentService,
) {
$this->cardService = $cardService;
$this->boardMapper = $boardMapper;
$this->labelMapper = $labelMapper;
$this->cardMapper = $cardMapper;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->userManager = $userManager;
$this->commentsManager = $commentsManager;
$this->attachmentService = $attachmentService;
}
public function findUpcomingCards(string $userId): array {
$userBoards = $this->boardMapper->findAllForUser($userId);
$boardOwnerIds = array_filter(array_map(function (Board $board) {
return count($board->getAcl() ?? []) === 0 ? $board->getId() : null;
return count($board->getAcl()) === 0 ? $board->getId() : null;
}, $userBoards));
$boardSharedIds = array_filter(array_map(function (Board $board) {
return count($board->getAcl() ?? []) > 0 ? $board->getId() : null;
return count($board->getAcl()) > 0 ? $board->getId() : null;
}, $userBoards));
$foundCards = array_merge(

View File

@@ -29,7 +29,6 @@ class PermissionService {
private array $users = [];
private CappedMemoryCache $boardCache;
/** @var CappedMemoryCache<array<Acl::PERMISSION_*, bool>> */
private CappedMemoryCache $permissionCache;
public function __construct(
@@ -50,16 +49,15 @@ class PermissionService {
/**
* Get current user permissions for a board by id
*
* @return array<Acl::PERMISSION_*, bool>
* @return bool|array
*/
public function getPermissions(int $boardId, ?string $userId = null): array {
public function getPermissions(int $boardId, ?string $userId = null) {
if ($userId === null) {
$userId = $this->userId;
}
$cacheKey = $boardId . '-' . $userId;
if ($cached = $this->permissionCache->get($cacheKey)) {
/** @var array<Acl::PERMISSION_*, bool> $cached */
return $cached;
}

View File

@@ -75,31 +75,35 @@ class StackService {
$this->stackServiceValidator = $stackServiceValidator;
}
/** @param Stack[] $stacks */
private function enrichStacksWithCards(array $stacks, $since = -1): void {
$cardsByStackId = $this->cardMapper->findAllForStacks(array_map(fn (Stack $stack) => $stack->getId(), $stacks), null, 0, $since);
private function enrichStackWithCards($stack, $since = -1) {
$cards = $this->cardMapper->findAll($stack->getId(), null, null, $since);
foreach ($cardsByStackId as $stackId => $cards) {
if (!$cards) {
continue;
}
if (\count($cards) === 0) {
return;
}
$stack->setCards($this->cardService->enrichCards($cards));
}
foreach ($stacks as $stack) {
if ($stack->getId() === $stackId) {
$stack->setCards($this->cardService->enrichCards($cards));
break;
}
}
private function enrichStacksWithCards($stacks, $since = -1) {
foreach ($stacks as $stack) {
$this->enrichStackWithCards($stack, $since);
}
}
/**
* @param $stackId
*
* @return \OCP\AppFramework\Db\Entity
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function find(int $stackId): Stack {
public function find($stackId) {
if (is_numeric($stackId) === false) {
throw new BadRequestException('stack id must be a number');
}
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
$stack = $this->stackMapper->find($stackId);
@@ -120,11 +124,17 @@ class StackService {
}
/**
* @return Stack[]
* @param $boardId
*
* @return array
* @throws \OCA\Deck\NoPermissionException
* @throws BadRequestException
*/
public function findAll(int $boardId, int $since = -1): array {
public function findAll($boardId, $since = -1) {
if (is_numeric($boardId) === false) {
throw new BadRequestException('boardId must be a number');
}
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ);
$stacks = $this->stackMapper->findAll($boardId);
$this->enrichStacksWithCards($stacks, $since);
@@ -132,11 +142,7 @@ class StackService {
return $stacks;
}
/**
* @return Stack[]
* @throws \OCP\DB\Exception
*/
public function findCalendarEntries(int $boardId): array {
public function findCalendarEntries($boardId) {
try {
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ);
} catch (NoPermissionException $e) {
@@ -146,11 +152,7 @@ class StackService {
return $this->stackMapper->findAll($boardId);
}
/**
* @return Stack[]
* @throws \OCP\DB\Exception
*/
public function fetchDeleted(int $boardId): array {
public function fetchDeleted($boardId) {
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
$stacks = $this->stackMapper->findDeleted($boardId);
$this->enrichStacksWithCards($stacks);
@@ -159,11 +161,17 @@ class StackService {
}
/**
* @return Stack[]
* @param $boardId
*
* @return array
* @throws \OCA\Deck\NoPermissionException
* @throws BadRequestException
*/
public function findAllArchived(int $boardId): array {
public function findAllArchived($boardId) {
if (is_numeric($boardId) === false) {
throw new BadRequestException('board id must be a number');
}
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ);
$stacks = $this->stackMapper->findAll($boardId);
$labels = $this->labelMapper->getAssignedLabelsForBoard($boardId);
@@ -178,18 +186,22 @@ class StackService {
$stacks[$stackIndex]->setCards($cards);
}
/** @var Stack[] $stacks */
return $stacks;
}
/**
* @param $title
* @param $boardId
* @param integer $order
*
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function create(string $title, int $boardId, int $order): Stack {
public function create($title, $boardId, $order) {
$this->stackServiceValidator->check(compact('title', 'boardId', 'order'));
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE);
@@ -211,13 +223,19 @@ class StackService {
}
/**
* @return Stack The deleted stack.
* @param $id
*
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete(int $id): Stack {
public function delete($id) {
if (is_numeric($id) === false) {
throw new BadRequestException('stack id must be a number');
}
$this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE);
$stack = $this->stackMapper->find($id);
@@ -229,19 +247,26 @@ class StackService {
);
$this->changeHelper->boardChanged($stack->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId()));
$this->enrichStacksWithCards([$stack]);
$this->enrichStackWithCards($stack);
return $stack;
}
/**
* @param $id
* @param $title
* @param $boardId
* @param $order
* @param $deletedAt
*
* @return \OCP\AppFramework\Db\Entity
* @throws StatusException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update(int $id, string $title, int $boardId, int $order, ?int $deletedAt): Stack {
public function update($id, $title, $boardId, $order, $deletedAt) {
$this->stackServiceValidator->check(compact('id', 'title', 'boardId', 'order'));
$this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE);
@@ -269,13 +294,16 @@ class StackService {
}
/**
* @return array<int, Stack> The stacks in the correct order.
* @param $id
* @param $order
*
* @return array
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function reorder(int $id, int $order): array {
public function reorder($id, $order) {
$this->stackServiceValidator->check(compact('id', 'order'));
$this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE);
@@ -296,7 +324,7 @@ class StackService {
if ($stack->id !== $id) {
$stack->setOrder($i++);
}
$stack = $this->stackMapper->update($stack);
$this->stackMapper->update($stack);
$result[$stack->getOrder()] = $stack;
}
$this->changeHelper->boardChanged($stackToSort->getBoardId());

View File

@@ -13,6 +13,7 @@ namespace OCA\Deck\Sharing;
use OC\Files\Cache\Cache;
use OCA\Deck\Cache\AttachmentCacheHelper;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\User;
@@ -98,13 +99,9 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
* @inheritDoc
*/
public function create(IShare $share) {
$cardId = (int)$share->getSharedWith();
$cardId = $share->getSharedWith();
$boardId = $this->cardMapper->findBoardId($cardId);
$valid = $boardId !== null;
if (!$valid) {
throw new GenericShareException('Card not found', $this->l->t('Card not found'), 404);
}
try {
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_EDIT);
} catch (NoPermissionException $e) {
@@ -150,7 +147,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
);
$data = $this->getRawShare($shareId);
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
$this->attachmentCacheHelper->clearAttachmentCount((int)$cardId);
return $this->createShareObject($data);
}
@@ -216,7 +213,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$data = $cursor->fetch();
$cursor->closeCursor();
@@ -300,7 +297,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
->executeStatement();
->execute();
/*
* Update all user defined group shares
@@ -313,7 +310,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
->executeStatement();
->execute();
/*
* Now update the permissions for all children that have not set it to 0
@@ -323,7 +320,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0)))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->executeStatement();
->execute();
return $share;
}
@@ -338,7 +335,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())));
$qb->executeStatement();
$qb->execute();
$this->attachmentCacheHelper->clearAttachmentCount((int)$share->getSharedWith());
}
@@ -358,7 +355,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
))
->executeQuery();
->execute();
$data = $stmt->fetch();
$stmt->closeCursor();
@@ -379,14 +376,14 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
'file_target' => $qb->createNamedParameter($share->getTarget()),
'permissions' => $qb->createNamedParameter(0),
'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()),
])->executeStatement();
])->execute();
} elseif ($data['permissions'] !== 0) {
// Already a userroom share. Update it.
$qb = $this->dbConnection->getQueryBuilder();
$qb->update('share')
->set('permissions', $qb->createNamedParameter(0))
->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id'])))
->executeStatement();
->execute();
}
}
@@ -400,7 +397,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))
);
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$data = $cursor->fetch();
$cursor->closeCursor();
@@ -417,7 +414,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))
);
$qb->executeStatement();
$qb->execute();
return $this->getShareById((int)$share->getId(), $recipient);
}
@@ -438,7 +435,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
))
->setMaxResults(1)
->executeQuery();
->execute();
$data = $stmt->fetch();
$stmt->closeCursor();
@@ -512,7 +509,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->orderBy('s.id');
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$shares = [];
while ($data = $cursor->fetch()) {
$shares[$data['fileid']][] = $this->createShareObject($data);
@@ -557,7 +554,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->setFirstResult($offset);
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$shares = [];
while ($data = $cursor->fetch()) {
$shares[] = $this->createShareObject($data);
@@ -585,7 +582,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->where($qb->expr()->eq('s.id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK)));
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$data = $cursor->fetch();
$cursor->closeCursor();
@@ -650,7 +647,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
));
$stmt = $query->executeQuery();
$stmt = $query->execute();
while ($data = $stmt->fetch()) {
$this->applyBoardPermission($shareMap[$data['parent']], (int)$data['permissions'], $userId);
@@ -680,7 +677,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->from('share')
->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_DECK)))
->executeQuery();
->execute();
$shares = [];
while ($data = $cursor->fetch()) {
@@ -752,7 +749,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$qb->andWhere($qb->expr()->eq('dc.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
while ($data = $cursor->fetch()) {
if (!$this->isAccessibleResult($data)) {
continue;
@@ -845,7 +842,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
/**
* @inheritDoc
* @psalm-suppress InvalidReturnType Not returning anything
*/
public function getShareByToken($token) {
throw new ShareNotFound();
@@ -855,7 +851,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->from('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
->executeQuery();
->execute();
$data = $cursor->fetch();
@@ -1019,7 +1015,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_DECK)))
->orderBy('id');
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
while ($data = $cursor->fetch()) {
$children[] = $this->createShareObject($data);
}
@@ -1042,7 +1038,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
)
);
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
while ($data = $cursor->fetch()) {
$share = $this->createShareObject($data);
@@ -1059,7 +1055,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->where($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK)))
->andWhere($qb->expr()->notIn('s.share_with', $qb->createNamedParameter($allCardIds, IQueryBuilder::PARAM_STR_ARRAY)));
$cursor = $qb->executeQuery();
$cursor = $qb->execute();
$shares = [];
while ($data = $cursor->fetch()) {
$shares[] = $this->createShareObject($data);

282
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "deck",
"version": "2.0.0-dev.0",
"version": "1.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "deck",
"version": "2.0.0-dev.0",
"version": "1.16.0",
"license": "agpl",
"dependencies": {
"@babel/polyfill": "^7.12.1",
@@ -3636,9 +3636,9 @@
"license": "MIT"
},
"node_modules/@nextcloud/dialogs": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.3.2.tgz",
"integrity": "sha512-ioZ483wmKdNX1HdSJ1EG7ewTSyQAqlmbBALkhT4guZdR9JG8VIdnijX15qwKgAWITG2y36PWoi9Rimb3dDf+7A==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.3.1.tgz",
"integrity": "sha512-lklTssGdphRZKoR07pYU88btqguEKcQjEpKYom342i1eiMPiejgmoPZEignWJvJhpaN9CT5FoGndCrqqS3BswA==",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@mdi/js": "^7.4.47",
@@ -3661,11 +3661,11 @@
"webdav": "^5.8.0"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || ^24.0.0",
"npm": "^10.5.1"
"node": "^20.0.0",
"npm": "^10.0.0"
},
"peerDependencies": {
"@nextcloud/vue": "^8.24.0",
"@nextcloud/vue": "^8.23.1",
"vue": "^2.7.16"
}
},
@@ -5402,11 +5402,10 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0"
@@ -6441,13 +6440,13 @@
"dev": true
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
@@ -6854,6 +6853,16 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -6890,9 +6899,7 @@
"peer": true
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7236,11 +7243,10 @@
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q=="
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
@@ -7278,6 +7284,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -7444,43 +7451,16 @@
"dev": true
},
"node_modules/cipher-base": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
"integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.2"
},
"engines": {
"node": ">= 0.10"
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/cipher-base/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/cjs-module-lexer": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
@@ -7755,19 +7735,18 @@
}
},
"node_modules/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
},
"engines": {
@@ -7791,39 +7770,6 @@
"dev": true,
"peer": true
},
"node_modules/compression/node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/compression/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"dev": true,
@@ -9069,6 +9015,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -9137,11 +9084,10 @@
}
},
"node_modules/editorconfig/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -9417,6 +9363,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9426,6 +9373,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -9440,6 +9388,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -9449,15 +9398,15 @@
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
"integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
"dev": true,
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
"get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -10977,15 +10926,12 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -11089,6 +11035,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -11142,6 +11089,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -11175,6 +11123,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -11397,6 +11346,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11482,6 +11432,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11494,7 +11445,9 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -11539,6 +11492,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -14433,11 +14387,10 @@
}
},
"node_modules/js-beautify/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -15108,6 +15061,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -16381,11 +16335,10 @@
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
@@ -17470,6 +17423,16 @@
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -18571,49 +18534,19 @@
"peer": true
},
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"license": "(MIT AND BSD-3-Clause)",
"peer": true,
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.0"
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
},
"bin": {
"sha.js": "bin.js"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sha.js/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -19727,10 +19660,11 @@
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@@ -19952,11 +19886,10 @@
"dev": true
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}
@@ -19968,9 +19901,9 @@
"dev": true
},
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
"integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
"integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -20910,9 +20843,9 @@
}
},
"node_modules/vue-at": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/vue-at/-/vue-at-2.5.2.tgz",
"integrity": "sha512-06FFK8V/VIS3appZf/l65H9ijmoVDrL1GHm/O/zRpHADGUulmOLa7OP3O3u6ab49n4HNJySQqYedPGqJz8EpQw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/vue-at/-/vue-at-2.5.1.tgz",
"integrity": "sha512-c17Rn8vVTXp/rvWV76jNB2PVx534eqIAP5/Tv1twCt0ebtRPIgNaJJwSaoRVISQ6vM6m3+owmtpAG1qr5g9fyQ==",
"engines": {
"node": ">= 14.x"
},
@@ -21241,10 +21174,9 @@
}
},
"node_modules/webdav/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "deck",
"description": "",
"version": "2.0.0-dev.0",
"version": "1.16.0",
"authors": [
{
"name": "Julius Härtl",
@@ -70,8 +70,8 @@
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": "^22.0.0",
"npm": "^10.5.0"
"node": "^20.0.0",
"npm": "^10.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.2.0",

View File

@@ -6,9 +6,7 @@
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config"
errorBaseline="tests/psalm-baseline.xml"
findUnusedBaselineEntry="true"
findUnusedCode="false"
phpVersion="8.2"
phpVersion="8.1"
>
<stubs>
<file name="tests/stub.phpstub" preloadClasses="true"/>
@@ -17,7 +15,6 @@
<directory name="lib" />
<ignoreFiles>
<directory name="vendor" />
<directory name="vendor-bin" />
</ignoreFiles>
</projectFiles>
<extraFiles>
@@ -29,6 +26,13 @@
<referencedMethod name="/Db\\.*::.*/" />
</errorLevel>
</UndefinedMagicMethod>
<UndefinedInterfaceMethod>
<errorLevel type="suppress">
<!-- FIXME Deprecated event handling -->
<referencedMethod name="OCP\IUserManager::listen" />
<referencedMethod name="OCP\IGroupManager::listen" />
</errorLevel>
</UndefinedInterfaceMethod>
<UndefinedClass>
<errorLevel type="suppress">
<referencedClass name="OC\*" />

View File

@@ -276,15 +276,18 @@ export default {
position: relative;
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.board {
padding-left: $board-spacing;
position: relative;
max-height: calc(100% - var(--default-clickable-area));
overflow: hidden;
overflow-x: auto;
flex-grow: 1;
scrollbar-gutter: stable;
}
/**
@@ -294,15 +297,11 @@ export default {
.smooth-dnd-container.horizontal {
display: flex;
align-items: stretch;
gap: $board-gap;
padding: 0 $board-gap;
height: 100%;
&:deep(.stack-draggable-wrapper.smooth-dnd-draggable-wrapper) {
display: flex;
height: auto;
flex: 0 1 $card-max-width;
min-width: $card-min-width;
.stack {
display: flex;
@@ -310,13 +309,16 @@ export default {
position: relative;
.smooth-dnd-container.vertical {
$margin-x: calc($stack-gap * -1);
flex-grow: 1;
display: flex;
flex-direction: column;
gap: $stack-gap;
padding: $stack-gap;
margin: 0 $margin-x;
// Margin left instead of padidng to avoid jumps on dropping a card
margin-left: $stack-spacing;
padding-right: $stack-spacing;
overflow-x: hidden;
overflow-y: auto;
padding-top: 15px;
margin-top: -10px;
scrollbar-gutter: stable;
}

View File

@@ -14,10 +14,10 @@
{{ stack.title }}
</h3>
<h3 v-else-if="!editing"
title="stack.title"
dir="auto"
tabindex="0"
:aria-label="stack.title"
:title="stack.title"
class="stack__title"
@click="startEditing(stack)"
@keydown.enter="startEditing(stack)">
@@ -108,7 +108,7 @@
</Container>
<transition name="slide-bottom" appear>
<div v-if="showAddCard" class="stack__card-add">
<div v-show="showAddCard" class="stack__card-add">
<form :class="{ 'icon-loading-small': stateCardCreating }"
@submit.prevent.stop="clickAddCard()">
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add a new card') }}</label>
@@ -365,31 +365,37 @@ export default {
@import './../../css/variables';
.stack {
width: 100%;
width: $stack-width + $stack-spacing * 3;
}
.stack__header {
display: flex;
position: sticky;
top: 0;
height: var(--default-clickable-area);
z-index: 100;
padding-left: $card-spacing;
padding-right: $card-spacing;
margin: 6px;
margin-top: 0;
cursor: grab;
background-color: var(--color-main-background);
// Smooth fade out of the cards at the top
&:before {
content: '';
content: ' ';
display: block;
position: absolute;
width: 100%;
height: $stack-gap;
bottom: 0;
width: calc(100% - 16px);
height: 20px;
top: 30px;
left: 0px;
z-index: 99;
transition: top var(--animation-slow);
background-image: linear-gradient(180deg, var(--color-main-background) 0%, transparent 100%);
transform: translateY(100%);
background-image: linear-gradient(180deg, var(--color-main-background) 3px, rgba(255, 255, 255, 0) 100%);
body.theme--dark & {
background-image: linear-gradient(180deg, var(--color-main-background) 3px, rgba(0, 0, 0, 0) 100%);
}
}
& > * {
@@ -398,10 +404,8 @@ export default {
}
h3, form {
flex: 1 1 auto;
min-width: 0;
flex-grow: 1;
display: flex;
align-items: center;
cursor: inherit;
margin: 0;
@@ -414,8 +418,9 @@ export default {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc($stack-width - 60px);
border-radius: 3px;
padding: $card-padding;
padding: 4px 4px;
font-size: var(--default-font-size);
&:focus-visible {
@@ -425,6 +430,7 @@ export default {
}
form {
margin: -4px;
input {
font-weight: bold;
padding: 0 6px;
@@ -447,25 +453,14 @@ export default {
flex-shrink: 0;
z-index: 100;
display: flex;
padding-bottom: $stack-gap;
margin-bottom: 5px;
padding-top: var(--default-grid-baseline);
background-color: var(--color-main-background);
position: relative;
// Smooth fade out of the cards at the top
&:before {
content: '';
display: block;
position: absolute;
width: 100%;
height: $stack-gap;
z-index: 99;
transition: bottom var(--animation-slow);
background-image: linear-gradient(0deg, var(--color-main-background) 0%, transparent 100%);
transform: translateY(-100%);
}
form {
display: flex;
margin-left: $stack-spacing;
margin-right: $stack-spacing;
width: 100%;
border: 2px solid var(--color-border-maxcontrast);
border-radius: var(--border-radius-large);
@@ -486,6 +481,7 @@ export default {
input {
border: none;
margin: 0;
padding: 4px;
}
}

View File

@@ -101,8 +101,6 @@ export default {
</script>
<style lang="scss" scoped>
@import './../../css/variables';
.badges {
display: flex;
width: 100%;
@@ -113,7 +111,6 @@ export default {
.icon-badge {
color: var(--color-text-maxcontrast);
display: flex;
align-items: center;
margin-right: 2px;
span,
@@ -128,7 +125,6 @@ export default {
flex-direction: row;
flex-wrap: wrap;
gap: 3px;
height: var(--default-clickable-area);
}
.badge-left, .badge-right {

View File

@@ -4,10 +4,13 @@
-->
<template>
<div v-if="cardId && ( attachments.length > 0 )" class="card-cover">
<div v-for="attachment in attachments"
<div v-if="referencePreview" class="card-cover">
<div class="image-wrapper rounded-left rounded-right" :style="{ backgroundImage: `url(${referencePreview})`}" />
</div>
<div v-else-if="cardId && ( attachments.length > 0 )" class="card-cover">
<div v-for="(attachment, index) in attachments"
:key="attachment.id"
class="image-wrapper"
:class="['image-wrapper', { 'rounded-left': index === 0 }, { 'rounded-right': index === attachments.length - 1 }]"
:style="{ backgroundImage: `url(${attachmentPreview(attachment)})` }" />
</div>
</template>
@@ -74,7 +77,9 @@ export default {
.card-cover {
height: 90px;
display: flex;
margin: $card-image-margin $card-image-margin 0;
margin-top: -4px;
margin-left: -4px;
margin-right: -4px;
.image-wrapper {
flex: 1;
@@ -82,6 +87,12 @@ export default {
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
&.rounded-left {
border-top-left-radius: calc(var(--border-radius-large) - 1px);
}
&.rounded-right {
border-top-right-radius: calc(var(--border-radius-large) - 1px);
}
}
}
</style>

View File

@@ -6,7 +6,7 @@
<template>
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
<div :ref="`card${card.id}`"
:class="{'compact': compactMode, 'current-card': currentCard, 'no-labels': !hasLabels, 'card__editable': canEdit, 'card__archived': card.archived, 'card__highlight': highlight}"
:class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'card__editable': canEdit, 'card__archived': card.archived, 'card__highlight': highlight}"
tag="div"
:tabindex="0"
class="card"
@@ -20,7 +20,7 @@
<CardCover v-if="showCardCover" :card-id="card.id" />
<div class="card-upper">
<h4 v-if="editingTitle === 0" key="title-view" dir="auto">
<span contenteditable="false">{{ displayTitle }}</span>
<span class="dragDisabled" contenteditable="false">{{ displayTitle }}</span>
</h4>
<h4 v-if="editingTitle >= 1"
key="title-edit"
@@ -331,14 +331,12 @@ export default {
border-radius: var(--border-radius-large);
font-size: 100%;
background-color: var(--color-main-background);
padding: $card-padding;
margin-bottom: $card-spacing;
padding: var(--default-grid-baseline) $card-padding;
border: 2px solid var(--color-border-dark);
width: 100%;
display: flex;
flex-direction: column;
gap: $card-gap;
overflow: hidden;
cursor: pointer;
&:deep(*:not(.dragDisabled)) {
cursor: pointer;
@@ -361,10 +359,12 @@ export default {
h4 {
font-weight: normal;
margin: 0;
padding: var(--default-grid-baseline);
flex-grow: 1;
font-size: 100%;
overflow: hidden;
word-wrap: break-word;
padding-left: 4px;
align-self: center;
:deep(a) {
@@ -374,6 +374,9 @@ export default {
&.editable {
span {
cursor: text;
padding-right: 8px;
padding-top: 3px;
padding-bottom: 3px;
&:focus, &:focus-visible {
outline: none;
@@ -382,7 +385,6 @@ export default {
&:focus-within {
outline: 2px solid var(--color-border-dark);
outline-offset: 4px;
border-radius: 3px;
}
}
@@ -425,6 +427,8 @@ export default {
.card-labels {
display: flex;
align-items: end;
padding-left: var(--default-grid-baseline);
padding-top: var(--default-grid-baseline);
.labels {
flex-wrap: wrap;
@@ -440,7 +444,7 @@ export default {
.card-related {
display: flex;
padding: 4px;
padding: 12px;
padding-bottom: 0px;
color: var(--color-text-maxcontrast);
@@ -465,8 +469,8 @@ export default {
height: 32px;
width: 32px;
}
&.no-labels {
padding-bottom: var(--default-grid-baseline);
&.has-labels {
padding-bottom: $card-padding;
}
.labels {
height: 6px;

View File

@@ -14,25 +14,45 @@
</div>
<div v-else-if="isValidFilter" class="overview">
<div v-for="columnProps in columnPropsList" :key="columnProps.title" class="dashboard-column">
<div class="dashboard-column__header">
<h3 class="dashboard-column__header-title"
:title="columnProps.title"
:aria-label="columnProps.title">
{{ t('deck', columnProps.title) }}
</h3>
<div class="dashboard-column">
<h3>{{ t('deck', 'Overdue') }}</h3>
<div v-for="card in sortCards('overdue')" :key="card.id">
<CardItem :id="card.id" />
</div>
<div class="dashboard-column__list">
<template v-if="columnProps.sort === false">
<CardItem v-for="card in filterCards(columnProps.filter)"
:id="card.id"
:key="card.id" />
</template>
<template v-else>
<CardItem v-for="card in sortCards(filterCards(columnProps.filter))"
:id="card.id"
:key="card.id" />
</template>
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'Today') }}</h3>
<div v-for="card in sortCards('today')" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'Tomorrow') }}</h3>
<div v-for="card in sortCards('tomorrow')" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'Next 7 days') }}</h3>
<div v-for="card in sortCards('nextSevenDays')" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'Later') }}</h3>
<div v-for="card in sortCards('later')" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'No due') }}</h3>
<div v-for="card in assignedCardsDashboard.nodue" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
</div>
@@ -53,34 +73,6 @@ const SUPPORTED_FILTERS = [
FILTER_UPCOMING,
]
const COLUMN_PROPS_LIST = [
{
title: 'Overdue',
filter: 'overdue',
},
{
title: 'Today',
filter: 'today',
},
{
title: 'Tomorrow',
filter: 'tomorrow',
},
{
title: 'Next 7 days',
filter: 'nextSevenDays',
},
{
title: 'Later',
filter: 'later',
},
{
title: 'No due',
filter: 'nodue',
sort: false,
},
]
export default {
name: 'Overview',
components: {
@@ -97,7 +89,6 @@ export default {
data() {
return {
loading: true,
columnPropsList: COLUMN_PROPS_LIST,
}
},
computed: {
@@ -134,16 +125,16 @@ export default {
}
this.loading = false
},
filterCards(when) {
return this.assignedCardsDashboard[when]
},
sortCards(cards) {
sortCards(when) {
const cards = this.assignedCardsDashboard[when]
if (!cards) {
return null
} else {
return cards.toSorted((current, next) => {
const currentDueDate = new Date(current.duedate)
const nextDueDate = new Date(next.duedate)
return currentDueDate - nextDueDate
})
}
@@ -160,75 +151,38 @@ export default {
position: relative;
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.overview {
position: relative;
overflow-x: auto;
flex-grow: 1;
scrollbar-gutter: stable;
height: calc(100% - var(--default-clickable-area));
overflow-x: scroll;
display: flex;
align-items: stretch;
gap: $board-gap;
padding: 0 $board-gap;
padding-left: $board-spacing;
padding-right: $board-spacing;
.dashboard-column {
display: flex;
flex-direction: column;
flex: 0 1 $card-max-width;
min-width: $card-min-width;
min-width: $stack-width;
width: $stack-width;
margin-left: $stack-spacing;
margin-right: $stack-spacing;
.dashboard-column__header {
display: flex;
h3 {
font-size: var(--default-font-size);
margin: -6px;
margin-bottom: 12px;
padding: 6px 13px;
position: sticky;
top: 0;
height: var(--default-clickable-area);
z-index: 100;
margin-top: 0;
background-color: var(--color-main-background);
// Smooth fade out of the cards at the top
&:before {
content: '';
display: block;
position: absolute;
width: 100%;
height: 20px;
top: 30px;
left: 0px;
z-index: 99;
transition: top var(--animation-slow);
background-image: linear-gradient(180deg, var(--color-main-background) 3px, rgba(255, 255, 255, 0) 100%);
body.theme--dark & {
background-image: linear-gradient(180deg, var(--color-main-background) 3px, rgba(0, 0, 0, 0) 100%);
}
}
}
.dashboard-column__header-title {
display: flex;
align-items: center;
height: var(--default-clickable-area);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: $card-padding;
font-size: var(--default-font-size);
}
.dashboard-column__list {
$margin-x: calc($stack-gap * -1);
display: flex;
flex-direction: column;
gap: $stack-gap;
padding: $stack-gap;
margin: 0 $margin-x;
overflow-y: auto;
scrollbar-gutter: stable;
border: 1px solid var(--color-main-background);
}
}
}

View File

@@ -4,19 +4,16 @@
-->
<template>
<section v-if="searchQuery!==''" class="global-search">
<header class="search-header">
<h2>
<NcRichText :text="$route.params.id ? t('deck', 'Search for {searchQuery} in other boards') : t('deck', 'Search for {searchQuery} in all boards')"
:arguments="queryStringArgs" />
<span v-if="loading" class="icon-loading-small" />
</h2>
<NcActions>
<NcActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
</NcActions>
</header>
<div v-if="searchQuery!==''" class="global-search">
<h2>
<NcRichText :text="t('deck', 'Search for {searchQuery} in all boards')" :arguments="queryStringArgs" />
<div v-if="loading" class="icon-loading-small" />
</h2>
<NcActions>
<NcActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
</NcActions>
<div class="search-wrapper">
<template v-if="loading || filteredResults.length > 0">
<div v-if="loading || filteredResults.length > 0" class="search-results">
<CardItem v-for="card in filteredResults"
:id="card.id"
:key="card.id"
@@ -29,12 +26,12 @@
{{ t('deck', 'No results found') }}
</div>
</InfiniteLoading>
</template>
<template v-else>
</div>
<div v-else>
<p>{{ t('deck', 'No results found') }}</p>
</template>
</div>
</div>
</section>
</div>
</template>
<script>
@@ -162,7 +159,7 @@ export default {
.global-search {
width: 100%;
padding: $board-gap;
padding: $board-spacing + $stack-spacing;
padding-bottom: 0;
overflow: hidden;
min-height: 35vh;
@@ -172,24 +169,17 @@ export default {
border-top: 1px solid var(--color-border);
z-index: 1010;
position: relative;
display: flex;
flex-direction: column;
.action-item.icon-close {
position: absolute;
top: 10px;
right: 10px;
}
.search-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
h2 {
margin: 0;
padding: var(--default-grid-baseline) var(--default-grid-baseline) $board-gap;
.search-wrapper {
overflow: scroll;
height: 100%;
position: relative;
padding: 10px;
}
h2 > div {
@@ -199,24 +189,23 @@ export default {
margin-right: 20px;
}
}
h2:deep(span) {
background-color: var(--color-background-dark);
padding: 3px;
border-radius: var(--border-radius);
}
.search-wrapper {
overflow: auto;
height: 100%;
position: relative;
.search-results {
display: flex;
gap: $stack-gap;
flex-wrap: wrap;
& > .drop-upload--card {
flex: 0 1 $card-max-width;
min-width: $card-min-width;
& > div {
flex-grow: 0;
}
}
&:deep(.card) {
width: $stack-width;
margin-right: $stack-spacing;
}
}
</style>

View File

@@ -62,13 +62,14 @@ export default {
$clickable-area: var(--default-clickable-area);
.card--placeholder {
min-width: $card-min-width;
max-width: $card-min-width;
width: $stack-width;
margin-right: $stack-spacing;
padding: $card-padding;
transition: box-shadow 0.1s ease-in-out;
box-shadow: 0 0 2px 0 var(--color-box-shadow);
border-radius: var(--border-radius-large);
font-size: 100%;
margin-bottom: $card-spacing;
height: 100px;
}

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