Compare commits

..

40 Commits

Author SHA1 Message Date
Julius Härtl
e6bed1134b Prepare 1.3.0-beta1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 22:39:20 +01:00
Julius Härtl
3356852d98 Merge pull request #2683 from nextcloud/bugfix/noid/calendar-click
Handle clicks on calendar entries
2021-01-04 22:33:35 +01:00
Julius Härtl
aa4fd11ce2 Merge pull request #2638 from nextcloud/enh/files 2021-01-04 22:33:17 +01:00
Julius Härtl
771b6da4cf Handle clicks on calendar entries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 22:28:26 +01:00
Julius Härtl
79468e6f06 Use board permissions to be applied for the shares
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 22:26:04 +01:00
Julius Härtl
0c6dfb2442 Properly cast columns before joining
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 17:14:11 +01:00
Julius Härtl
fccc6379ae Refresh cookie jar when acting as a different user
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:33:41 +01:00
Julius Härtl
1316d6bd41 Ensure user exists
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:30:34 +01:00
Julius Härtl
897e7e1e5c Show error on upload failure
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:30:25 +01:00
Julius Härtl
99f4f9a95a Remove old file handling
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:30:15 +01:00
Julius Härtl
3b0087f35a Try to debug psql tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:20:49 +01:00
Julius Härtl
7d8ce30e3f Fix eslint
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:08:01 +01:00
Julius Härtl
aeb05d3687 Remove rather useless unit tests that are just passing arguments
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:01:56 +01:00
Julius Härtl
9d1e4722e7 Make 1.3 21 only
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:01:56 +01:00
Julius Härtl
08842d239e Fix tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 16:01:55 +01:00
Julius Härtl
482a679cef Testing: add fallback for testing old attachments
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 15:59:07 +01:00
Julius Härtl
019444f9fc Handle unshare in frontend
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 15:59:06 +01:00
Julius Härtl
199cccf86b Add 1.1 api version to handle other attachment types properly
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 15:58:40 +01:00
Julius Härtl
8ab7019693 Add test for removing share
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:11:14 +01:00
Julius Härtl
00c24065c6 Test sharing a file to a card
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:11:13 +01:00
Julius Härtl
6f040d030f Add integration tests for sharing permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:11:13 +01:00
Julius Härtl
609a7b275f Integration tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:55 +01:00
Julius Härtl
db2ceae720 Fix attachment direct link
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
b26caaa6e5 Fix integration tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
74814ad614 Fix test mocking
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
7291c46d0d WIP work on delete/update
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
c440b7ff7d Remove autocomplete plugin for deck sharing results
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
82e2621eb1 Properly insert files to description
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:54 +01:00
Julius Härtl
32b2823538 Still fallback to old attachment display method if needed
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
e095302750 Fix counting attachments
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
65e19f9ea4 Move to card selector modal
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
a53e50e55f Bump to master for new sharing API changes
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
99379beb56 Move to dedicated file attachment service
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
67c90b1da8 Attachments sidebar
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
02237040d4 Handle nonexisting share provider registration
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:53 +01:00
Julius Härtl
715ab2249b Collaboration search provider
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:52 +01:00
Julius Härtl
bd032dfaf4 Open file in viewer if available
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:52 +01:00
Julius Härtl
ffd1f677c5 Fetch new attachment type
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:52 +01:00
Julius Härtl
d9ef7afa9e Load viewer
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:52 +01:00
Julius Härtl
e92d34bfcb Implement share provider for deck
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-01-04 12:10:52 +01:00
62 changed files with 3188 additions and 1231 deletions

View File

@@ -6,7 +6,7 @@ on:
jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:

View File

@@ -81,6 +81,8 @@ jobs:
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
cat config/config.php
./occ user:list
./occ app:enable --force ${{ env.APP_NAME }}
php -S localhost:8080 &

View File

@@ -66,7 +66,7 @@ jobs:
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, zip, gd
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql
coverage: none
- name: Set up PHPUnit

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ocp-version: [ 'dev-master', 'v20.0.1' ]
ocp-version: [ 'dev-master' ]
name: Nextcloud ${{ matrix.ocp-version }}
steps:
- name: Checkout

View File

@@ -13,6 +13,7 @@ $config
->notPath('build')
->notPath('l10n')
->notPath('src')
->notPath('node_modules')
->notPath('vendor')
->in(__DIR__);
return $config;

View File

@@ -1,25 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.2.5 - 2021-03-05
## 1.3.0 - unreleased
### Fixed
* [#2845](https://github.com/nextcloud/deck/pull/2845) Search by mail on the board sharing input
* [#2850](https://github.com/nextcloud/deck/pull/2850) Switch to Content-Disposition attachment and check for sane mimetypes
* [#2860](https://github.com/nextcloud/deck/pull/2860) Properly pass the user to fetch circles when calling through occ
## 1.2.4 - 2021-02-02
### Fixed
* [#2715](https://github.com/nextcloud/deck/pull/2715) Handle clicks on calendar entries
* [#2752](https://github.com/nextcloud/deck/pull/2752) Remove invalid activity parameters
* [#2755](https://github.com/nextcloud/deck/pull/2755) Properly set author for activity events that are triggered by cron
* [#2757](https://github.com/nextcloud/deck/pull/2757) Remove repair step which is no longer needed as we cleanup properly
* [#2758](https://github.com/nextcloud/deck/pull/2758) Fix deck activity emails not being translated
## 1.2.3 - 2021-01-04
### Added
* [#2638](https://github.com/nextcloud/deck/pull/2638) Sharing files to cards
* [#2683](https://github.com/nextcloud/deck/pull/2683) Handle clicks on calendar entries
* Nextcloud 21 compatiblity
### Fixed
* [#2622](https://github.com/nextcloud/deck/pull/2622) Fix gradient and stack header spacing for safari

View File

@@ -17,7 +17,7 @@
- 🚀 Get your project organized
</description>
<version>1.2.5</version>
<version>1.3.0-beta1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
@@ -32,17 +32,22 @@
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<php min-version="5.6"/>
<php min-version="7.3"/>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="5.5">mysql</database>
<nextcloud min-version="18" max-version="20" />
<nextcloud min-version="21" max-version="21" />
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
<job>OCA\Deck\Cron\ScheduledNotifications</job>
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
</background-jobs>
<repair-steps>
<post-migration>
<step>OCA\Deck\Migration\UnknownUsers</step>
</post-migration>
</repair-steps>
<commands>
<command>OCA\Deck\Command\UserExport</command>
</commands>

View File

@@ -80,59 +80,66 @@ return [
['name' => 'label#delete', 'url' => '/labels/{labelId}', 'verb' => 'DELETE'],
// api
['name' => 'board_api#index', 'url' => '/api/v1.0/boards', 'verb' => 'GET'],
['name' => 'board_api#get', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'GET'],
['name' => 'board_api#create', 'url' => '/api/v1.0/boards', 'verb' => 'POST'],
['name' => 'board_api#delete', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'DELETE'],
['name' => 'board_api#update', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'PUT'],
['name' => 'board_api#undo_delete', 'url' => '/api/v1.0/boards/{boardId}/undo_delete', 'verb' => 'POST'],
['name' => 'board_api#addAcl', 'url' => '/api/v1.0/boards/{boardId}/acl', 'verb' => 'POST'],
['name' => 'board_api#deleteAcl', 'url' => '/api/v1.0/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
['name' => 'board_api#updateAcl', 'url' => '/api/v1.0/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
['name' => 'board_api#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
['name' => 'board_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'GET'],
['name' => 'board_api#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
['name' => 'board_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'DELETE'],
['name' => 'board_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'PUT'],
['name' => 'board_api#undo_delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/undo_delete', 'verb' => 'POST'],
['name' => 'board_api#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
['name' => 'stack_api#index', 'url' => '/api/v1.0/boards/{boardId}/stacks', 'verb' => 'GET'],
['name' => 'stack_api#getArchived', 'url' => '/api/v1.0/boards/{boardId}/stacks/archived', 'verb' => 'GET'],
['name' => 'stack_api#get', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'GET'],
['name' => 'stack_api#create', 'url' => '/api/v1.0/boards/{boardId}/stacks', 'verb' => 'POST'],
['name' => 'stack_api#update', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'PUT'],
['name' => 'stack_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'DELETE'],
['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'],
['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'],
['name' => 'stack_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}', 'verb' => 'GET'],
['name' => 'stack_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'POST'],
['name' => 'stack_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}', 'verb' => 'PUT'],
['name' => 'stack_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}', 'verb' => 'DELETE'],
['name' => 'card_api#get', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'GET'],
['name' => 'card_api#create', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', 'verb' => 'POST'],
['name' => 'card_api#update', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'PUT'],
['name' => 'card_api#assignLabel', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', 'verb' => 'PUT'],
['name' => 'card_api#removeLabel', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel', 'verb' => 'PUT'],
['name' => 'card_api#assignUser', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser', 'verb' => 'PUT'],
['name' => 'card_api#unassignUser', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/unassignUser', 'verb' => 'PUT'],
['name' => 'card_api#reorder', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder', 'verb' => 'PUT'],
['name' => 'card_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'DELETE'],
['name' => 'card_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'GET'],
['name' => 'card_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards', 'verb' => 'POST'],
['name' => 'card_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'PUT'],
['name' => 'card_api#assignLabel', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', 'verb' => 'PUT'],
['name' => 'card_api#removeLabel', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel', 'verb' => 'PUT'],
['name' => 'card_api#assignUser', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser', 'verb' => 'PUT'],
['name' => 'card_api#unassignUser', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/unassignUser', 'verb' => 'PUT'],
['name' => 'card_api#reorder', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder', 'verb' => 'PUT'],
['name' => 'card_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'DELETE'],
['name' => 'card_api#findAllWithDue', 'url' => '/api/v1.0/dashboard/due', 'verb' => 'GET'],
['name' => 'card_api#findAllWithDue', 'url' => '/api/v{apiVersion}/dashboard/due', 'verb' => 'GET'],
['name' => 'label_api#get', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'GET'],
['name' => 'label_api#create', 'url' => '/api/v1.0/boards/{boardId}/labels', 'verb' => 'POST'],
['name' => 'label_api#update', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'PUT'],
['name' => 'label_api#delete', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'DELETE'],
['name' => 'label_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels/{labelId}', 'verb' => 'GET'],
['name' => 'label_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels', 'verb' => 'POST'],
['name' => 'label_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels/{labelId}', 'verb' => 'PUT'],
['name' => 'label_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels/{labelId}', 'verb' => 'DELETE'],
['name' => 'attachment_api#getAll', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'GET'],
['name' => 'attachment_api#display', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'GET'],
['name' => 'attachment_api#create', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'POST'],
['name' => 'attachment_api#update', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'PUT'],
['name' => 'attachment_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'DELETE'],
['name' => 'attachment_api#restore', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'],
['name' => 'attachment_api#getAll', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'GET', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#display', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'GET', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'POST', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'PUT', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#restore', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'board_api#preflighted_cors', 'url' => '/api/v1.0/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
['name' => 'attachment_api_v11#getAll', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'GET', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'attachment_api_v11#display', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{type}/{attachmentId}', 'verb' => 'GET', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'attachment_api_v11#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'POST', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'attachment_api_v11#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{type}/{attachmentId}', 'verb' => 'PUT', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'attachment_api_v11#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{type}/{attachmentId}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'attachment_api_v11#restore', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{type}/{attachmentId}/restore', 'verb' => 'PUT', 'requirements' => ['apiVersion' => '1.1']],
['name' => 'board_api#preflighted_cors', 'url' => '/api/v{apiVersion}/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
],
'ocs' => [
['name' => 'Config#get', 'url' => '/api/v1.0/config', 'verb' => 'GET'],
['name' => 'Config#setValue', 'url' => '/api/v1.0/config/{key}', 'verb' => 'POST'],
['name' => 'Config#get', 'url' => '/api/v{apiVersion}/config', 'verb' => 'GET'],
['name' => 'Config#setValue', 'url' => '/api/v{apiVersion}/config/{key}', 'verb' => 'POST'],
['name' => 'comments_api#list', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'GET'],
['name' => 'comments_api#create', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'POST'],
['name' => 'comments_api#update', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
['name' => 'comments_api#delete', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
['name' => 'comments_api#list', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments', 'verb' => 'GET'],
['name' => 'comments_api#create', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments', 'verb' => 'POST'],
['name' => 'comments_api#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
['name' => 'comments_api#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
['name' => 'overview_api#upcomingCards', 'url' => '/api/v1.0/overview/upcoming', 'verb' => 'GET'],
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
]
];

View File

@@ -2,6 +2,7 @@
"name": "nextcloud/deck",
"type": "project",
"license": "AGPLv3",
"minimum-stability": "dev",
"authors": [
{
"name": "Julius Härtl",
@@ -13,7 +14,7 @@
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^20",
"christophwurst/nextcloud": "dev-master",
"phpunit/phpunit": "^8",
"nextcloud/coding-standard": "^0.4.0",
"symfony/event-dispatcher": "^4.0",

24
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4a3d65807490679a4de897a8643385bb",
"content-hash": "3172d27bd19b2a125db3197c495deda9",
"packages": [
{
"name": "cogpowered/finediff",
@@ -225,25 +225,26 @@
},
{
"name": "christophwurst/nextcloud",
"version": "v20.0.4",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/ChristophWurst/nextcloud_composer.git",
"reference": "a207b55848d1ac4c83a954eac90c07714bbdaaed"
"reference": "0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/a207b55848d1ac4c83a954eac90c07714bbdaaed",
"reference": "a207b55848d1ac4c83a954eac90c07714bbdaaed",
"url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd",
"reference": "0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd",
"shasum": ""
},
"require": {
"php": "^7.2"
"php": "^7.3 || ~8.0.0"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "20.0.0-dev"
"dev-master": "21.0.0-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -259,9 +260,9 @@
"description": "Composer package containing Nextcloud's public API (classes, interfaces)",
"support": {
"issues": "https://github.com/ChristophWurst/nextcloud_composer/issues",
"source": "https://github.com/ChristophWurst/nextcloud_composer/tree/v20.0.4"
"source": "https://github.com/ChristophWurst/nextcloud_composer/tree/master"
},
"time": "2020-12-23T12:42:07+00:00"
"time": "2020-12-23T22:55:20+00:00"
},
{
"name": "composer/package-versions-deprecated",
@@ -4893,9 +4894,10 @@
}
],
"aliases": [],
"minimum-stability": "stable",
"minimum-stability": "dev",
"stability-flags": {
"roave/security-advisories": 20
"roave/security-advisories": 20,
"christophwurst/nextcloud": 20
},
"prefer-stable": false,
"prefer-lowest": false,

View File

@@ -26,7 +26,6 @@
namespace OCA\Deck\Activity;
use InvalidArgumentException;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Assignment;
@@ -45,8 +44,8 @@ use OCP\Activity\IManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Comments\IComment;
use OCP\IL10N;
use OCP\IUser;
use OCP\L10N\IFactory;
class ActivityManager {
public const DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED = 'DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED';
@@ -58,7 +57,7 @@ class ActivityManager {
private $attachmentMapper;
private $aclMapper;
private $stackMapper;
private $l10nFactory;
private $l10n;
public const DECK_OBJECT_BOARD = 'deck_board';
public const DECK_OBJECT_CARD = 'deck_card';
@@ -112,7 +111,7 @@ class ActivityManager {
StackMapper $stackMapper,
AttachmentMapper $attachmentMapper,
AclMapper $aclMapper,
IFactory $l10nFactory,
IL10N $l10n,
$userId
) {
$this->manager = $manager;
@@ -122,119 +121,117 @@ class ActivityManager {
$this->stackMapper = $stackMapper;
$this->attachmentMapper = $attachmentMapper;
$this->aclMapper = $aclMapper;
$this->l10nFactory = $l10nFactory;
$this->l10n = $l10n;
$this->userId = $userId;
}
/**
* @param string $subjectIdentifier
* @param $subjectIdentifier
* @param array $subjectParams
* @param bool $ownActivity
* @return string
*/
public function getActivityFormat($language, $subjectIdentifier, $subjectParams = [], $ownActivity = false) {
public function getActivityFormat($subjectIdentifier, $subjectParams = [], $ownActivity = false) {
$subject = '';
$l = $this->l10nFactory->get(Application::APP_ID, $language);
switch ($subjectIdentifier) {
case self::SUBJECT_BOARD_CREATE:
$subject = $ownActivity ? $l->t('You have created a new board {board}'): $l->t('{user} has created a new board {board}');
$subject = $ownActivity ? $this->l10n->t('You have created a new board {board}'): $this->l10n->t('{user} has created a new board {board}');
break;
case self::SUBJECT_BOARD_DELETE:
$subject = $ownActivity ? $l->t('You have deleted the board {board}') : $l->t('{user} has deleted the board {board}');
$subject = $ownActivity ? $this->l10n->t('You have deleted the board {board}') : $this->l10n->t('{user} has deleted the board {board}');
break;
case self::SUBJECT_BOARD_RESTORE:
$subject = $ownActivity ? $l->t('You have restored the board {board}') : $l->t('{user} has restored the board {board}');
$subject = $ownActivity ? $this->l10n->t('You have restored the board {board}') : $this->l10n->t('{user} has restored the board {board}');
break;
case self::SUBJECT_BOARD_SHARE:
$subject = $ownActivity ? $l->t('You have shared the board {board} with {acl}') : $l->t('{user} has shared the board {board} with {acl}');
$subject = $ownActivity ? $this->l10n->t('You have shared the board {board} with {acl}') : $this->l10n->t('{user} has shared the board {board} with {acl}');
break;
case self::SUBJECT_BOARD_UNSHARE:
$subject = $ownActivity ? $l->t('You have removed {acl} from the board {board}') : $l->t('{user} has removed {acl} from the board {board}');
$subject = $ownActivity ? $this->l10n->t('You have removed {acl} from the board {board}') : $this->l10n->t('{user} has removed {acl} from the board {board}');
break;
case self::SUBJECT_BOARD_UPDATE_TITLE:
$subject = $ownActivity ? $l->t('You have renamed the board {before} to {board}') : $l->t('{user} has renamed the board {before} to {board}');
$subject = $ownActivity ? $this->l10n->t('You have renamed the board {before} to {board}') : $this->l10n->t('{user} has renamed the board {before} to {board}');
break;
case self::SUBJECT_BOARD_UPDATE_ARCHIVED:
if (isset($subjectParams['after']) && $subjectParams['after']) {
$subject = $ownActivity ? $l->t('You have archived the board {board}') : $l->t('{user} has archived the board {before}');
$subject = $ownActivity ? $this->l10n->t('You have archived the board {board}') : $this->l10n->t('{user} has archived the board {before}');
} else {
$subject = $ownActivity ? $l->t('You have unarchived the board {board}') : $l->t('{user} has unarchived the board {before}');
$subject = $ownActivity ? $this->l10n->t('You have unarchived the board {board}') : $this->l10n->t('{user} has unarchived the board {before}');
}
break;
case self::SUBJECT_STACK_CREATE:
$subject = $ownActivity ? $l->t('You have created a new list {stack} on board {board}') : $l->t('{user} has created a new list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have created a new list {stack} on board {board}') : $this->l10n->t('{user} has created a new list {stack} on board {board}');
break;
case self::SUBJECT_STACK_UPDATE:
$subject = $ownActivity ? $l->t('You have created a new list {stack} on board {board}') : $l->t('{user} has created a new list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have created a new list {stack} on board {board}') : $this->l10n->t('{user} has created a new list {stack} on board {board}');
break;
case self::SUBJECT_STACK_UPDATE_TITLE:
$subject = $ownActivity ? $l->t('You have renamed list {before} to {stack} on board {board}') : $l->t('{user} has renamed list {before} to {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have renamed list {before} to {stack} on board {board}') : $this->l10n->t('{user} has renamed list {before} to {stack} on board {board}');
break;
case self::SUBJECT_STACK_DELETE:
$subject = $ownActivity ? $l->t('You have deleted list {stack} on board {board}') : $l->t('{user} has deleted list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have deleted list {stack} on board {board}') : $this->l10n->t('{user} has deleted list {stack} on board {board}');
break;
case self::SUBJECT_CARD_CREATE:
$subject = $ownActivity ? $l->t('You have created card {card} in list {stack} on board {board}') : $l->t('{user} has created card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have created card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has created card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_CARD_DELETE:
$subject = $ownActivity ? $l->t('You have deleted card {card} in list {stack} on board {board}') : $l->t('{user} has deleted card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have deleted card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has deleted card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_CARD_UPDATE_TITLE:
$subject = $ownActivity ? $l->t('You have renamed the card {before} to {card}') : $l->t('{user} has renamed the card {before} to {card}');
$subject = $ownActivity ? $this->l10n->t('You have renamed the card {before} to {card}') : $this->l10n->t('{user} has renamed the card {before} to {card}');
break;
case self::SUBJECT_CARD_UPDATE_DESCRIPTION:
if (!isset($subjectParams['before'])) {
$subject = $ownActivity ? $l->t('You have added a description to card {card} in list {stack} on board {board}') : $l->t('{user} has added a description to card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have added a description to card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has added a description to card {card} in list {stack} on board {board}');
} else {
$subject = $ownActivity ? $l->t('You have updated the description of card {card} in list {stack} on board {board}') : $l->t('{user} has updated the description of the card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have updated the description of card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has updated the description of the card {card} in list {stack} on board {board}');
}
break;
case self::SUBJECT_CARD_UPDATE_ARCHIVE:
$subject = $ownActivity ? $l->t('You have archived card {card} in list {stack} on board {board}') : $l->t('{user} has archived card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have archived card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has archived card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_CARD_UPDATE_UNARCHIVE:
$subject = $ownActivity ? $l->t('You have unarchived card {card} in list {stack} on board {board}') : $l->t('{user} has unarchived card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have unarchived card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has unarchived card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_CARD_UPDATE_DUEDATE:
if (!isset($subjectParams['after'])) {
$subject = $ownActivity ? $l->t('You have removed the due date of card {card}') : $l->t('{user} has removed the due date of card {card}');
$subject = $ownActivity ? $this->l10n->t('You have removed the due date of card {card}') : $this->l10n->t('{user} has removed the due date of card {card}');
} elseif (!isset($subjectParams['before']) && isset($subjectParams['after'])) {
$subject = $ownActivity ? $l->t('You have set the due date of card {card} to {after}') : $l->t('{user} has set the due date of card {card} to {after}');
$subject = $ownActivity ? $this->l10n->t('You have set the due date of card {card} to {after}') : $this->l10n->t('{user} has set the due date of card {card} to {after}');
} else {
$subject = $ownActivity ? $l->t('You have updated the due date of card {card} to {after}') : $l->t('{user} has updated the due date of card {card} to {after}');
$subject = $ownActivity ? $this->l10n->t('You have updated the due date of card {card} to {after}') : $this->l10n->t('{user} has updated the due date of card {card} to {after}');
}
break;
case self::SUBJECT_LABEL_ASSIGN:
$subject = $ownActivity ? $l->t('You have added the tag {label} to card {card} in list {stack} on board {board}') : $l->t('{user} has added the tag {label} to card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have added the tag {label} to card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has added the tag {label} to card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_LABEL_UNASSING:
$subject = $ownActivity ? $l->t('You have removed the tag {label} from card {card} in list {stack} on board {board}') : $l->t('{user} has removed the tag {label} from card {card} in list {stack} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have removed the tag {label} from card {card} in list {stack} on board {board}') : $this->l10n->t('{user} has removed the tag {label} from card {card} in list {stack} on board {board}');
break;
case self::SUBJECT_CARD_USER_ASSIGN:
$subject = $ownActivity ? $l->t('You have assigned {assigneduser} to card {card} on board {board}') : $l->t('{user} has assigned {assigneduser} to card {card} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have assigned {assigneduser} to card {card} on board {board}') : $this->l10n->t('{user} has assigned {assigneduser} to card {card} on board {board}');
break;
case self::SUBJECT_CARD_USER_UNASSIGN:
$subject = $ownActivity ? $l->t('You have unassigned {assigneduser} from card {card} on board {board}') : $l->t('{user} has unassigned {assigneduser} from card {card} on board {board}');
$subject = $ownActivity ? $this->l10n->t('You have unassigned {assigneduser} from card {card} on board {board}') : $this->l10n->t('{user} has unassigned {assigneduser} from card {card} on board {board}');
break;
case self::SUBJECT_CARD_UPDATE_STACKID:
$subject = $ownActivity ? $l->t('You have moved the card {card} from list {stackBefore} to {stack}') : $l->t('{user} has moved the card {card} from list {stackBefore} to {stack}');
$subject = $ownActivity ? $this->l10n->t('You have moved the card {card} from list {stackBefore} to {stack}') : $this->l10n->t('{user} has moved the card {card} from list {stackBefore} to {stack}');
break;
case self::SUBJECT_ATTACHMENT_CREATE:
$subject = $ownActivity ? $l->t('You have added the attachment {attachment} to card {card}') : $l->t('{user} has added the attachment {attachment} to card {card}');
$subject = $ownActivity ? $this->l10n->t('You have added the attachment {attachment} to card {card}') : $this->l10n->t('{user} has added the attachment {attachment} to card {card}');
break;
case self::SUBJECT_ATTACHMENT_UPDATE:
$subject = $ownActivity ? $l->t('You have updated the attachment {attachment} on card {card}') : $l->t('{user} has updated the attachment {attachment} on card {card}');
$subject = $ownActivity ? $this->l10n->t('You have updated the attachment {attachment} on card {card}') : $this->l10n->t('{user} has updated the attachment {attachment} on card {card}');
break;
case self::SUBJECT_ATTACHMENT_DELETE:
$subject = $ownActivity ? $l->t('You have deleted the attachment {attachment} from card {card}') : $l->t('{user} has deleted the attachment {attachment} from card {card}');
$subject = $ownActivity ? $this->l10n->t('You have deleted the attachment {attachment} from card {card}') : $this->l10n->t('{user} has deleted the attachment {attachment} from card {card}');
break;
case self::SUBJECT_ATTACHMENT_RESTORE:
$subject = $ownActivity ? $l->t('You have restored the attachment {attachment} to card {card}') : $l->t('{user} has restored the attachment {attachment} to card {card}');
$subject = $ownActivity ? $this->l10n->t('You have restored the attachment {attachment} to card {card}') : $this->l10n->t('{user} has restored the attachment {attachment} to card {card}');
break;
case self::SUBJECT_CARD_COMMENT_CREATE:
$subject = $ownActivity ? $l->t('You have commented on card {card}') : $l->t('{user} has commented on card {card}');
$subject = $ownActivity ? $this->l10n->t('You have commented on card {card}') : $this->l10n->t('{user} has commented on card {card}');
break;
default:
break;
@@ -383,7 +380,7 @@ class ActivityManager {
case self::SUBJECT_ATTACHMENT_UPDATE:
case self::SUBJECT_ATTACHMENT_DELETE:
case self::SUBJECT_ATTACHMENT_RESTORE:
$subjectParams = $this->findDetailsForAttachment($entity->getId());
$subjectParams = $this->findDetailsForAttachment($entity);
break;
case self::SUBJECT_BOARD_SHARE:
case self::SUBJECT_BOARD_UNSHARE:
@@ -405,15 +402,15 @@ class ActivityManager {
if ($subject === self::SUBJECT_CARD_UPDATE_STACKID) {
$subjectParams['stackBefore'] = $this->stackMapper->find($additionalParams['before']);
$subjectParams['stack'] = $this->stackMapper->find($additionalParams['after']);
unset($additionalParams['after'], $additionalParams['before']);
}
$subjectParams['author'] = $author === null ? $this->userId : $author;
$subjectParams['author'] = $this->userId;
$event = $this->manager->generateEvent();
$event->setApp('deck')
->setType($eventType)
->setAuthor($subjectParams['author'])
->setAuthor($author === null ? $this->userId : $author)
->setObject($objectType, (int)$object->getId(), $object->getTitle())
->setSubject($subject, array_merge($subjectParams, $additionalParams))
->setTimestamp(time());
@@ -530,8 +527,7 @@ class ActivityManager {
];
}
private function findDetailsForAttachment($attachmentId) {
$attachment = $this->attachmentMapper->find($attachmentId);
private function findDetailsForAttachment($attachment) {
$data = $this->findDetailsForCard($attachment->getCardId());
return array_merge($data, ['attachment' => $attachment]);
}

View File

@@ -143,14 +143,12 @@ class DeckProvider implements IProvider {
$params = $this->parseParamForLabel($subjectParams, $params);
$params = $this->parseParamForAssignedUser($subjectParams, $params);
$params = $this->parseParamForAcl($subjectParams, $params);
if ($subjectIdentifier !== ActivityManager::SUBJECT_CARD_UPDATE_STACKID) {
$params = $this->parseParamForChanges($subjectParams, $params, $event);
}
$params = $this->parseParamForChanges($subjectParams, $params, $event);
$params = $this->parseParamForComment($subjectParams, $params, $event);
$params = $this->parseParamForDuedate($subjectParams, $params, $event);
try {
$subject = $this->activityManager->getActivityFormat($language, $subjectIdentifier, $subjectParams, $ownActivity);
$subject = $this->activityManager->getActivityFormat($subjectIdentifier, $subjectParams, $ownActivity);
$this->setSubjects($event, $subject, $params);
} catch (\Exception $e) {
}

View File

@@ -43,6 +43,8 @@ use OCA\Deck\Notification\Notifier;
use OCA\Deck\Search\DeckProvider;
use OCA\Deck\Service\FullTextSearchService;
use OCA\Deck\Service\PermissionService;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\Sharing\Listener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -62,6 +64,7 @@ use OCP\IServerContainer;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
use OCP\Share\IManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
@@ -92,6 +95,16 @@ class Application20 extends App implements IBootstrap {
$context->injectFn(Closure::fromCallable([$this, 'registerNotifications']));
$context->injectFn(Closure::fromCallable([$this, 'registerFullTextSearch']));
$context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources']));
$context->injectFn(function (IManager $shareManager) {
if (method_exists($shareManager, 'registerShareProvider')) {
$shareManager->registerShareProvider(DeckShareProvider::class);
}
});
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
$listener->register($eventDispatcher);
});
}
public function register(IRegistrationContext $context): void {

View File

@@ -50,7 +50,11 @@ class Capabilities implements ICapability {
return [
'deck' => [
'version' => $this->appManager->getAppVersion('deck'),
'canCreateBoards' => $this->permissionService->canCreate()
'canCreateBoards' => $this->permissionService->canCreate(),
'apiVersions' => [
'1.0',
'1.1'
]
]
];
}

View File

@@ -42,8 +42,13 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function getAll() {
public function getAll($apiVersion) {
$attachment = $this->attachmentService->findAll($this->request->getParam('cardId'), true);
if ($apiVersion === '1.0') {
$attachment = array_filter($attachment, function ($attachment) {
return $attachment->getType() === 'deck_file';
});
}
return new DataResponse($attachment, HTTP::STATUS_OK);
}
@@ -53,8 +58,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function display() {
return $this->attachmentService->display($this->request->getParam('attachmentId'));
public function display($cardId, $attachmentId, $type = 'deck_file') {
return $this->attachmentService->display($cardId, $attachmentId, $type);
}
/**
@@ -63,8 +68,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function create($type, $data) {
$attachment = $this->attachmentService->create($this->request->getParam('cardId'), $type, $data);
public function create($cardId, $type, $data) {
$attachment = $this->attachmentService->create($cardId, $type, $data);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
@@ -74,8 +79,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function update($data) {
$attachment = $this->attachmentService->update($this->request->getParam('attachmentId'), $data);
public function update($cardId, $attachmentId, $data, $type = 'deck_file') {
$attachment = $this->attachmentService->update($cardId, $attachmentId, $data, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
@@ -85,8 +90,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function delete() {
$attachment = $this->attachmentService->delete($this->request->getParam('attachmentId'));
public function delete($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->delete($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
@@ -96,8 +101,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired
*
*/
public function restore() {
$attachment = $this->attachmentService->restore($this->request->getParam('attachmentId'));
public function restore($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->restore($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (c) 2018 Ryan Fletcher <ryan.fletcher@codepassion.ca>
*
* @author Ryan Fletcher <ryan.fletcher@codepassion.ca>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Controller;
class AttachmentApiV11Controller extends AttachmentApiController {
}

View File

@@ -52,8 +52,13 @@ class AttachmentController extends Controller {
* @return \OCP\AppFramework\Http\Response
* @throws \OCA\Deck\NotFoundException
*/
public function display($attachmentId) {
return $this->attachmentService->display($attachmentId);
public function display($cardId, $attachmentId) {
if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->display($cardId, $attachmentId, $type);
}
/**
@@ -70,21 +75,36 @@ class AttachmentController extends Controller {
/**
* @NoAdminRequired
*/
public function update($attachmentId) {
return $this->attachmentService->update($attachmentId, $this->request->getParam('data'));
public function update($cardId, $attachmentId) {
if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data'), $type);
}
/**
* @NoAdminRequired
*/
public function delete($attachmentId) {
return $this->attachmentService->delete($attachmentId);
public function delete($cardId, $attachmentId) {
if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->delete($cardId, $attachmentId, $type);
}
/**
* @NoAdminRequired
*/
public function restore($attachmentId) {
return $this->attachmentService->restore($attachmentId);
public function restore($cardId, $attachmentId) {
if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->restore($cardId, $attachmentId, $type);
}
}

View File

@@ -26,7 +26,10 @@ namespace OCA\Deck\Controller;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCA\Files\Event\LoadSidebar;
use OCA\Viewer\Event\LoadViewer;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
@@ -34,23 +37,24 @@ use OCP\AppFramework\Controller;
class PageController extends Controller {
private $permissionService;
private $userId;
private $l10n;
private $initialState;
private $configService;
private $eventDispatcher;
public function __construct(
$AppName,
IRequest $request,
PermissionService $permissionService,
IInitialStateService $initialStateService,
ConfigService $configService
ConfigService $configService,
IEventDispatcher $eventDispatcher
) {
parent::__construct($AppName, $request);
$this->permissionService = $permissionService;
$this->initialState = $initialStateService;
$this->configService = $configService;
$this->eventDispatcher = $eventDispatcher;
}
/**
@@ -65,6 +69,11 @@ class PageController extends Controller {
$this->initialState->provideInitialState(Application::APP_ID, 'canCreate', $this->permissionService->canCreate());
$this->initialState->provideInitialState(Application::APP_ID, 'config', $this->configService->getAll());
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
}
$response = new TemplateResponse('deck', 'main');
if (\OC::$server->getConfig()->getSystemValueBool('debug', false)) {

View File

@@ -85,6 +85,16 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
return $board;
}
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups,null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
}
/**
* Find all boards for a given user
*
@@ -159,7 +169,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
$circles = array_map(function ($circle) {
return $circle->getUniqueId();
}, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true));
if (count($circles) === 0) {
return [];
}

View File

@@ -149,7 +149,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function queryCardsByBoards(array $boardIds): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
$qb->select('c.*', 's.board_id')
->selectAlias('s.title', 'stack_title')
->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id'))
->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)));
@@ -279,6 +280,27 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds);
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')),
$qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))
)
);
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$result = $qb->execute();
$all = $result->fetchAll();
$result->closeCursor();
return $all;
}
public function delete(Entity $entity): Entity {
// delete assigned labels
$this->labelMapper->deleteLabelAssignmentsForCard($entity->getId());

View File

@@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Migration;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\Migration\IRepairStep;
use OCP\Migration\IOutput;
class UnknownUsers implements IRepairStep {
private $userManager;
private $groupManager;
private $aclMapper;
private $boardMapper;
public function __construct(IUserManager $userManager, IGroupManager $groupManager, AclMapper $aclMapper, BoardMapper $boardMapper) {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->aclMapper = $aclMapper;
$this->boardMapper = $boardMapper;
}
/*
* @inheritdoc
*/
public function getName() {
return 'Delete orphaned ACL rules';
}
/**
* @inheritdoc
*/
public function run(IOutput $output) {
$boards = $this->boardMapper->findAll();
/** @var Board $board */
foreach ($boards as $board) {
$acls = $this->aclMapper->findAll($board->getId());
/** @var Acl $acl */
foreach ($acls as $acl) {
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$user = $this->userManager->get($acl->getParticipant());
if ($user === null) {
$this->aclMapper->delete($acl);
}
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
$group = $this->groupManager->get($acl->getParticipant());
if ($group === null) {
$this->aclMapper->delete($acl);
}
}
}
}
}
}

View File

@@ -58,18 +58,6 @@ class AttachmentService {
/** @var ChangeHelper */
private $changeHelper;
/**
* AttachmentService constructor.
*
* @param AttachmentMapper $attachmentMapper
* @param CardMapper $cardMapper
* @param PermissionService $permissionService
* @param Application $application
* @param ICacheFactory $cacheFactory
* @param $userId
* @param IL10N $l10n
* @throws \OCP\AppFramework\QueryException
*/
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ChangeHelper $changeHelper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n, ActivityManager $activityManager) {
$this->attachmentMapper = $attachmentMapper;
$this->cardMapper = $cardMapper;
@@ -84,6 +72,7 @@ class AttachmentService {
// Register shipped attachment services
// TODO: move this to a plugin based approach once we have different types of attachments
$this->registerAttachmentService('deck_file', FileService::class);
$this->registerAttachmentService('file', FilesAppService::class);
}
/**
@@ -124,6 +113,15 @@ class AttachmentService {
if ($withDeleted) {
$attachments = array_merge($attachments, $this->attachmentMapper->findToDelete($cardId, false));
}
foreach (array_keys($this->services) as $attachmentType) {
/** @var IAttachmentService $service */
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
$attachments = array_merge($attachments, $service->listAttachments((int)$cardId));
}
}
foreach ($attachments as &$attachment) {
try {
$service = $this->getService($attachment->getType());
@@ -132,6 +130,7 @@ class AttachmentService {
// Ingore invalid attachment types when extending the data
}
}
return $attachments;
}
@@ -148,8 +147,17 @@ class AttachmentService {
$count = $this->cache->get('card-' . $cardId);
if (!$count) {
$count = count($this->attachmentMapper->findAll($cardId));
foreach (array_keys($this->services) as $attachmentType) {
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
$count += $service->getAttachmentCount((int)$cardId);
}
}
$this->cache->set('card-' . $cardId, $count);
}
return $count;
}
@@ -189,21 +197,20 @@ class AttachmentService {
try {
$service = $this->getService($attachment->getType());
$service->create($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
if ($attachment->getData() === null) {
throw new StatusException($this->l10n->t('No data was provided to create an attachment.'));
}
$attachment = $this->attachmentMapper->insert($attachment);
// extend data so the frontend can use it properly after creating
try {
$service = $this->getService($attachment->getType());
if (!$service instanceof ICustomAttachmentService) {
if ($attachment->getData() === null) {
throw new StatusException($this->l10n->t('No data was provided to create an attachment.'));
}
$attachment = $this->attachmentMapper->insert($attachment);
}
$service->extendData($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_CREATE);
return $attachment;
@@ -215,46 +222,69 @@ class AttachmentService {
*
* @param $attachmentId
* @return Response
* @throws BadRequestException
* @throws NoPermissionException
* @throws NotFoundException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function display($attachmentId) {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
}
public function display($cardId, $attachmentId, $type = 'deck_file') {
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {
throw new NoPermissionException('Permission denied');
}
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_READ);
try {
$service = $this->getService($attachment->getType());
return $service->display($attachment);
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
throw new NotFoundException();
}
if (!$service instanceof ICustomAttachmentService) {
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {
throw new NoPermissionException('Permission denied');
}
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_READ);
try {
$service = $this->getService($attachment->getType());
} catch (InvalidAttachmentType $e) {
throw new NotFoundException();
}
} else {
$attachment = new Attachment();
$attachment->setId($attachmentId);
$attachment->setType($type);
$attachment->setCardId($cardId);
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_READ);
}
return $service->display($attachment);
}
/**
* Update an attachment with custom data
*
* @param $attachmentId
* @param $request
* @param $data
* @return mixed
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
* @throws NoPermissionException
*/
public function update($attachmentId, $data) {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
public function update($cardId, $attachmentId, $data, $type = 'deck_file') {
try {
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
throw new NotFoundException();
}
if ($service instanceof ICustomAttachmentService) {
try {
$attachment = new Attachment();
$attachment->setId($attachmentId);
$attachment->setType($type);
$attachment->setData($data);
$attachment->setCardId($cardId);
$service->update($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
return $attachment;
} catch (\Exception $e) {
throw new NotFoundException();
}
}
if ($data === false || $data === null) {
@@ -279,12 +309,8 @@ class AttachmentService {
$attachment->setLastModified(time());
$this->attachmentMapper->update($attachment);
// extend data so the frontend can use it properly after creating
try {
$service = $this->getService($attachment->getType());
$service->extendData($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
$service->extendData($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_UPDATE);
return $attachment;
@@ -301,9 +327,23 @@ class AttachmentService {
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete($attachmentId) {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
public function delete($cardId, $attachmentId, $type = 'deck_file') {
try {
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
throw new NotFoundException();
}
if ($service instanceof ICustomAttachmentService) {
$attachment = new Attachment();
$attachment->setId($attachmentId);
$attachment->setType($type);
$attachment->setCardId($cardId);
$service->extendData($attachment);
$service->delete($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
try {
@@ -315,25 +355,21 @@ class AttachmentService {
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $attachment->getCardId());
try {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
$this->changeHelper->cardChanged($attachment->getCardId());
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment);
} catch (InvalidAttachmentType $e) {
// just delete without further action
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
$this->changeHelper->cardChanged($attachment->getCardId());
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment);
$attachment = $this->attachmentMapper->delete($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
public function restore($attachmentId) {
public function restore($cardId, $attachmentId, $type = 'deck_file') {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
}

View File

@@ -113,13 +113,13 @@ class BoardService {
$this->userId = $userId;
}
public function getUserBoards(int $since = -1, $includeArchived = true): array {
$userInfo = $this->getBoardPrerequisites();
$userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null, $since, $includeArchived);
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null, $since, $includeArchived);
$circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null, $since, $includeArchived);
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
/**
* Get all boards that are shared with a user, their groups or circles
*/
public function getUserBoards(int $since = -1, bool $includeArchived = true): array {
return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived);
}
/**
* @return array
*/
@@ -324,7 +324,7 @@ class BoardService {
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $new_board, ActivityManager::SUBJECT_BOARD_CREATE);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $new_board, ActivityManager::SUBJECT_BOARD_CREATE, [], $userId);
$this->changeHelper->boardChanged($new_board->getId());
$this->eventDispatcher->dispatch(

View File

@@ -29,6 +29,7 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl;
@@ -116,9 +117,20 @@ class CardService {
return $cards;
}
public function search($boardIds, $term) {
$cards = $this->cardMapper->search($boardIds, $term);
return $cards;
public function search(string $term, int $limit = null, int $offset = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
return $this->cardMapper->search($boardIds, $term, $limit, $offset);
}
public function searchRaw(string $term, int $limit = null, int $offset = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
return $this->cardMapper->searchRaw($boardIds, $term, $limit, $offset);
}
/**
@@ -138,6 +150,11 @@ class CardService {
$card = $this->cardMapper->find($cardId);
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
$attachments = $this->attachmentService->findAll($cardId, true);
if (\OC::$server->getRequest()->getParam('apiVersion') === '1.0') {
$attachments = array_filter($attachments, function ($attachment) {
return $attachment->getType() === 'deck_file';
});
}
$card->setAssignedUsers($assignedUsers);
$card->setAttachments($attachments);
$this->enrich($card);

View File

@@ -32,6 +32,7 @@ use OCA\Deck\NoPermissionException;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUserSession;
class ConfigService {
public const SETTING_BOARD_NOTIFICATION_DUE_OFF = 'off';
@@ -46,9 +47,10 @@ class ConfigService {
public function __construct(
IConfig $config,
IGroupManager $groupManager,
$userId
IUserSession $userSession
) {
$this->userId = $userId;
// Session is required here in order to make the tests properly inject the userId later on
$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
$this->groupManager = $groupManager;
$this->config = $config;
}
@@ -148,4 +150,8 @@ class ConfigService {
}, $groups);
return array_filter($groups);
}
public function getAttachmentFolder(): string {
return $this->config->getUserValue($this->userId, 'deck', 'attachment_folder', '/Deck');
}
}

View File

@@ -27,9 +27,10 @@ use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\StatusException;
use OCA\Deck\Exceptions\ConflictException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
@@ -48,7 +49,6 @@ class FileService implements IAttachmentService {
private $rootFolder;
private $config;
private $attachmentMapper;
private $mimeTypeDetector;
public function __construct(
IL10N $l10n,
@@ -57,8 +57,7 @@ class FileService implements IAttachmentService {
ILogger $logger,
IRootFolder $rootFolder,
IConfig $config,
AttachmentMapper $attachmentMapper,
IMimeTypeDetector $mimeTypeDetector
AttachmentMapper $attachmentMapper
) {
$this->l10n = $l10n;
$this->appData = $appData;
@@ -67,7 +66,6 @@ class FileService implements IAttachmentService {
$this->rootFolder = $rootFolder;
$this->config = $config;
$this->attachmentMapper = $attachmentMapper;
$this->mimeTypeDetector = $mimeTypeDetector;
}
/**
@@ -227,14 +225,27 @@ class FileService implements IAttachmentService {
/**
* @param Attachment $attachment
* @return StreamResponse
* @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse
* @throws \Exception
*/
public function display(Attachment $attachment) {
$file = $this->getFileFromRootFolder($attachment);
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
return $response;
}

View File

@@ -0,0 +1,269 @@
<?php
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Constants;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Share;
use OCP\Share\IManager;
use OCP\Share\IShare;
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $request;
private $rootFolder;
private $shareProvider;
private $shareManager;
private $userId;
private $configService;
private $l10n;
private $preview;
private $permissionService;
public function __construct(
IRequest $request,
IL10N $l10n,
IRootFolder $rootFolder,
IManager $shareManager,
ConfigService $configService,
DeckShareProvider $shareProvider,
IPreview $preview,
PermissionService $permissionService,
string $userId = null
) {
$this->request = $request;
$this->l10n = $l10n;
$this->rootFolder = $rootFolder;
$this->configService = $configService;
$this->shareProvider = $shareProvider;
$this->shareManager = $shareManager;
$this->userId = $userId;
$this->preview = $preview;
}
public function listAttachments(int $cardId): array {
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
$shares = array_filter($shares, function ($share) {
return $share->getPermissions() > 0;
});
return array_map(function (IShare $share) use ($cardId) {
$file = $share->getNode();
$attachment = new Attachment();
$attachment->setType('file');
$attachment->setId((int)$share->getId());
$attachment->setCardId($cardId);
$attachment->setCreatedBy($share->getSharedBy());
$attachment->setData($file->getName());
$attachment->setLastModified($file->getMTime());
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
$attachment->setDeletedAt(0);
return $attachment;
}, $shares);
}
public function getAttachmentCount(int $cardId): int {
/** @var IDBConnection $qb */
$db = \OC::$server->getDatabaseConnection();
$qb = $db->getQueryBuilder();
$qb->select('s.id', 'f.fileid', 'f.path')
->selectAlias('st.id', 'storage_string_id')
->from('share', 's')
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK)))
->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($cardId)))
->andWhere($qb->expr()->isNull('s.parent'))
->andWhere($qb->expr()->orX(
$qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')),
$qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder'))
));
$count = 0;
$cursor = $qb->execute();
while ($data = $cursor->fetch()) {
if ($this->shareProvider->isAccessibleResult($data)) {
$count++;
}
}
$cursor->closeCursor();
return $count;
}
public function extendData(Attachment $attachment) {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$share = $this->shareProvider->getShareById($attachment->getId());
$file = $share->getNode();
$attachment->setExtendedData([
'path' => $userFolder->getRelativePath($file->getPath()),
'fileid' => $file->getId(),
'data' => $file->getName(),
'filesize' => $file->getSize(),
'mimetype' => $file->getMimeType(),
'info' => pathinfo($file->getName()),
'hasPreview' => $this->preview->isAvailable($file),
'permissions' => $share->getPermissions(),
]);
return $attachment;
}
public function display(Attachment $attachment) {
try {
$share = $this->shareProvider->getShareById($attachment->getId());
} catch (Share\Exceptions\ShareNotFound $e) {
throw new NotFoundException('File not found');
}
$file = $share->getNode();
if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) {
throw new NotFoundException('File not found');
}
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
return $response;
}
public function create(Attachment $attachment) {
$file = $this->getUploadedFile();
$fileName = $file['name'];
$userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
$folder = $userFolder->get($this->configService->getAttachmentFolder());
} catch (NotFoundException $e) {
$folder = $userFolder->newFolder($this->configService->getAttachmentFolder());
}
$fileName = $folder->getNonExistingName($fileName);
$target = $folder->newFile($fileName);
$content = fopen($file['tmp_name'], 'rb');
if ($content === false) {
throw new StatusException('Could not read file');
}
$target->putContent($content);
fclose($content);
$share = $this->shareManager->newShare();
$share->setNode($target);
$share->setShareType(ISHARE::TYPE_DECK);
$share->setSharedWith((string)$attachment->getCardId());
$share->setPermissions(Constants::PERMISSION_READ);
$share->setSharedBy($this->userId);
$share = $this->shareManager->createShare($share);
$attachment->setId((int)$share->getId());
$attachment->setData($target->getName());
return $attachment;
}
/**
* @return array
* @throws StatusException
*/
private function getUploadedFile() {
$file = $this->request->getUploadedFile('file');
$error = null;
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];
if (empty($file)) {
$error = $this->l10n->t('No file uploaded or file size exceeds maximum of %s', [\OCP\Util::humanFileSize(\OCP\Util::uploadLimit())]);
}
if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
$error = $phpFileUploadErrors[$file['error']];
}
if ($error !== null) {
throw new StatusException($error);
}
return $file;
}
public function update(Attachment $attachment) {
$share = $this->shareProvider->getShareById($attachment->getId());
$target = $share->getNode();
$file = $this->getUploadedFile();
$fileName = $file['name'];
$attachment->setData($fileName);
$content = fopen($file['tmp_name'], 'rb');
if ($content === false) {
throw new StatusException('Could not read file');
}
$target->putContent($content);
fclose($content);
$attachment->setLastModified(time());
return $attachment;
}
public function delete(Attachment $attachment) {
$share = $this->shareProvider->getShareById($attachment->getId());
$file = $share->getNode();
$attachment->setData($file->getName());
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) {
$file->delete();
return;
}
$this->shareManager->deleteFromSelf($share, $this->userId);
}
public function allowUndo() {
return false;
}
public function markAsDeleted(Attachment $attachment) {
throw new \Exception('Not implemented');
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Service;
/**
* Interface to implement in case attachments are handled by a different backend than
* then oc_deck_attachments table, e.g. for file sharing. When this interface is used
* for implementing an attachment handler no backlink will be stored in the deck attachments
* table and it is up to the implementation to track attachment to card relation.
*/
interface ICustomAttachmentService {
public function listAttachments(int $cardId): array;
public function getAttachmentCount(int $cardId): int;
}

View File

@@ -142,7 +142,7 @@ class PermissionService {
}
if ($permission === Acl::PERMISSION_SHARE && $this->shareManager->sharingDisabledForUser($this->userId)) {
return false;
throw new NoPermissionException('Permission denied');
}
if ($this->userIsBoardOwner($boardId, $userId)) {

File diff suppressed because it is too large Load Diff

112
lib/Sharing/Listener.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Sharing;
use OC\Files\Filesystem;
use OCA\Deck\Service\ConfigService;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Share\Events\VerifyMountPointEvent;
use OCP\Share\IShare;
use Symfony\Component\EventDispatcher\GenericEvent;
class Listener {
/** @var ConfigService */
private $configService;
public function __construct(ConfigService $configService) {
$this->configService = $configService;
}
public function register(IEventDispatcher $dispatcher): void {
/**
* @psalm-suppress UndefinedClass
*/
$dispatcher->addListener('OCP\Share::preShare', [self::class, 'listenPreShare'], 1000);
$dispatcher->addListener(VerifyMountPointEvent::class, [self::class, 'listenVerifyMountPointEvent'], 1000);
}
public static function listenPreShare(GenericEvent $event): void {
/** @var self $listener */
$listener = \OC::$server->query(self::class);
$listener->overwriteShareTarget($event);
}
public static function listenVerifyMountPointEvent(VerifyMountPointEvent $event): void {
/** @var self $listener */
$listener = \OC::$server->query(self::class);
$listener->overwriteMountPoint($event);
}
public function overwriteShareTarget(GenericEvent $event): void {
/** @var IShare $share */
$share = $event->getSubject();
if ($share->getShareType() !== IShare::TYPE_DECK
&& $share->getShareType() !== DeckShareProvider::SHARE_TYPE_DECK_USER) {
return;
}
$target = DeckShareProvider::DECK_FOLDER_PLACEHOLDER . '/' . $share->getNode()->getName();
$target = Filesystem::normalizePath($target);
$share->setTarget($target);
}
public function overwriteMountPoint(VerifyMountPointEvent $event): void {
$share = $event->getShare();
$view = $event->getView();
if ($share->getShareType() !== IShare::TYPE_DECK
&& $share->getShareType() !== DeckShareProvider::SHARE_TYPE_DECK_USER) {
return;
}
if ($event->getParent() === DeckShareProvider::DECK_FOLDER_PLACEHOLDER) {
try {
$userId = $view->getOwner('/');
} catch (\Exception $e) {
// If we fail to get the owner of the view from the cache,
// e.g. because the user never logged in but a cron job runs
// We fallback to calculating the owner from the root of the view:
if (substr_count($view->getRoot(), '/') >= 2) {
// /37c09aa0-1b92-4cf6-8c66-86d8cac8c1d0/files
[, $userId, ] = explode('/', $view->getRoot(), 3);
} else {
// Something weird is going on, we can't fallback more
// so for now we don't overwrite the share path ¯\_(ツ)_/¯
return;
}
}
$parent = $this->configService->getAttachmentFolder();
$event->setParent($parent);
if (!$event->getView()->is_dir($parent)) {
$event->getView()->mkdir($parent);
}
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Sharing;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Share\IShare;
class ShareAPIHelper {
private $urlGenerator;
private $timeFactory;
private $cardMapper;
private $permissionService;
private $l10n;
public function __construct(IURLGenerator $urlGenerator, ITimeFactory $timeFactory, CardMapper $cardMapper, PermissionService $permissionService, IL10N $l10n) {
$this->urlGenerator = $urlGenerator;
$this->timeFactory = $timeFactory;
$this->cardMapper = $cardMapper;
$this->permissionService = $permissionService;
$this->l10n = $l10n;
}
public function formatShare(IShare $share): array {
$result = [];
$card = $this->cardMapper->find($share->getSharedWith());
$boardId = $this->cardMapper->findBoardId($card->getId());
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = $card->getTitle();
$result['share_with_link'] = $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $card->getId();
return $result;
}
public function createShare(IShare $share, string $shareWith, int $permissions, $expireDate) {
$share->setSharedWith($shareWith);
$share->setPermissions($permissions);
if ($expireDate !== '') {
try {
$expireDate = $this->parseDate($expireDate);
$share->setExpirationDate($expireDate);
} catch (\Exception $e) {
throw new OCSNotFoundException($this->l10n->t('Invalid date, date format must be YYYY-MM-DD'));
}
}
}
/**
* Make sure that the passed date is valid ISO 8601
* So YYYY-MM-DD
* If not throw an exception
*
* Copied from \OCA\Files_Sharing\Controller\ShareAPIController::parseDate.
*
* @param string $expireDate
* @return \DateTime
* @throws \Exception
*/
private function parseDate(string $expireDate): \DateTime {
try {
$date = $this->timeFactory->getDateTime($expireDate);
} catch (\Exception $e) {
throw new \Exception('Invalid date. Format must be YYYY-MM-DD');
}
$date->setTime(0, 0, 0);
return $date;
}
/**
* Returns whether the given user can access the given room share or not.
*
* A user can access a room share only if she is a participant of the room.
*
* @param IShare $share
* @param string $user
* @return bool
*/
public function canAccessShare(IShare $share, string $user): bool {
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_READ, $user);
} catch (NoPermissionException $e) {
return false;
}
return true;
}
}

View File

@@ -21,21 +21,37 @@
-->
<template>
<Modal :title="t('deck', 'Select the card to link to a project')" @close="close">
<Modal class="card-selector" @close="close">
<div id="modal-inner" :class="{ 'icon-loading': loading }">
<h3>{{ title }}</h3>
<Multiselect v-model="selectedBoard"
:placeholder="t('deck', 'Select a board')"
:options="boards"
:disabled="loading"
label="title"
@select="fetchCardsFromBoard" />
@select="fetchCardsFromBoard">
<template slot="singleLabel" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
<template slot="option" slot-scope="props">
<span>
<span :style="{ 'backgroundColor': '#' + props.option.color }" class="board-bullet" />
<span>{{ props.option.title }}</span>
</span>
</template>
</Multiselect>
<Multiselect v-model="selectedCard"
:placeholder="t('deck', 'Select a card')"
:options="cardsFromBoard"
:disabled="loading || selectedBoard === ''"
label="title" />
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="select">
{{ t('deck', 'Link to card') }}
{{ action }}
</button>
<button @click="close">
{{ t('deck', 'Cancel') }}
@@ -56,6 +72,16 @@ export default {
Modal,
Multiselect,
},
props: {
title: {
type: String,
default: t('deck', 'Select the card to link to a project'),
},
action: {
type: String,
default: t('deck', 'Link to card'),
},
},
data() {
return {
boards: [],
@@ -67,10 +93,7 @@ export default {
},
computed: {
isBoardAndStackChoosen() {
if (this.selectedBoard === '' || this.selectedCard === '') {
return false
}
return true
return !(this.selectedBoard === '' || this.selectedCard === '')
},
},
beforeMount() {
@@ -113,7 +136,12 @@ export default {
width: 90vw;
max-width: 400px;
padding: 20px;
height: 500px;
height: 200px;
}
.multiselect {
width: 100%;
margin-bottom: 10px;
}
ul {
@@ -129,10 +157,6 @@ export default {
background-color: var(--color-background-dark);
}
li.selected {
border: 1px solid var(--color-primary);
}
.board-bullet {
display: inline-block;
width: 12px;
@@ -142,12 +166,11 @@ export default {
cursor: pointer;
}
li > span,
.avatar {
vertical-align: middle;
}
button {
float: right;
}
.card-selector::v-deep .modal-container {
overflow: visible !important;
}
</style>

View File

@@ -10,7 +10,7 @@
:loading="isLoading || !!isSearching"
:disabled="isLoading"
track-by="multiselectKey"
:internal-search="false"
:internal-search="true"
@input="clickAddAcl"
@search-change="asyncFind">
<template #noOptions>
@@ -73,7 +73,6 @@ import { CollectionList } from 'nextcloud-vue-collections'
import { mapGetters, mapState } from 'vuex'
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'
import { debounce } from 'lodash'
export default {
name: 'SharingTabSidebar',
@@ -149,13 +148,18 @@ export default {
this.asyncFind('')
},
methods: {
debouncedFind: debounce(async function(query) {
this.isSearching = true
await this.$store.dispatch('loadSharees', query)
this.isSearching = false
}, 300),
async asyncFind(query) {
await this.debouncedFind(query)
// manual debounce to handle async searching more easily and have more control over the loading state
const timestamp = (new Date()).getTime()
if (!this.isSearching || timestamp > this.isSearching + 300) {
this.isSearching = timestamp
await this.$store.dispatch('loadSharees', query)
// only reset searching flag if the most recent search finished
if (this.isSearching === timestamp) {
this.isSearching = false
}
}
},
async clickAddAcl() {
this.addAclForAPI = {

View File

@@ -22,17 +22,22 @@
<template>
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
<button class="icon-upload" @click="clickAddNewAttachmment()">
{{ t('deck', 'Upload attachment') }}
</button>
<input ref="localAttachments"
<div class="button-group">
<button class="icon-upload" @click="uploadNewFile()">
{{ t('deck', 'Upload new files') }}
</button>
<button class="icon-folder" @click="shareFromFiles()">
{{ t('deck', 'Share from Files') }}
</button>
</div>
<input ref="filesAttachment"
type="file"
style="display: none;"
multiple
@change="handleUploadFile">
<ul class="attachment-list">
<li v-for="attachment in uploadQueue" :key="attachment.name" class="attachment">
<a class="fileicon" :style="mimetypeForAttachment('none')" />
<a class="fileicon" :style="mimetypeForAttachment()" />
<div class="details">
<a>
<div class="filename">
@@ -45,9 +50,11 @@
<li v-for="attachment in attachments"
:key="attachment.id"
class="attachment">
<a class="fileicon" :style="mimetypeForAttachment(attachment.extendedData.mimetype)" :href="attachmentUrl(attachment)" />
<a class="fileicon"
:style="mimetypeForAttachment(attachment)"
@click.prevent="showViewer(attachment)" />
<div class="details">
<a :href="attachmentUrl(attachment)" target="_blank">
<a @click.prevent="showViewer(attachment)">
<div class="filename">
<span class="basename">{{ attachment.data }}</span>
</div>
@@ -61,12 +68,18 @@
{{ t('deck', 'Add this attachment') }}
</ActionButton>
</Actions>
<Actions v-if="removable">
<ActionButton v-if="attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
{{ t('deck', 'Delete Attachment') }}
<Actions v-if="removable" :force-menu="true">
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
{{ t('deck', 'Show in files') }}
</ActionLink>
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Unshare file') }}
</ActionButton>
<ActionButton v-else icon="icon-history" @click="$emit('restoreAttachment', attachment)">
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
{{ t('deck', 'Delete Attachment') }}
</ActionButton>
<ActionButton v-else-if="!attachment.extendedData.fileid" icon="icon-history" @click="$emit('restoreAttachment', attachment)">
{{ t('deck', 'Restore Attachment') }}
</ActionButton>
</Actions>
@@ -76,21 +89,31 @@
</template>
<script>
import { Actions, ActionButton } from '@nextcloud/vue'
import axios from '@nextcloud/axios'
import { Actions, ActionButton, ActionLink } from '@nextcloud/vue'
import AttachmentDragAndDrop from '../AttachmentDragAndDrop'
import relativeDate from '../../mixins/relativeDate'
import { formatFileSize } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
import { mapState } from 'vuex'
import { loadState } from '@nextcloud/initial-state'
import attachmentUpload from '../../mixins/attachmentUpload'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
const maxUploadSizeState = loadState('deck', 'maxUploadSize')
const picker = getFilePickerBuilder(t('deck', 'File to share'))
.setMultiSelect(false)
.setModal(true)
.setType(1)
.allowDirectories()
.build()
export default {
name: 'AttachmentList',
components: {
Actions,
ActionButton,
ActionLink,
AttachmentDragAndDrop,
},
mixins: [relativeDate, attachmentUpload],
@@ -120,20 +143,29 @@ export default {
},
computed: {
attachments() {
return [...this.$store.getters.attachmentsByCard(this.cardId)].sort((a, b) => b.id - a.id)
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
},
mimetypeForAttachment() {
return (mimetype) => {
const url = OC.MimeType.getIconUrl(mimetype)
return (attachment) => {
if (!attachment) {
return {}
}
const url = attachment.extendedData.hasPreview ? this.attachmentPreview(attachment) : OC.MimeType.getIconUrl(attachment.extendedData.mimetype)
const styles = {
'background-image': `url("${url}")`,
}
return styles
}
},
attachmentPreview() {
return (attachment) => (attachment.extendedData.fileid ? generateUrl(`/core/preview?fileId=${attachment.extendedData.fileid}&x=64&y=64&a=true`) : null)
},
attachmentUrl() {
return (attachment) => generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
},
internalLink() {
return (attachment) => generateUrl('/f/' + attachment.extendedData.fileid)
},
formattedFileSize() {
return (filesize) => formatFileSize(filesize)
},
@@ -151,29 +183,78 @@ export default {
}
},
},
created() {
this.$store.dispatch('fetchAttachments', this.cardId)
watch: {
cardId: {
immediate: true,
handler() {
this.$store.dispatch('fetchAttachments', this.cardId)
},
},
},
methods: {
handleUploadFile(event) {
const files = event.target.files ?? []
for (const file of files) {
this.onLocalAttachmentSelected(file)
this.onLocalAttachmentSelected(file, 'file')
}
event.target.value = ''
},
uploadNewFile() {
this.$refs.filesAttachment.click()
},
shareFromFiles() {
picker.pick()
.then(async(path) => {
console.debug(`path ${path} selected for sharing`)
if (!path.startsWith('/')) {
throw new Error(t('files', 'Invalid path selected'))
}
axios.post(generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares', {
path,
shareType: 12,
shareWith: '' + this.cardId,
}).then(() => {
this.$store.dispatch('fetchAttachments', this.cardId)
})
})
},
unshareAttachment(attachment) {
this.$store.dispatch('unshareAttachment', attachment)
},
clickAddNewAttachmment() {
this.$refs.localAttachments.click()
},
showViewer(attachment) {
if (attachment.extendedData.fileid && window.OCA.Viewer.availableHandlers.map(handler => handler.mimes).flat().includes(attachment.extendedData.mimetype)) {
window.OCA.Viewer.open(attachment.extendedData.path)
return
}
if (attachment.extendedData.fileid) {
window.location = generateUrl('/f/' + attachment.extendedData.fileid)
return
}
window.location = generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
},
},
}
</script>
<style lang="scss" scoped>
.icon-upload {
padding-left: 35px;
background-position: 10px center;
.button-group {
display: flex;
.icon-upload, .icon-folder {
padding-left: 44px;
background-position: 16px center;
flex-grow: 1;
height: 44px;
margin-bottom: 12px;
text-align: left;
}
}
.attachment-list {

View File

@@ -140,7 +140,17 @@ export default {
}
},
attachmentUrl() {
return (attachment) => generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
return (attachment) => {
if (attachment.extendedData.fileid) {
return generateUrl('/f/' + attachment.extendedData.fileid)
}
return generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
}
},
attachmentPreview() {
return (attachment) => (attachment.extendedData.fileid
? generateUrl(`/core/preview?fileId=${attachment.extendedData.fileid}&x=600&y=600&a=true`)
: generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`))
},
formattedFileSize() {
return (filesize) => formatFileSize(filesize)
@@ -169,12 +179,15 @@ export default {
addAttachment(attachment) {
const descString = this.$refs.markdownEditor.easymde.value()
let embed = ''
if (attachment.extendedData.mimetype.includes('image')) {
if ((attachment.type === 'file' && attachment.extendedData.hasPreview) || attachment.extendedData.mimetype.includes('image')) {
embed = '!'
}
const attachmentString = embed + '[📎 ' + attachment.data + '](' + this.attachmentUrl(attachment) + ')'
this.$refs.markdownEditor.easymde.value(descString + '\n' + attachmentString)
const attachmentString = embed + '[📎 ' + attachment.data + '](' + this.attachmentPreview(attachment) + ')'
const newContent = descString + '\n' + attachmentString
this.$refs.markdownEditor.easymde.value(newContent)
this.description = newContent
this.modalShow = false
this.updateDescription()
},
clickedPreview(e) {
if (e.target.getAttribute('type') === 'checkbox') {

View File

@@ -24,9 +24,8 @@ import Vue from 'vue'
import BoardSelector from './BoardSelector'
import CardSelector from './CardSelector'
import './../css/collections.css'
import FileSharingPicker from './views/FileSharingPicker'
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
@@ -34,7 +33,15 @@ __webpack_public_path__ = OC.linkTo('deck', 'js/');
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC;
Vue.prototype.OC = OC
window.addEventListener('DOMContentLoaded', () => {
if (OCA.Sharing && OCA.Sharing.ShareSearch) {
OCA.Sharing.ShareSearch.addNewResult(FileSharingPicker)
} else {
console.error('OCA.Sharing.ShareSearch not ready')
}
});
((function(OCP) {

View File

@@ -32,7 +32,7 @@ export default {
}
},
methods: {
async onLocalAttachmentSelected(file) {
async onLocalAttachmentSelected(file, type) {
if (this.maxUploadSize > 0 && file.size > this.maxUploadSize) {
showError(
t('deck', 'Failed to upload {name}', { name: file.name }) + ' - '
@@ -45,7 +45,7 @@ export default {
this.$set(this.uploadQueue, file.name, { name: file.name, progress: 0 })
const bodyFormData = new FormData()
bodyFormData.append('cardId', this.cardId)
bodyFormData.append('type', 'deck_file')
bodyFormData.append('type', type)
bodyFormData.append('file', file)
await queue.add(async() => {
try {
@@ -63,7 +63,7 @@ export default {
this.overwriteAttachment = err.response.data.data
this.modalShow = true
} else {
showError(err.response.data.message)
showError(err.response.data ? err.response.data.message : 'Failed to upload file')
}
}
this.$delete(this.uploadQueue, file.name)
@@ -78,7 +78,7 @@ export default {
bodyFormData.append('file', this.file)
this.$store.dispatch('updateAttachment', {
cardId: this.cardId,
attachmentId: this.overwriteAttachment.id,
attachment: this.overwriteAttachment,
formData: bodyFormData,
})

View File

@@ -47,10 +47,10 @@ export class AttachmentApi {
return response.data
}
async updateAttachment({ cardId, attachmentId, formData }) {
async updateAttachment({ cardId, attachment, formData }) {
const response = await axios({
method: 'POST',
url: this.url(`/cards/${cardId}/attachment/${attachmentId}`),
url: this.url(`/cards/${cardId}/attachment/${attachment.type}:${attachment.id}`),
data: formData,
})
return response.data
@@ -59,14 +59,14 @@ export class AttachmentApi {
async deleteAttachment(attachment) {
await axios({
method: 'DELETE',
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}`),
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.type}:${attachment.id}`),
})
}
async restoreAttachment(attachment) {
const response = await axios({
method: 'GET',
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}/restore`),
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.type}:${attachment.id}/restore`),
})
return response.data
}
@@ -74,7 +74,7 @@ export class AttachmentApi {
async displayAttachment(attachment) {
const response = await axios({
method: 'GET',
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}`),
url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.type}:${attachment.id}`),
})
return response.data
}

View File

@@ -0,0 +1,43 @@
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares'
const createShare = async function({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) {
try {
const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label })
if (!request?.data?.ocs) {
throw request
}
return request
} catch (error) {
console.error('Error while creating share', error)
OC.Notification.showTemporary(t('files_sharing', 'Error creating the share'), { type: 'error' })
throw error
}
}
export {
createShare,
}

View File

@@ -52,21 +52,28 @@ export default {
},
updateAttachment(state, { cardId, attachment }) {
const existingIndex = state.attachments[attachment.cardId].findIndex(a => a.id === attachment.id)
const existingIndex = state.attachments[attachment.cardId].findIndex(a => a.id === attachment.id && a.type === attachment.type)
if (existingIndex !== -1) {
Vue.set(state.attachments[cardId], existingIndex, attachment)
}
},
deleteAttachment(state, deletedAttachment) {
const existingIndex = state.attachments[deletedAttachment.cardId].findIndex(a => a.id === deletedAttachment.id)
const existingIndex = state.attachments[deletedAttachment.cardId].findIndex(a => a.id === deletedAttachment.id && a.type === deletedAttachment.type)
if (existingIndex !== -1) {
state.attachments[deletedAttachment.cardId][existingIndex].deletedAt = Date.now() / 1000 | 0
}
},
unshareAttachment(state, deletedAttachment) {
const existingIndex = state.attachments[deletedAttachment.cardId].findIndex(a => a.id === deletedAttachment.id && a.type === deletedAttachment.type)
if (existingIndex !== -1) {
state.attachments[deletedAttachment.cardId][existingIndex].deletedAt = -1
}
},
restoreAttachment(state, restoredAttachment) {
const existingIndex = state.attachments[restoredAttachment.cardId].findIndex(a => a.id === restoredAttachment.id)
const existingIndex = state.attachments[restoredAttachment.cardId].findIndex(a => a.id === restoredAttachment.id && a.type === restoredAttachment.type)
if (existingIndex !== -1) {
state.attachments[restoredAttachment.cardId][existingIndex].deletedAt = 0
}
@@ -85,9 +92,9 @@ export default {
commit('cardIncreaseAttachmentCount', cardId)
},
async updateAttachment({ commit }, { cardId, attachmentId, formData }) {
const attachment = await apiClient.updateAttachment({ cardId, attachmentId, formData })
commit('updateAttachment', { cardId, attachment })
async updateAttachment({ commit }, { cardId, attachment, formData }) {
const result = await apiClient.updateAttachment({ cardId, attachment, formData })
commit('updateAttachment', { cardId, attachment: result })
},
async deleteAttachment({ commit }, attachment) {
@@ -96,6 +103,12 @@ export default {
commit('cardDecreaseAttachmentCount', attachment.cardId)
},
async unshareAttachment({ commit }, attachment) {
await apiClient.deleteAttachment(attachment)
commit('unshareAttachment', attachment)
commit('cardDecreaseAttachmentCount', attachment.cardId)
},
async restoreAttachment({ commit }, attachment) {
const restoredAttachment = await apiClient.restoreAttachment(attachment)
commit('restoreAttachment', restoredAttachment)

View File

@@ -414,7 +414,7 @@ export default new Vuex.Store({
params.append('search', query)
params.append('format', 'json')
params.append('perPage', 20)
params.append('itemType', [0, 1, 4, 7])
params.append('itemType', [0, 1, 7])
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', response.data.ocs.data)

View File

@@ -0,0 +1,64 @@
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import CardSelector from '../CardSelector'
import { createShare } from '../services/SharingApi'
export default {
icon: 'icon-deck',
displayName: t('deck', 'Share with a Deck card'),
handler: async self => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const SelectorView = Vue.extend(CardSelector)
const ComponentVM = new SelectorView({
propsData: {
title: t('deck', 'Share {file} with a Deck card', { file: decodeURIComponent(self.fileInfo.name) }),
action: t('deck', 'Share'),
},
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Canceled'))
})
ComponentVM.$root.$on('select', async(id) => {
const result = await createShare({
path: self.fileInfo.path + '/' + self.fileInfo.name,
shareType: 12,
shareWith: '' + id,
})
ComponentVM.$el.remove()
ComponentVM.$destroy()
resolve(result.data.ocs.data)
})
})
},
condition: self => true,
}

1
tests/data/test.txt Normal file
View File

@@ -0,0 +1 @@
Hello world

View File

@@ -4,9 +4,11 @@ default:
paths:
- '%paths.base%/../features/'
contexts:
- FeatureContext:
- ServerContext:
baseUrl: http://localhost:8080/index.php/ocs/
admin:
- admin
- admin
regular_user_password: 123456
regular_user_password: 123456
- BoardContext:
baseUrl: http://localhost:8080/

View File

@@ -10,27 +10,83 @@ Feature: acl
And group "group1" exists
Given user "user1" belongs to group "group1"
Scenario: Request the main frontend page
Given Logging in using web as "user0"
When Sending a "GET" to "/index.php/apps/deck" without requesttoken
Then the HTTP status code should be "200"
Scenario: Fetch the board list
Given Logging in using web as "user0"
When Sending a "GET" to "/index.php/apps/deck/boards" with requesttoken
Then the HTTP status code should be "200"
And the Content-Type should be "application/json; charset=utf-8"
When fetching the board list
Then the response should have a status code "200"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of owned board
Given Logging in using web as "admin"
And creates a board named "MyPrivateAdminBoard" with color "fafafa"
When "admin" fetches the board named "MyPrivateAdminBoard"
Then the HTTP status code should be "200"
And the Content-Type should be "application/json; charset=utf-8"
When fetches the board named "MyPrivateAdminBoard"
Then the response should have a status code "200"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of an other users board
Given Logging in using web as "admin"
And creates a board named "MyPrivateAdminBoard" with color "fafafa"
When "user0" fetches the board named "MyPrivateAdminBoard"
Then the HTTP status code should be "403"
And the Content-Type should be "application/json; charset=utf-8"
And creates a board named "MyPrivateAdminBoard" with color "ff0000"
Given Logging in using web as "user0"
When fetches the board named "MyPrivateAdminBoard"
Then the response should have a status code "403"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Share a board
Given Logging in using web as "user0"
And creates a board named "Shared board" with color "ff0000"
And shares the board with user "user1"
| permissionEdit | 0 |
| permissionShare | 0 |
| permissionManage | 0 |
And the response should have a status code 200
And shares the board with user "user2"
| permissionEdit | 1 |
| permissionShare | 1 |
| permissionManage | 1 |
And the response should have a status code 200
Given Logging in using web as "user2"
When fetches the board named "Shared board"
Then the current user should have "read" permissions on the board
And the current user should have "edit" permissions on the board
And the current user should have "share" permissions on the board
And the current user should have "manage" permissions on the board
And create a stack named "Stack"
And the response should have a status code 200
And create a card named "Test"
And the response should have a status code 200
Given Logging in using web as "user1"
When fetches the board named "Shared board"
And create a card named "Test"
And the response should have a status code 403
Then the current user should have "read" permissions on the board
And the current user should not have "edit" permissions on the board
And the current user should not have "share" permissions on the board
And the current user should not have "manage" permissions on the board
And create a stack named "Stack"
And the response should have a status code 403
Scenario: Reshare a board
Given Logging in using web as "user0"
And creates a board named "Reshared board" with color "ff0000"
And shares the board with user "user1"
| permissionEdit | 0 |
| permissionShare | 1 |
| permissionManage | 0 |
And the response should have a status code 200
Given Logging in using web as "user1"
When fetches the board named "Shared board"
And shares the board with user "user2"
| permissionEdit | 1 |
| permissionShare | 1 |
| permissionManage | 1 |
And the response should have a status code 200
Given Logging in using web as "user2"
When fetches the board named "Shared board"
Then the current user should have "read" permissions on the board
And the current user should not have "edit" permissions on the board
And the current user should have "share" permissions on the board
And the current user should not have "manage" permissions on the board

View File

@@ -0,0 +1,154 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
require_once __DIR__ . '/../../vendor/autoload.php';
class BoardContext implements Context {
use RequestTrait;
/** @var array Last board response */
private $board = null;
/** @var array last stack response */
private $stack = null;
/** @var array last card response */
private $card = null;
/**
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
*/
public function createsABoardNamedWithColor($title, $color) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
'title' => $title,
'color' => $color
]);
$this->response->getBody()->seek(0);
$this->board = json_decode((string)$this->response->getBody(), true);
}
/**
* @When /^fetches the board named "([^"]*)"$/
*/
public function fetchesTheBoardNamed($boardName) {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $this->board['id'], []);
$this->response->getBody()->seek(0);
$this->board = json_decode((string)$this->response->getBody(), true);
}
/**
* @When shares the board with user :user
*/
public function sharesTheBoardWithUser($user, TableNode $permissions = null) {
$defaults = [
'permissionEdit' => '0',
'permissionShare' => '0',
'permissionManage' => '0'
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 0,
'participant' => $user,
'permissionEdit' => $result['permissionEdit'] === '1',
'permissionShare' => $result['permissionShare'] === '1',
'permissionManage' => $result['permissionManage'] === '1',
]);
}
/**
* @When shares the board with group :group
*/
public function sharesTheBoardWithGroup($group, TableNode $permissions = null) {
$defaults = [
'permissionEdit' => '0',
'permissionShare' => '0',
'permissionManage' => '0'
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 1,
'participant' => $group,
'permissionEdit' => $result['permissionEdit'] === '1',
'permissionShare' => $result['permissionShare'] === '1',
'permissionManage' => $result['permissionManage'] === '1',
]);
}
/**
* @When /^fetching the board list$/
*/
public function fetchingTheBoardList() {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards');
}
/**
* @When /^fetching the board with id "([^"]*)"$/
*/
public function fetchingTheBoardWithId($id) {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id);
}
/**
* @Given /^create a stack named "([^"]*)"$/
*/
public function createAStackNamed($name) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/stacks', [
'title' => $name,
'boardId' => $this->board['id']
]);
$this->response->getBody()->seek(0);
$this->stack = json_decode((string)$this->response->getBody(), true);
}
/**
* @Given /^create a card named "([^"]*)"$/
*/
public function createACardNamed($name) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/cards', [
'title' => $name,
'stackId' => $this->stack['id']
]);
$this->response->getBody()->seek(0);
$this->card = json_decode((string)$this->response->getBody(), true);
}
/**
* @Then /^the current user should have "(read|edit|share|manage)" permissions on the board$/
*/
public function theCurrentUserShouldHavePermissionsOnTheBoard($permission) {
Assert::assertTrue($this->getPermissionsValue($permission));
}
/**
* @Then /^the current user should not have "(read|edit|share|manage)" permissions on the board$/
*/
public function theCurrentUserShouldNotHavePermissionsOnTheBoard($permission) {
Assert::assertFalse($this->getPermissionsValue($permission));
}
private function getPermissionsValue($permission) {
$mapping = [
'read' => 'PERMISSION_READ',
'edit' => 'PERMISSION_EDIT',
'share' => 'PERMISSION_SHARE',
'manage' => 'PERMISSION_MANAGE',
];
return $this->board['permissions'][$mapping[$permission]];
}
/**
* @When /^share the file "([^"]*)" with the card$/
*/
public function shareWithTheCard($file) {
$table = new TableNode([
['path', $file],
['shareType', 12],
['shareWith', (string)$this->card['id']],
]);
$this->serverContext->creatingShare($table);
}
}

View File

@@ -1,174 +0,0 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use Behat\Gherkin\Node\PyStringNode;
use GuzzleHttp\Exception\ClientException;
require_once __DIR__ . '/../../vendor/autoload.php';
class FeatureContext implements Context {
use WebDav;
/** @var string */
private $mappedUserId;
private $lastInsertIds = [];
/**
* @BeforeSuite
*/
public static function addFilesToSkeleton() {
}
/**
* @When :user requests the deck list
*/
/**
* @When Sending a :method to :url with JSON
*/
public function sendingAToWithJSON($method, $url, \Behat\Gherkin\Node\PyStringNode $data) {
$this->sendJSONrequest($method, $url, json_decode($data));
}
/**
* @When :user creates a new deck with name :name
*/
public function createsANewDeckWithName($user, $content) {
$client = new GuzzleHttp\Client();
$this->response = $client->post(
'http://localhost:8080/index.php/apps/deck/boards',
[
'form_params' => [
'name' => $name,
],
'auth' => [
$this->mappedUserId,
'test',
],
]
);
}
/**
* @Then the response should have a status code :code
* @param string $code
* @throws InvalidArgumentException
*/
public function theResponseShouldHaveAStatusCode($code) {
$currentCode = $this->response->getStatusCode();
if ($currentCode !== (int)$code) {
throw new InvalidArgumentException(
sprintf(
'Expected %s as code got %s',
$code,
$currentCode
)
);
}
}
/**
* @Then the response should be a JSON array with the following mandatory values
* @param TableNode $table
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table) {
$expectedValues = $table->getColumnsHash();
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
foreach ($expectedValues as $value) {
if ((string)$realResponseArray[$value['key']] !== (string)$value['value']) {
throw new InvalidArgumentException(
sprintf(
'Expected %s for key %s got %s',
(string)$value['value'],
$value['key'],
(string)$realResponseArray[$value['key']]
)
);
}
}
}
/**
* @Then the response should be a JSON array with a length of :length
* @param int $length
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithALengthOf($length) {
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
PHPUnit_Framework_Assert::assertEquals($realResponseArray, "foo");
if ((int)count($realResponseArray) !== (int)$length) {
throw new InvalidArgumentException(
sprintf(
'Expected %d as length got %d',
$length,
count($realResponseArray)
)
);
}
}
/**
* @Then /^I should get:$/
*
* @param PyStringNode $string
* @throws \Exception
*/
public function iShouldGet(PyStringNode $string) {
if ((string) $string !== trim($this->cliOutput)) {
throw new Exception(sprintf(
'Expected "%s" but received "%s".',
$string,
$this->cliOutput
));
}
return;
}
private function sendJSONrequest($method, $url, $data) {
$baseUrl = substr($this->baseUrl, 0, -5);
$client = new Client;
try {
$this->response = $client->request(
$method,
$baseUrl . $url,
[
'cookies' => $this->cookieJar,
'json' => $data,
'headers' => [
'requesttoken' => $this->requestToken
]
]
);
} catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
/**
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
*/
public function createsABoardNamedWithColor($title, $color) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
'title' => $title,
'color' => $color
]
);
$response = json_decode($this->response->getBody()->getContents(), true);
$this->lastInsertIds[$title] = $response['id'];
}
/**
* @When /^"([^"]*)" fetches the board named "([^"]*)"$/
*/
public function fetchesTheBoardNamed($user, $boardName) {
$this->loggingInUsingWebAs($user);
$id = $this->lastInsertIds[$boardName];
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/'.$id, []);
}
}

View File

@@ -0,0 +1,121 @@
<?php
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\Assert;
require_once __DIR__ . '/../../vendor/autoload.php';
trait RequestTrait {
private $baseUrl;
private $adminUser;
private $regularUser;
private $cookieJar;
private $response;
public function __construct($baseUrl, $admin = 'admin', $regular_user_password = '123456') {
$this->baseUrl = $baseUrl;
$this->adminUser = $admin === 'admin' ? ['admin', 'admin'] : $admin;
$this->regularUser = $regular_user_password;
}
/** @var ServerContext */
private $serverContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->serverContext = $environment->getContext('ServerContext');
}
/**
* @Then the response should have a status code :code
* @param string $code
* @throws InvalidArgumentException
*/
public function theResponseShouldHaveStatusCode($code) {
$currentCode = $this->response->getStatusCode();
if ($currentCode !== (int)$code) {
throw new InvalidArgumentException(
sprintf(
'Expected %s as code got %s',
$code,
$currentCode
)
);
}
}
/**
* @Then /^the response Content-Type should be "([^"]*)"$/
* @param string $contentType
*/
public function theResponseContentTypeShouldbe($contentType) {
Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]);
}
/**
* @Then the response should be a JSON array with the following mandatory values
* @param TableNode $table
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table) {
$this->response->getBody()->seek(0);
$expectedValues = $table->getColumnsHash();
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
foreach ($expectedValues as $value) {
if ((string)$realResponseArray[$value['key']] !== (string)$value['value']) {
throw new InvalidArgumentException(
sprintf(
'Expected %s for key %s got %s',
(string)$value['value'],
$value['key'],
(string)$realResponseArray[$value['key']]
)
);
}
}
}
/**
* @Then the response should be a JSON array with a length of :length
* @param int $length
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithALengthOf($length) {
$this->response->getBody()->seek(0);
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
if ((int)count($realResponseArray) !== (int)$length) {
throw new InvalidArgumentException(
sprintf(
'Expected %d as length got %d',
$length,
count($realResponseArray)
)
);
}
}
private function sendJSONrequest($method, $url, $data = []) {
$client = new Client;
try {
$this->response = $client->request(
$method,
$this->baseUrl . $url,
[
'cookies' => $this->serverContext->getCookieJar(),
'json' => $data,
'headers' => [
'requesttoken' => $this->serverContext->getReqestToken()
]
]
);
} catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Behat\Behat\Context\Context;
use GuzzleHttp\Cookie\CookieJar;
require_once __DIR__ . '/../../vendor/autoload.php';
class ServerContext implements Context {
use WebDav;
/** @var string */
private $mappedUserId;
private $lastInsertIds = [];
/**
* @BeforeSuite
*/
public static function addFilesToSkeleton() {
}
/**
* @Given /^acting as user "([^"]*)"$/
*/
public function actingAsUser($user) {
$this->cookieJar = new CookieJar();
$this->loggingInUsingWebAs($user);
$this->asAn($user);
}
public function getCookieJar(): CookieJar {
return $this->cookieJar;
}
public function getReqestToken(): string {
return $this->requestToken;
}
}

View File

@@ -1,6 +1,7 @@
Feature: decks
Background:
Given user "admin" exists
Given user "user0" exists
Scenario: Request the main frontend page
@@ -10,27 +11,21 @@ Feature: decks
Scenario: Fetch the board list
Given Logging in using web as "admin"
When Sending a "GET" to "/index.php/apps/deck/boards" with requesttoken
Then the HTTP status code should be "200"
And the Content-Type should be "application/json; charset=utf-8"
When fetching the board list
Then the response should have a status code "200"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of a nonexisting board
Given Logging in using web as "admin"
When Sending a "GET" to "/index.php/apps/deck/boards/13379" with requesttoken
Then the HTTP status code should be "403"
And the Content-Type should be "application/json; charset=utf-8"
When fetching the board with id "99999999"
Then the response should have a status code "403"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Create a new board
Given Logging in using web as "admin"
When Sending a "POST" to "/index.php/apps/deck/boards" with JSON
"""
{
"title": "MyBoard",
"color": "000000"
}
"""
Then the HTTP status code should be "200"
And the Content-Type should be "application/json; charset=utf-8"
When creates a board named "MyBoard" with color "000000"
Then the response should have a status code "200"
And the response Content-Type should be "application/json; charset=utf-8"
And the response should be a JSON array with the following mandatory values
|key|value|
|title|MyBoard|

View File

@@ -0,0 +1,137 @@
Feature: File sharing
Background:
Given user "admin" exists
And user "user0" exists
And user "user1" exists
And user "user2" exists
And user "user3" exists
Given group "group0" exists
And group "group1" exists
Given user "user2" belongs to group "group1"
Given user "user3" belongs to group "group1"
Scenario: Share a file with a card by the board owner
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with user "user1"
Then the HTTP status code should be "200"
Given using new dav path
When User "user0" uploads file "../data/test.txt" to "/user0-file.txt"
Then the HTTP status code should be "201"
Given acting as user "user0"
When share the file "/user0-file.txt" with the card
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And as "user1" the file "/Deck/user0-file.txt" exists
Scenario: Share a file with a card by another user
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with user "user1"
| permissionEdit | 1 |
| permissionShare | 1 |
| permissionManage | 1 |
Then the HTTP status code should be "200"
Given using new dav path
When User "user1" uploads file "../data/test.txt" to "/user1-file.txt"
Then the HTTP status code should be "201"
Given acting as user "user1"
And share the file "/user1-file.txt" with the card
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And as "user0" the file "/Deck/user1-file.txt" exists
And as "user1" the file "/Deck/user1-file.txt" does not exist
Scenario: Share a file with a card by another user fails without edit permission
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with user "user1"
Then the HTTP status code should be "200"
Given using new dav path
When User "user1" uploads file "../data/test.txt" to "/user1-file.txt"
Then the HTTP status code should be "201"
Given acting as user "user1"
And share the file "/user1-file.txt" with the card
Then the OCS status code should be "404"
And the HTTP status code should be "200"
And as "user0" the file "/Deck/user1-file.txt" does not exist
Scenario: Share a file with a card by another user through a group
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with group "group1"
Then the HTTP status code should be "200"
Given using new dav path
When User "user0" uploads file "../data/test.txt" to "/user0-file2.txt"
Then the HTTP status code should be "201"
Given acting as user "user0"
When share the file "/user0-file2.txt" with the card
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And as "user2" the file "/Deck/user0-file2.txt" exists
And as "user0" the file "/Deck/user0-file2.txt" does not exist
Scenario: Remove incoming group share as a user
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with group "group1"
Then the HTTP status code should be "200"
Given using new dav path
When User "user0" uploads file "../data/test.txt" to "/user0-file2.txt"
Then the HTTP status code should be "201"
Given acting as user "user0"
When share the file "/user0-file2.txt" with the card
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And as "user2" the file "/Deck/user0-file2.txt" exists
And as "user3" the file "/Deck/user0-file2.txt" exists
And as "user0" the file "/Deck/user0-file2.txt" does not exist
Given User "user2" deletes file "/Deck/user0-file2.txt"
And as "user2" the file "/Deck/user0-file2.txt" does not exist
And as "user3" the file "/Deck/user0-file2.txt" exists
Scenario: Remove a share as the owner
Given acting as user "user0"
And creates a board named "Shared board" with color "fafafa"
And create a stack named "Stack"
And create a card named "Test"
And shares the board with group "group1"
Then the HTTP status code should be "200"
Given using new dav path
When User "user0" uploads file "../data/test.txt" to "/user0-file2.txt"
Then the HTTP status code should be "201"
Given acting as user "user0"
When share the file "/user0-file2.txt" with the card
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And as "user2" the file "/Deck/user0-file2.txt" exists
And as "user3" the file "/Deck/user0-file2.txt" exists
And as "user0" the file "/Deck/user0-file2.txt" does not exist
Given acting as user "user0"
When Deleting last share
And as "user2" the file "/Deck/user0-file2.txt" does not exist
And as "user3" the file "/Deck/user0-file2.txt" does not exist

View File

@@ -15,10 +15,8 @@ INSTALLED=$($OCC status | grep installed: | cut -d " " -f 5)
if [ "$INSTALLED" == "true" ]; then
$OCC app:enable deck
else
if [ "$SCENARIO_TO_RUN" != "setup_features/setup.feature" ]; then
echo "Nextcloud instance needs to be installed" >&2
exit 1
fi
echo "Nextcloud instance needs to be installed" >&2
exit 1
fi
composer install
@@ -36,7 +34,7 @@ echo $PHPPID
export TEST_SERVER_URL="http://localhost:$PORT/ocs/"
vendor/bin/behat
vendor/bin/behat $SCENARIO_TO_RUN
RESULT=$?
kill $PHPPID

View File

@@ -4,18 +4,6 @@
<TypeDoesNotContainType occurrences="1">
<code>$message !== null</code>
</TypeDoesNotContainType>
<UndefinedMagicMethod occurrences="9">
<code>getArchived</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getCardId</code>
<code>getCardId</code>
<code>getStackId</code>
<code>getTitle</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Activity/DeckProvider.php">
<InvalidScalarArgument occurrences="1">
@@ -28,23 +16,9 @@
</DuplicateClass>
</file>
<file src="lib/AppInfo/Application20.php">
<UndefinedClass occurrences="1">
<code>IBootstrap</code>
</UndefinedClass>
</file>
<file src="lib/AppInfo/ApplicationLegacy.php">
<UndefinedInterfaceMethod occurrences="2">
<code>listen</code>
<code>listen</code>
</UndefinedInterfaceMethod>
</file>
<file src="lib/Collaboration/Resources/ResourceProviderCard.php">
<UndefinedMagicMethod occurrences="4">
<code>getAcl</code>
<code>getOwner</code>
<code>getTitle</code>
<code>getTitle</code>
</UndefinedMagicMethod>
<RedundantCondition occurrences="1">
<code>method_exists($shareManager, 'registerShareProvider')</code>
</RedundantCondition>
</file>
<file src="lib/Command/UserExport.php">
<ImplementedReturnTypeMismatch occurrences="1">
@@ -90,11 +64,9 @@
</InvalidScalarArgument>
</file>
<file src="lib/Controller/PageController.php">
<MissingDependency occurrences="3">
<code>Application</code>
<code>Application</code>
<code>Application</code>
</MissingDependency>
<UndefinedClass occurrences="1">
<code>LoadSidebar</code>
</UndefinedClass>
</file>
<file src="lib/Controller/StackApiController.php">
<RedundantCondition occurrences="1">
@@ -104,21 +76,6 @@
<code>Util</code>
</UndefinedClass>
</file>
<file src="lib/Cron/CardDescriptionActivity.php">
<UndefinedClass occurrences="1">
<code>Job</code>
</UndefinedClass>
</file>
<file src="lib/Cron/DeleteCron.php">
<UndefinedClass occurrences="1">
<code>Job</code>
</UndefinedClass>
</file>
<file src="lib/Cron/ScheduledNotifications.php">
<UndefinedClass occurrences="1">
<code>Job</code>
</UndefinedClass>
</file>
<file src="lib/DAV/Calendar.php">
<UndefinedClass occurrences="1">
<code>ExternalCalendar</code>
@@ -140,18 +97,6 @@
<code>NotFound</code>
</UndefinedClass>
</file>
<file src="lib/Dashboard/DeckWidget.php">
<UndefinedClass occurrences="1">
<code>IWidget</code>
</UndefinedClass>
</file>
<file src="lib/Db/Acl.php">
<UndefinedMagicMethod occurrences="3">
<code>getPermissionEdit</code>
<code>getPermissionManage</code>
<code>getPermissionShare</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/AclMapper.php">
<ParamNameMismatch occurrences="1">
<code>$aclId</code>
@@ -161,32 +106,12 @@
<ParamNameMismatch occurrences="1">
<code>$cardId</code>
</ParamNameMismatch>
<UndefinedMagicMethod occurrences="9">
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/AttachmentMapper.php">
<UndefinedMagicMethod occurrences="2">
<code>getCardId</code>
<code>getCardId</code>
</UndefinedMagicMethod>
<UndefinedVariable occurrences="1">
<code>$query</code>
</UndefinedVariable>
</file>
<file src="lib/Db/Board.php">
<UndefinedMagicMethod occurrences="1">
<code>getLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/BoardMapper.php">
<ParamNameMismatch occurrences="1">
<code>$boardId</code>
@@ -194,53 +119,20 @@
<UndefinedClass occurrences="1">
<code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="2">
<code>setAcl</code>
<code>setLabels</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/Card.php">
<UndefinedClass occurrences="2">
<code>VCalendar</code>
<code>VCalendar</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="9">
<code>getArchived</code>
<code>getArchived</code>
<code>getDescription</code>
<code>getLabels</code>
<code>getLabels</code>
<code>getLastModified</code>
<code>getLastModified</code>
<code>getStackId</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/CardMapper.php">
<ImplicitToStringCast occurrences="1">
<code>$qb-&gt;createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)</code>
</ImplicitToStringCast>
<InvalidScalarArgument occurrences="1">
<code>$entity-&gt;getId()</code>
</InvalidScalarArgument>
<ParamNameMismatch occurrences="1">
<code>$cardId</code>
</ParamNameMismatch>
<UndefinedMagicMethod occurrences="13">
<code>getDescription</code>
<code>getDescription</code>
<code>getDuedate</code>
<code>setCreatedAt</code>
<code>setDatabaseType</code>
<code>setDatabaseType</code>
<code>setDescription</code>
<code>setDescription</code>
<code>setLabels</code>
<code>setLastModified</code>
<code>setLastModified</code>
<code>setNotified</code>
<code>setNotified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/ChangeHelper.php">
<UndefinedThisPropertyAssignment occurrences="3">
@@ -269,69 +161,26 @@
<code>\OCA\Circles\Model\Circle</code>
</UndefinedDocblockClass>
</file>
<file src="lib/Db/Label.php">
<UndefinedMagicMethod occurrences="1">
<code>getLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/LabelMapper.php">
<ParamNameMismatch occurrences="1">
<code>$labelId</code>
</ParamNameMismatch>
<UndefinedMagicMethod occurrences="2">
<code>setLastModified</code>
<code>setLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/RelationalEntity.php">
<UndefinedMagicMethod occurrences="1">
<code>getETag</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/Stack.php">
<UndefinedClass occurrences="2">
<code>VCalendar</code>
<code>VCalendar</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="2">
<code>getLastModified</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/StackMapper.php">
<ParamNameMismatch occurrences="1">
<code>$stackId</code>
</ParamNameMismatch>
<UndefinedMagicMethod occurrences="1">
<code>getBoardId</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Listeners/BeforeTemplateRenderedListener.php">
<UndefinedClass occurrences="1">
<code>BeforeTemplateRenderedEvent</code>
</UndefinedClass>
</file>
<file src="lib/Migration/UnknownUsers.php">
<UndefinedMagicMethod occurrences="4">
<code>getParticipant</code>
<code>getParticipant</code>
<code>getType</code>
<code>getType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Notification/NotificationHelper.php">
<InvalidScalarArgument occurrences="1">
<code>$board-&gt;getId()</code>
</InvalidScalarArgument>
<MissingDependency occurrences="1">
<code>Application</code>
</MissingDependency>
<UndefinedMagicMethod occurrences="3">
<code>getTitle</code>
<code>getTitle</code>
<code>getTitle</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Notification/Notifier.php">
<RedundantCast occurrences="7">
@@ -348,42 +197,12 @@
<InvalidPropertyAssignmentValue occurrences="1">
<code>[]</code>
</InvalidPropertyAssignmentValue>
<UndefinedClass occurrences="2">
<code>IndexDocument</code>
<code>SearchTemplate</code>
</UndefinedClass>
</file>
<file src="lib/Search/BoardSearchResultEntry.php">
<UndefinedClass occurrences="1">
<code>SearchResultEntry</code>
</UndefinedClass>
</file>
<file src="lib/Search/CardSearchResultEntry.php">
<UndefinedClass occurrences="1">
<code>SearchResultEntry</code>
</UndefinedClass>
</file>
<file src="lib/Search/DeckProvider.php">
<UndefinedClass occurrences="1">
<code>IProvider</code>
</UndefinedClass>
</file>
<file src="lib/Service/AssignmentService.php">
<InvalidScalarArgument occurrences="2">
<code>$cardId</code>
<code>$cardId</code>
</InvalidScalarArgument>
<UndefinedMagicMethod occurrences="9">
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
<code>setCardId</code>
<code>setParticipant</code>
<code>setType</code>
</UndefinedMagicMethod>
<UndefinedThisPropertyAssignment occurrences="1">
<code>$this-&gt;currentUser</code>
</UndefinedThisPropertyAssignment>
@@ -391,86 +210,11 @@
<code>$this-&gt;currentUser</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Service/AttachmentService.php">
<MissingDependency occurrences="1">
<code>Application</code>
</MissingDependency>
<UndefinedMagicMethod occurrences="13">
<code>getCardId</code>
<code>getCardId</code>
<code>getCardId</code>
<code>getData</code>
<code>getType</code>
<code>getType</code>
<code>setCardId</code>
<code>setCreatedAt</code>
<code>setCreatedBy</code>
<code>setData</code>
<code>setLastModified</code>
<code>setLastModified</code>
<code>setType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/BoardService.php">
<InvalidArgument occurrences="6">
<code>'\OCA\Deck\Board::onCreate'</code>
<code>'\OCA\Deck\Board::onDelete'</code>
<code>'\OCA\Deck\Board::onDelete'</code>
<code>'\OCA\Deck\Board::onShareEdit'</code>
<code>'\OCA\Deck\Board::onUpdate'</code>
<code>'\OCA\Deck\Board::onUpdate'</code>
</InvalidArgument>
<MissingDependency occurrences="3">
<code>Application</code>
<code>Application</code>
<code>Application</code>
</MissingDependency>
<TooManyArguments occurrences="2">
<code>findAll</code>
<code>findAll</code>
</TooManyArguments>
<UndefinedMagicMethod occurrences="40">
<code>getAcl</code>
<code>getAcl</code>
<code>getAcl</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getParticipant</code>
<code>getType</code>
<code>setBoardId</code>
<code>setBoardId</code>
<code>setBoardId</code>
<code>setBoardId</code>
<code>setColor</code>
<code>setColor</code>
<code>setColor</code>
<code>setColor</code>
<code>setColor</code>
<code>setLabels</code>
<code>setOwner</code>
<code>setOwner</code>
<code>setParticipant</code>
<code>setPermissionEdit</code>
<code>setPermissionEdit</code>
<code>setPermissionManage</code>
<code>setPermissionManage</code>
<code>setPermissionShare</code>
<code>setPermissionShare</code>
<code>setPermissions</code>
<code>setPermissions</code>
<code>setPermissions</code>
<code>setPermissions</code>
<code>setSettings</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/CardService.php">
<TooFewArguments occurrences="1">
@@ -479,52 +223,6 @@
<UndefinedDocblockClass occurrences="1">
<code>\OCP\AppFramework\Db\</code>
</UndefinedDocblockClass>
<UndefinedMagicMethod occurrences="44">
<code>getArchived</code>
<code>getArchived</code>
<code>getArchived</code>
<code>getArchived</code>
<code>getArchived</code>
<code>getDescription</code>
<code>getDescription</code>
<code>getDescription</code>
<code>getDescriptionPrev</code>
<code>getDescriptionPrev</code>
<code>getLastEditor</code>
<code>getLastEditor</code>
<code>getLastEditor</code>
<code>getOrder</code>
<code>setArchived</code>
<code>setArchived</code>
<code>setArchived</code>
<code>setAssignedUsers</code>
<code>setAssignedUsers</code>
<code>setAttachmentCount</code>
<code>setAttachments</code>
<code>setCommentsUnread</code>
<code>setDeletedAt</code>
<code>setDeletedAt</code>
<code>setDescription</code>
<code>setDescription</code>
<code>setDescriptionPrev</code>
<code>setDescriptionPrev</code>
<code>setDuedate</code>
<code>setDuedate</code>
<code>setLabels</code>
<code>setLastEditor</code>
<code>setOrder</code>
<code>setOrder</code>
<code>setOwner</code>
<code>setOwner</code>
<code>setStackId</code>
<code>setStackId</code>
<code>setStackId</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setTitle</code>
<code>setType</code>
<code>setType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/CirclesService.php">
<UndefinedClass occurrences="2">
@@ -533,13 +231,6 @@
</UndefinedClass>
</file>
<file src="lib/Service/CommentService.php">
<MissingDependency occurrences="5">
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
</MissingDependency>
<UndefinedThisPropertyAssignment occurrences="2">
<code>$this-&gt;cardMapper</code>
<code>$this-&gt;permissionService</code>
@@ -555,25 +246,7 @@
<code>$this-&gt;permissionService</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Service/ConfigService.php">
<InvalidScalarArgument occurrences="1">
<code>(int)$value</code>
</InvalidScalarArgument>
<MissingDependency occurrences="7">
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
<code>Application</code>
</MissingDependency>
</file>
<file src="lib/Service/DefaultBoardService.php">
<MissingDependency occurrences="2">
<code>Application</code>
<code>Application</code>
</MissingDependency>
<TypeDoesNotContainNull occurrences="6">
<code>$color === false || $color === null</code>
<code>$color === null</code>
@@ -597,94 +270,46 @@
<code>is_resource($content)</code>
<code>is_resource($content)</code>
</RedundantCondition>
<UndefinedMagicMethod occurrences="10">
<code>getCardId</code>
<code>getCardId</code>
<code>getCardId</code>
<code>getData</code>
<code>getData</code>
<code>setData</code>
<code>setData</code>
<code>setDeletedAt</code>
<code>setExtendedData</code>
<code>setLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/FullTextSearchService.php">
<UndefinedClass occurrences="2">
<code>DocumentAccess</code>
<code>IndexDocument</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="4">
<code>getDescription</code>
<code>getDescription</code>
<code>getTitle</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/LabelService.php">
<UndefinedMagicMethod occurrences="4">
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>setBoardId</code>
<code>setColor</code>
<code>setColor</code>
<code>setTitle</code>
<code>setTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/OverviewService.php">
<UndefinedMagicMethod occurrences="4">
<code>setAssignedUsers</code>
<code>setAttachmentCount</code>
<code>setCommentsUnread</code>
<code>setLabels</code>
</UndefinedMagicMethod>
<file src="lib/Service/FilesAppService.php">
<MissingDependency occurrences="4">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>IRootFolder</code>
<code>Share\Exceptions\ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Service/PermissionService.php">
<UndefinedClass occurrences="2">
<code>\OCA\Circles\Api\v1\Circles</code>
<code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="6">
<code>getAcl</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getParticipant</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
<code>getType</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Service/StackService.php">
<InvalidArgument occurrences="3">
<code>'\OCA\Deck\Stack::onCreate'</code>
<code>'\OCA\Deck\Stack::onDelete'</code>
<code>'\OCA\Deck\Stack::onUpdate'</code>
</InvalidArgument>
<UndefinedClass occurrences="1">
<code>BadRquestException</code>
</UndefinedClass>
<UndefinedMagicMethod occurrences="14">
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getBoardId</code>
<code>getOrder</code>
<code>setBoardId</code>
<code>setBoardId</code>
<code>setCards</code>
<code>setDeletedAt</code>
<code>setDeletedAt</code>
<code>setOrder</code>
<code>setOrder</code>
<code>setTitle</code>
<code>setTitle</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Sharing/DeckShareProvider.php">
<InvalidReturnStatement occurrences="1">
<code>$shares</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>getSharesInFolder</code>
</InvalidReturnType>
<MissingDependency occurrences="7">
<code>GenericShareException</code>
<code>GenericShareException</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Sharing/Listener.php">
<InvalidArgument occurrences="1">
<code>[self::class, 'listenPreShare']</code>
</InvalidArgument>
</file>
</files>

View File

@@ -39,7 +39,6 @@ use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\IL10N;
use OCP\IUser;
use OCP\L10N\IFactory;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
@@ -61,8 +60,6 @@ class ActivityManagerTest extends TestCase {
private $attachmentMapper;
/** @var AclMapper|MockObject */
private $aclMapper;
/** @var IFactory|MockObject */
private $l10nFactory;
/** @var IL10N|MockObject */
private $l10n;
/** @var string */
@@ -76,7 +73,6 @@ class ActivityManagerTest extends TestCase {
$this->stackMapper = $this->createMock(StackMapper::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->aclMapper = $this->createMock(AclMapper::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->l10n = $this->createMock(IL10N::class);
$this->activityManager = new ActivityManager(
$this->manager,
@@ -86,7 +82,7 @@ class ActivityManagerTest extends TestCase {
$this->stackMapper,
$this->attachmentMapper,
$this->aclMapper,
$this->l10nFactory,
$this->l10n,
$this->userId
);
}
@@ -98,20 +94,17 @@ class ActivityManagerTest extends TestCase {
->will($this->returnCallback(function ($s) {
return $s;
}));
$this->l10nFactory->method('get')
->with('deck', 'cz')
->willReturn($this->l10n);
foreach ($managerClass->getConstants() as $constant => $value) {
if (strpos($constant, 'SUBJECT') === 0) {
$format = $this->activityManager->getActivityFormat('cz', $value, [], false);
$format = $this->activityManager->getActivityFormat($value, [], false);
if ($format !== '') {
$this->assertStringContainsString('{user}', $format);
} else {
/** @noinspection ForgottenDebugOutputInspection */
print_r('No activity string found for '. $constant . PHP_EOL);
}
$format = $this->activityManager->getActivityFormat('cz', $value, [], true);
$format = $this->activityManager->getActivityFormat($value, [], true);
if ($format !== '') {
$this->assertStringStartsWith('You', $format);
} else {
@@ -323,10 +316,6 @@ class ActivityManagerTest extends TestCase {
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->attachmentMapper->expects($this->once())
->method('find')
->with(777)
->willReturn($attachment);
$this->cardMapper->expects($this->once())
->method('find')
->with(555)
@@ -347,7 +336,7 @@ class ActivityManagerTest extends TestCase {
'archived' => $card->getArchived()
],
'attachment' => $attachment
], $this->invokePrivate($this->activityManager, 'findDetailsForAttachment', [777]));
], $this->invokePrivate($this->activityManager, 'findDetailsForAttachment', [$attachment]));
}
public function invokePrivate(&$object, $methodName, array $parameters = []) {

View File

@@ -0,0 +1,128 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Migration;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\Migration\IOutput;
class UnknownUserTest extends \Test\TestCase {
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var AclMapper */
private $aclMapper;
/** @var BoardMapper */
private $boardMapper;
/** @var UnknownUsers */
private $unknownUsers;
public function setUp(): void {
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->aclMapper = $this->createMock(AclMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->unknownUsers = new UnknownUsers($this->userManager, $this->groupManager, $this->aclMapper, $this->boardMapper);
}
public function testGetName() {
$this->assertEquals('Delete orphaned ACL rules', $this->unknownUsers->getName());
}
public function testRun() {
/** @var IOutput $output */
$output = $this->createMock(IOutput::class);
$boards = [
$this->getBoard(1,'Test', 'admin'),
];
$acl = [
$this->getAcl(Acl::PERMISSION_TYPE_USER, 'existing', 1),
$this->getAcl(Acl::PERMISSION_TYPE_USER, 'not existing', 1),
$this->getAcl(Acl::PERMISSION_TYPE_GROUP, 'existing', 1),
$this->getAcl(Acl::PERMISSION_TYPE_GROUP, 'not existing', 1),
];
$this->aclMapper->expects($this->at(0))
->method('findAll')
->with(1)
->willReturn($acl);
$this->userManager->expects($this->at(0))
->method('get')
->with('existing')
->willReturn(true);
$this->userManager->expects($this->at(1))
->method('get')
->with('not existing')
->willReturn(null);
$this->groupManager->expects($this->at(0))
->method('get')
->with('existing')
->willReturn(true);
$this->groupManager->expects($this->at(1))
->method('get')
->with('not existing')
->willReturn(null);
$this->boardMapper->expects($this->once())
->method('findAll')
->willReturn($boards);
$this->aclMapper->expects($this->at(1))
->method('delete')
->with($acl[1]);
$this->aclMapper->expects($this->at(2))
->method('delete')
->with($acl[3]);
$this->unknownUsers->run($output);
}
/** @return Acl */
public function getAcl($type = Acl::PERMISSION_TYPE_USER, $participant = 'admin', $boardId = 123) {
$acl = new Acl();
$acl->setParticipant($participant);
$acl->setType($type);
$acl->setBoardId($boardId);
return $acl;
}
/** @return Board */
public function getBoard($id, $title, $owner) {
$board = new Board();
$board->setId($id);
$board->setTitle($title);
$board->setOwner($owner);
return $board;
}
}

View File

@@ -42,7 +42,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/** @internal Just for testing the service registration */
class MyAttachmentService {
class MyAttachmentService implements IAttachmentService {
public function extendData(Attachment $attachment) {
}
public function display(Attachment $attachment) {
@@ -84,6 +84,10 @@ class AttachmentServiceTest extends TestCase {
private $l10n;
/** @var ChangeHelper */
private $changeHelper;
/**
* @var IAttachmentService|MockObject
*/
private $filesAppServiceImpl;
/**
* @throws \OCP\AppFramework\QueryException
@@ -92,6 +96,8 @@ class AttachmentServiceTest extends TestCase {
parent::setUp();
$this->attachmentServiceImpl = $this->createMock(IAttachmentService::class);
$this->filesAppServiceImpl = $this->createMock(IAttachmentService::class);
$this->appContainer = $this->createMock(IAppContainer::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
@@ -105,6 +111,8 @@ class AttachmentServiceTest extends TestCase {
$this->cacheFactory->expects($this->any())->method('createDistributed')->willReturn($this->cache);
$this->appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($this->attachmentServiceImpl);
$this->appContainer->expects($this->at(1))->method('query')->with(FilesAppService::class)->willReturn($this->filesAppServiceImpl);
$this->application->expects($this->any())
->method('getContainer')
->willReturn($this->appContainer);
@@ -119,8 +127,12 @@ class AttachmentServiceTest extends TestCase {
$application = $this->createMock(Application::class);
$appContainer = $this->createMock(IAppContainer::class);
$fileServiceMock = $this->createMock(FileService::class);
$appContainer->expects($this->at(1))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$fileAppServiceMock = $this->createMock(FilesAppService::class);
$appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($fileServiceMock);
$appContainer->expects($this->at(1))->method('query')->with(FilesAppService::class)->willReturn($fileAppServiceMock);
$appContainer->expects($this->at(2))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
@@ -135,8 +147,10 @@ class AttachmentServiceTest extends TestCase {
$application = $this->createMock(Application::class);
$appContainer = $this->createMock(IAppContainer::class);
$fileServiceMock = $this->createMock(FileService::class);
$fileAppServiceMock = $this->createMock(FilesAppService::class);
$appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($fileServiceMock);
$appContainer->expects($this->at(1))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$appContainer->expects($this->at(1))->method('query')->with(FilesAppService::class)->willReturn($fileAppServiceMock);
$appContainer->expects($this->at(2))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
@@ -256,7 +270,7 @@ class AttachmentServiceTest extends TestCase {
->method('display')
->with($attachment)
->willReturn($response);
$actual = $this->attachmentService->display(1);
$actual = $this->attachmentService->display(1, 1);
$this->assertEquals($response, $actual);
}
@@ -272,8 +286,8 @@ class AttachmentServiceTest extends TestCase {
$this->attachmentServiceImpl->expects($this->once())
->method('display')
->with($attachment)
->will($this->throwException(new InvalidAttachmentType('deck_file')));
$this->attachmentService->display(1);
->will($this->throwException(new NotFoundException('deck_file')));
$this->attachmentService->display(1, 1);
}
public function testUpdate() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
@@ -295,7 +309,7 @@ class AttachmentServiceTest extends TestCase {
$a->setExtendedData(['mime' => 'image/jpeg']);
});
$actual = $this->attachmentService->update(1, 'file_name.jpg');
$actual = $this->attachmentService->update(1, 1, 'file_name.jpg');
$expected->setExtendedData(['mime' => 'image/jpeg']);
$expected->setLastModified($attachment->getLastModified());
@@ -319,7 +333,7 @@ class AttachmentServiceTest extends TestCase {
$this->attachmentMapper->expects($this->once())
->method('delete')
->willReturn($attachment);
$actual = $this->attachmentService->delete(1);
$actual = $this->attachmentService->delete(1, 1);
$this->assertEquals($expected, $actual);
}
@@ -344,7 +358,7 @@ class AttachmentServiceTest extends TestCase {
->method('update')
->willReturn($attachment);
$expected->setDeletedAt(23);
$actual = $this->attachmentService->delete(1);
$actual = $this->attachmentService->delete(1, 1);
$this->assertEquals($expected, $actual);
}
@@ -364,7 +378,7 @@ class AttachmentServiceTest extends TestCase {
->method('update')
->willReturn($attachment);
$expected->setDeletedAt(0);
$actual = $this->attachmentService->restore(1);
$actual = $this->attachmentService->restore(1, 1);
$this->assertEquals($expected, $actual);
}
@@ -381,6 +395,6 @@ class AttachmentServiceTest extends TestCase {
$this->attachmentServiceImpl->expects($this->once())
->method('allowUndo')
->willReturn(false);
$actual = $this->attachmentService->restore(1);
$actual = $this->attachmentService->restore(1, 1);
}
}

View File

@@ -123,20 +123,9 @@ class BoardServiceTest extends TestCase {
$b3 = new Board();
$b3->setId(3);
$this->boardMapper->expects($this->once())
->method('findAllByUser')
->method('findAllForUser')
->with('admin')
->willReturn([$b1, $b2]);
$this->stackMapper->expects($this->any())
->method('findAll')
->willReturn([]);
$this->boardMapper->expects($this->once())
->method('findAllByGroups')
->with('admin', ['a', 'b', 'c'])
->willReturn([$b2, $b3]);
$this->boardMapper->expects($this->once())
->method('findAllByCircles')
->with('admin')
->willReturn([]);
->willReturn([$b1, $b2, $b3]);
$user = $this->createMock(IUser::class);
$this->groupManager->method('getUserGroupIds')
->willReturn(['a', 'b', 'c']);

View File

@@ -25,10 +25,10 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
@@ -57,8 +57,6 @@ class FileServiceTest extends TestCase {
private $config;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var IMimeTypeDetector|MockObject */
private $mimeTypeDetector;
public function setUp(): void {
parent::setUp();
@@ -69,8 +67,7 @@ class FileServiceTest extends TestCase {
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->config = $this->createMock(IConfig::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper, $this->mimeTypeDetector);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper);
}
public function mockGetFolder($cardId) {
@@ -271,13 +268,51 @@ class FileServiceTest extends TestCase {
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$this->mimeTypeDetector->expects($this->once())
->method('getSecureMimeType')
->willReturn('image/jpeg');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Type', 'image/jpeg');
$expected->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$this->assertEquals($expected, $actual);
}
public function testDisplayPdf() {
$this->config->expects($this->once())
->method('getSystemValue')
->willReturn('123');
$appDataFolder = $this->createMock(Folder::class);
$deckAppDataFolder = $this->createMock(Folder::class);
$cardFolder = $this->createMock(Folder::class);
$this->rootFolder->expects($this->once())->method('get')->willReturn($appDataFolder);
$appDataFolder->expects($this->once())->method('get')->willReturn($deckAppDataFolder);
$deckAppDataFolder->expects($this->once())->method('get')->willReturn($cardFolder);
$attachment = $this->getAttachment();
$file = $this->createMock(\OCP\Files\File::class);
$cardFolder->expects($this->once())->method('get')->willReturn($file);
$file->expects($this->any())
->method('getMimeType')
->willReturn('application/pdf');
$file->expects($this->any())
->method('getName')
->willReturn('file1');
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$expected->addHeader('Content-Type', 'application/pdf');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$this->assertEquals($expected, $actual);
}

View File

@@ -1,148 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2018 Ryan Fletcher <ryan.fletcher@codepassion.ca>
*
* @author Ryan Fletcher <ryan.fletcher@codepassion.ca>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Db\Attachment;
class AttachmentApiControllerTest extends \Test\TestCase {
private $appName = 'deck';
private $controller;
private $request;
private $attachmentExample;
private $cardId;
private $attachmentService;
public function setUp(): void {
parent::setUp();
$this->attachmentExample = new Attachment();
$this->attachmentExample->setId(1);
$this->cardId = 1;
$this->request = $this->createMock(IRequest::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->controller = new AttachmentApiController(
$this->appName,
$this->request,
$this->attachmentService
);
}
public function testGetAll() {
$allAttachments = [$this->attachmentExample];
$this->attachmentService->expects($this->once())
->method('findAll')
->willReturn($allAttachments);
$this->request->expects($this->once())
->method('getParam')
->with('cardId')
->willReturn($allAttachments);
$expected = new DataResponse($allAttachments, HTTP::STATUS_OK);
$actual = $this->controller->getAll();
$this->assertEquals($expected, $actual);
}
public function testDisplay() {
$this->attachmentService->expects($this->once())
->method('display')
->willReturn($this->attachmentExample);
$this->request->expects($this->once())
->method('getParam')
->willReturn($this->attachmentExample->getId());
$expected = $this->attachmentExample;
$actual = $this->controller->display();
$this->assertEquals($expected, $actual);
}
public function testCreate() {
$type = 'not null';
$data = ['not null'];
$this->attachmentService->expects($this->once())
->method('create')
->willReturn($this->attachmentExample);
$this->request->expects($this->once())
->method('getParam')
->with('cardId')
->willReturn($this->cardId);
$expected = new DataResponse($this->attachmentExample, HTTP::STATUS_OK);
$actual = $this->controller->create($type, $data);
$this->assertEquals($expected, $actual);
}
public function testUpdate() {
// FIXME: what is data supposed to be in this context?
$data = ['not empty data'];
$this->attachmentService->expects($this->once())
->method('update')
->willReturn($this->attachmentExample);
$this->request->expects($this->once())
->method('getParam')
->willReturn($this->attachmentExample->getId());
$expected = new DataResponse($this->attachmentExample, HTTP::STATUS_OK);
$actual = $this->controller->update($data);
$this->assertEquals($expected, $actual);
}
public function testDelete() {
$this->attachmentService->expects($this->once())
->method('delete')
->willReturn($this->attachmentExample);
$this->request->expects($this->once())
->method('getParam')
->willReturn($this->attachmentExample->getId());
$expected = new DataResponse($this->attachmentExample, HTTP::STATUS_OK);
$actual = $this->controller->delete();
$this->assertEquals($expected, $actual);
}
public function testRestore() {
$this->attachmentService->expects($this->once())
->method('restore')
->willReturn($this->attachmentExample);
$this->request->expects($this->once())
->method('getParam')
->willReturn($this->attachmentExample->getId());
$expected = new DataResponse($this->attachmentExample, HTTP::STATUS_OK);
$actual = $this->controller->restore();
$this->assertEquals($expected, $actual);
}
}

View File

@@ -1,99 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Controller;
use OCA\Deck\Service\AttachmentService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class AttachmentControllerTest extends \Test\TestCase {
/** @var Controller|\PHPUnit\Framework\MockObject\MockObject */
private $controller;
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var string */
private $userId = 'user';
public function setUp(): void {
$this->request = $this->createMock(IRequest::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->controller = new AttachmentController(
'deck',
$this->request,
$this->attachmentService
);
}
public function testGetAll() {
$this->attachmentService->expects($this->once())->method('findAll')->with(1);
$this->controller->getAll(1);
}
public function testDisplay() {
$this->attachmentService->expects($this->once())->method('display')->with(2);
$this->controller->display(2);
}
public function testCreate() {
$this->request->expects($this->exactly(2))
->method('getParam')
->will($this->onConsecutiveCalls('type', 'data'));
$this->attachmentService->expects($this->once())
->method('create')
->with(1, 'type', 'data')
->willReturn(1);
$this->assertEquals(1, $this->controller->create(1));
}
public function testUpdate() {
$this->request->expects($this->exactly(1))
->method('getParam')
->will($this->onConsecutiveCalls('data'));
$this->attachmentService->expects($this->once())
->method('update')
->with(2, 'data')
->willReturn(1);
$this->assertEquals(1, $this->controller->update(2));
}
public function testDelete() {
$this->attachmentService->expects($this->once())
->method('delete')
->with(234)
->willReturn(1);
$this->assertEquals(1, $this->controller->delete(234));
}
public function testRestore() {
$this->attachmentService->expects($this->once())
->method('restore')
->with(234)
->willReturn(1);
$this->assertEquals(1, $this->controller->restore(234));
}
}

View File

@@ -26,6 +26,7 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\IRequest;
@@ -37,6 +38,7 @@ class PageControllerTest extends \Test\TestCase {
private $permissionService;
private $initialState;
private $configService;
private $eventDispatcher;
public function setUp(): void {
$this->l10n = $this->createMock(IL10N::class);
@@ -44,9 +46,10 @@ class PageControllerTest extends \Test\TestCase {
$this->permissionService = $this->createMock(PermissionService::class);
$this->configService = $this->createMock(ConfigService::class);
$this->initialState = $this->createMock(IInitialStateService::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->controller = new PageController(
'deck', $this->request, $this->permissionService, $this->initialState, $this->configService
'deck', $this->request, $this->permissionService, $this->initialState, $this->configService, $this->eventDispatcher
);
}