Merge pull request #2638 from nextcloud/enh/files

This commit is contained in:
Julius Härtl
2021-01-04 22:33:17 +01:00
committed by GitHub
52 changed files with 2842 additions and 1124 deletions

View File

@@ -81,6 +81,8 @@ jobs:
fi fi
mkdir data mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin ./occ 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 }} ./occ app:enable --force ${{ env.APP_NAME }}
php -S localhost:8080 & php -S localhost:8080 &

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
- 🚀 Get your project organized - 🚀 Get your project organized
</description> </description>
<version>1.2.2</version> <version>1.3.0-beta1</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>Julius Härtl</author> <author>Julius Härtl</author>
<namespace>Deck</namespace> <namespace>Deck</namespace>
@@ -36,7 +36,7 @@
<database min-version="9.4">pgsql</database> <database min-version="9.4">pgsql</database>
<database>sqlite</database> <database>sqlite</database>
<database min-version="5.5">mysql</database> <database min-version="5.5">mysql</database>
<nextcloud min-version="18" max-version="21" /> <nextcloud min-version="21" max-version="21" />
</dependencies> </dependencies>
<background-jobs> <background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job> <job>OCA\Deck\Cron\DeleteCron</job>

View File

@@ -80,59 +80,66 @@ return [
['name' => 'label#delete', 'url' => '/labels/{labelId}', 'verb' => 'DELETE'], ['name' => 'label#delete', 'url' => '/labels/{labelId}', 'verb' => 'DELETE'],
// api // api
['name' => 'board_api#index', 'url' => '/api/v1.0/boards', 'verb' => 'GET'], ['name' => 'board_api#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
['name' => 'board_api#get', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'GET'], ['name' => 'board_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'GET'],
['name' => 'board_api#create', 'url' => '/api/v1.0/boards', 'verb' => 'POST'], ['name' => 'board_api#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
['name' => 'board_api#delete', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'DELETE'], ['name' => 'board_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'DELETE'],
['name' => 'board_api#update', 'url' => '/api/v1.0/boards/{boardId}', 'verb' => 'PUT'], ['name' => 'board_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'PUT'],
['name' => 'board_api#undo_delete', 'url' => '/api/v1.0/boards/{boardId}/undo_delete', 'verb' => 'POST'], ['name' => 'board_api#undo_delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/undo_delete', 'verb' => 'POST'],
['name' => 'board_api#addAcl', 'url' => '/api/v1.0/boards/{boardId}/acl', 'verb' => 'POST'], ['name' => 'board_api#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
['name' => 'board_api#deleteAcl', 'url' => '/api/v1.0/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'], ['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
['name' => 'board_api#updateAcl', 'url' => '/api/v1.0/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'], ['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#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'],
['name' => 'stack_api#getArchived', 'url' => '/api/v1.0/boards/{boardId}/stacks/archived', 'verb' => 'GET'], ['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'],
['name' => 'stack_api#get', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'GET'], ['name' => 'stack_api#get', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}', 'verb' => 'GET'],
['name' => 'stack_api#create', 'url' => '/api/v1.0/boards/{boardId}/stacks', 'verb' => 'POST'], ['name' => 'stack_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'POST'],
['name' => 'stack_api#update', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'PUT'], ['name' => 'stack_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}', 'verb' => 'PUT'],
['name' => 'stack_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}', 'verb' => 'DELETE'], ['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#get', 'url' => '/api/v{apiVersion}/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#create', 'url' => '/api/v{apiVersion}/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#update', 'url' => '/api/v{apiVersion}/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#assignLabel', 'url' => '/api/v{apiVersion}/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#removeLabel', 'url' => '/api/v{apiVersion}/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#assignUser', 'url' => '/api/v{apiVersion}/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#unassignUser', 'url' => '/api/v{apiVersion}/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#reorder', 'url' => '/api/v{apiVersion}/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#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#get', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels/{labelId}', 'verb' => 'GET'],
['name' => 'label_api#create', 'url' => '/api/v1.0/boards/{boardId}/labels', 'verb' => 'POST'], ['name' => 'label_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels', 'verb' => 'POST'],
['name' => 'label_api#update', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'PUT'], ['name' => 'label_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/labels/{labelId}', 'verb' => 'PUT'],
['name' => 'label_api#delete', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'DELETE'], ['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#getAll', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'GET', 'requirements' => ['apiVersion' => '1.0']],
['name' => 'attachment_api#display', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'GET'], ['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/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments', 'verb' => 'POST'], ['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/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'PUT'], ['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/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'DELETE'], ['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/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'], ['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' => [ 'ocs' => [
['name' => 'Config#get', 'url' => '/api/v1.0/config', 'verb' => 'GET'], ['name' => 'Config#get', 'url' => '/api/v{apiVersion}/config', 'verb' => 'GET'],
['name' => 'Config#setValue', 'url' => '/api/v1.0/config/{key}', 'verb' => 'POST'], ['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#list', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments', 'verb' => 'GET'],
['name' => 'comments_api#create', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'POST'], ['name' => 'comments_api#create', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments', 'verb' => 'POST'],
['name' => 'comments_api#update', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'], ['name' => 'comments_api#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
['name' => 'comments_api#delete', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'], ['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", "name": "nextcloud/deck",
"type": "project", "type": "project",
"license": "AGPLv3", "license": "AGPLv3",
"minimum-stability": "dev",
"authors": [ "authors": [
{ {
"name": "Julius Härtl", "name": "Julius Härtl",
@@ -13,7 +14,7 @@
}, },
"require-dev": { "require-dev": {
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^20", "christophwurst/nextcloud": "dev-master",
"phpunit/phpunit": "^8", "phpunit/phpunit": "^8",
"nextcloud/coding-standard": "^0.4.0", "nextcloud/coding-standard": "^0.4.0",
"symfony/event-dispatcher": "^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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "4a3d65807490679a4de897a8643385bb", "content-hash": "3172d27bd19b2a125db3197c495deda9",
"packages": [ "packages": [
{ {
"name": "cogpowered/finediff", "name": "cogpowered/finediff",
@@ -225,25 +225,26 @@
}, },
{ {
"name": "christophwurst/nextcloud", "name": "christophwurst/nextcloud",
"version": "v20.0.4", "version": "dev-master",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ChristophWurst/nextcloud_composer.git", "url": "https://github.com/ChristophWurst/nextcloud_composer.git",
"reference": "a207b55848d1ac4c83a954eac90c07714bbdaaed" "reference": "0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/a207b55848d1ac4c83a954eac90c07714bbdaaed", "url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd",
"reference": "a207b55848d1ac4c83a954eac90c07714bbdaaed", "reference": "0c78518f688ea2ceb1e23ff2931e0e1db1b75ddd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2" "php": "^7.3 || ~8.0.0"
}, },
"default-branch": true,
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "20.0.0-dev" "dev-master": "21.0.0-dev"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@@ -259,9 +260,9 @@
"description": "Composer package containing Nextcloud's public API (classes, interfaces)", "description": "Composer package containing Nextcloud's public API (classes, interfaces)",
"support": { "support": {
"issues": "https://github.com/ChristophWurst/nextcloud_composer/issues", "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", "name": "composer/package-versions-deprecated",
@@ -4893,9 +4894,10 @@
} }
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "dev",
"stability-flags": { "stability-flags": {
"roave/security-advisories": 20 "roave/security-advisories": 20,
"christophwurst/nextcloud": 20
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,

View File

@@ -380,7 +380,7 @@ class ActivityManager {
case self::SUBJECT_ATTACHMENT_UPDATE: case self::SUBJECT_ATTACHMENT_UPDATE:
case self::SUBJECT_ATTACHMENT_DELETE: case self::SUBJECT_ATTACHMENT_DELETE:
case self::SUBJECT_ATTACHMENT_RESTORE: case self::SUBJECT_ATTACHMENT_RESTORE:
$subjectParams = $this->findDetailsForAttachment($entity->getId()); $subjectParams = $this->findDetailsForAttachment($entity);
break; break;
case self::SUBJECT_BOARD_SHARE: case self::SUBJECT_BOARD_SHARE:
case self::SUBJECT_BOARD_UNSHARE: case self::SUBJECT_BOARD_UNSHARE:
@@ -527,8 +527,7 @@ class ActivityManager {
]; ];
} }
private function findDetailsForAttachment($attachmentId) { private function findDetailsForAttachment($attachment) {
$attachment = $this->attachmentMapper->find($attachmentId);
$data = $this->findDetailsForCard($attachment->getCardId()); $data = $this->findDetailsForCard($attachment->getCardId());
return array_merge($data, ['attachment' => $attachment]); return array_merge($data, ['attachment' => $attachment]);
} }

View File

@@ -43,6 +43,8 @@ use OCA\Deck\Notification\Notifier;
use OCA\Deck\Search\DeckProvider; use OCA\Deck\Search\DeckProvider;
use OCA\Deck\Service\FullTextSearchService; use OCA\Deck\Service\FullTextSearchService;
use OCA\Deck\Service\PermissionService; use OCA\Deck\Service\PermissionService;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\Sharing\Listener;
use OCP\AppFramework\App; use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -62,6 +64,7 @@ use OCP\IServerContainer;
use OCP\IUser; use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager; use OCP\Notification\IManager as NotificationManager;
use OCP\Share\IManager;
use OCP\Util; use OCP\Util;
use Psr\Container\ContainerInterface; 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, 'registerNotifications']));
$context->injectFn(Closure::fromCallable([$this, 'registerFullTextSearch'])); $context->injectFn(Closure::fromCallable([$this, 'registerFullTextSearch']));
$context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources'])); $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 { public function register(IRegistrationContext $context): void {

View File

@@ -50,7 +50,11 @@ class Capabilities implements ICapability {
return [ return [
'deck' => [ 'deck' => [
'version' => $this->appManager->getAppVersion('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 * @NoCSRFRequired
* *
*/ */
public function getAll() { public function getAll($apiVersion) {
$attachment = $this->attachmentService->findAll($this->request->getParam('cardId'), true); $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); return new DataResponse($attachment, HTTP::STATUS_OK);
} }
@@ -53,8 +58,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired * @NoCSRFRequired
* *
*/ */
public function display() { public function display($cardId, $attachmentId, $type = 'deck_file') {
return $this->attachmentService->display($this->request->getParam('attachmentId')); return $this->attachmentService->display($cardId, $attachmentId, $type);
} }
/** /**
@@ -63,8 +68,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired * @NoCSRFRequired
* *
*/ */
public function create($type, $data) { public function create($cardId, $type, $data) {
$attachment = $this->attachmentService->create($this->request->getParam('cardId'), $type, $data); $attachment = $this->attachmentService->create($cardId, $type, $data);
return new DataResponse($attachment, HTTP::STATUS_OK); return new DataResponse($attachment, HTTP::STATUS_OK);
} }
@@ -74,8 +79,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired * @NoCSRFRequired
* *
*/ */
public function update($data) { public function update($cardId, $attachmentId, $data, $type = 'deck_file') {
$attachment = $this->attachmentService->update($this->request->getParam('attachmentId'), $data); $attachment = $this->attachmentService->update($cardId, $attachmentId, $data, $type);
return new DataResponse($attachment, HTTP::STATUS_OK); return new DataResponse($attachment, HTTP::STATUS_OK);
} }
@@ -85,8 +90,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired * @NoCSRFRequired
* *
*/ */
public function delete() { public function delete($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->delete($this->request->getParam('attachmentId')); $attachment = $this->attachmentService->delete($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK); return new DataResponse($attachment, HTTP::STATUS_OK);
} }
@@ -96,8 +101,8 @@ class AttachmentApiController extends ApiController {
* @NoCSRFRequired * @NoCSRFRequired
* *
*/ */
public function restore() { public function restore($cardId, $attachmentId, $type = 'deck_file') {
$attachment = $this->attachmentService->restore($this->request->getParam('attachmentId')); $attachment = $this->attachmentService->restore($cardId, $attachmentId, $type);
return new DataResponse($attachment, HTTP::STATUS_OK); 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 * @return \OCP\AppFramework\Http\Response
* @throws \OCA\Deck\NotFoundException * @throws \OCA\Deck\NotFoundException
*/ */
public function display($attachmentId) { public function display($cardId, $attachmentId) {
return $this->attachmentService->display($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 * @NoAdminRequired
*/ */
public function update($attachmentId) { public function update($cardId, $attachmentId) {
return $this->attachmentService->update($attachmentId, $this->request->getParam('data')); if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data'), $type);
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
*/ */
public function delete($attachmentId) { public function delete($cardId, $attachmentId) {
return $this->attachmentService->delete($attachmentId); if (strpos($attachmentId, ':') === false) {
$type = 'deck_file';
} else {
[$type, $attachmentId] = explode(':', $attachmentId);
}
return $this->attachmentService->delete($cardId, $attachmentId, $type);
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
*/ */
public function restore($attachmentId) { public function restore($cardId, $attachmentId) {
return $this->attachmentService->restore($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\AppInfo\Application;
use OCA\Deck\Service\ConfigService; use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService; use OCA\Deck\Service\PermissionService;
use OCA\Files\Event\LoadSidebar;
use OCA\Viewer\Event\LoadViewer;
use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IInitialStateService; use OCP\IInitialStateService;
use OCP\IRequest; use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
@@ -34,23 +37,24 @@ use OCP\AppFramework\Controller;
class PageController extends Controller { class PageController extends Controller {
private $permissionService; private $permissionService;
private $userId;
private $l10n;
private $initialState; private $initialState;
private $configService; private $configService;
private $eventDispatcher;
public function __construct( public function __construct(
$AppName, $AppName,
IRequest $request, IRequest $request,
PermissionService $permissionService, PermissionService $permissionService,
IInitialStateService $initialStateService, IInitialStateService $initialStateService,
ConfigService $configService ConfigService $configService,
IEventDispatcher $eventDispatcher
) { ) {
parent::__construct($AppName, $request); parent::__construct($AppName, $request);
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
$this->initialState = $initialStateService; $this->initialState = $initialStateService;
$this->configService = $configService; $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, 'canCreate', $this->permissionService->canCreate());
$this->initialState->provideInitialState(Application::APP_ID, 'config', $this->configService->getAll()); $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'); $response = new TemplateResponse('deck', 'main');
if (\OC::$server->getConfig()->getSystemValueBool('debug', false)) { if (\OC::$server->getConfig()->getSystemValueBool('debug', false)) {

View File

@@ -85,6 +85,16 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
return $board; 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 * Find all boards for a given user
* *

View File

@@ -149,7 +149,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function queryCardsByBoards(array $boardIds): IQueryBuilder { public function queryCardsByBoards(array $boardIds): IQueryBuilder {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('c.*') $qb->select('c.*', 's.board_id')
->selectAlias('s.title', 'stack_title')
->from('deck_cards', 'c') ->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id')) ->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))); ->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); 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 { public function delete(Entity $entity): Entity {
// delete assigned labels // delete assigned labels
$this->labelMapper->deleteLabelAssignmentsForCard($entity->getId()); $this->labelMapper->deleteLabelAssignmentsForCard($entity->getId());

View File

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

View File

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

View File

@@ -29,6 +29,7 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager; use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet; use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card; use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl; use OCA\Deck\Db\Acl;
@@ -116,9 +117,20 @@ class CardService {
return $cards; return $cards;
} }
public function search($boardIds, $term) { public function search(string $term, int $limit = null, int $offset = null): array {
$cards = $this->cardMapper->search($boardIds, $term); $boards = $this->boardService->getUserBoards();
return $cards; $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); $card = $this->cardMapper->find($cardId);
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId()); $assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
$attachments = $this->attachmentService->findAll($cardId, true); $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->setAssignedUsers($assignedUsers);
$card->setAttachments($attachments); $card->setAttachments($attachments);
$this->enrich($card); $this->enrich($card);

View File

@@ -32,6 +32,7 @@ use OCA\Deck\NoPermissionException;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroup; use OCP\IGroup;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IUserSession;
class ConfigService { class ConfigService {
public const SETTING_BOARD_NOTIFICATION_DUE_OFF = 'off'; public const SETTING_BOARD_NOTIFICATION_DUE_OFF = 'off';
@@ -46,9 +47,10 @@ class ConfigService {
public function __construct( public function __construct(
IConfig $config, IConfig $config,
IGroupManager $groupManager, 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->groupManager = $groupManager;
$this->config = $config; $this->config = $config;
} }
@@ -148,4 +150,8 @@ class ConfigService {
}, $groups); }, $groups);
return array_filter($groups); return array_filter($groups);
} }
public function getAttachmentFolder(): string {
return $this->config->getUserValue($this->userId, 'deck', 'attachment_folder', '/Deck');
}
} }

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)) { if ($permission === Acl::PERMISSION_SHARE && $this->shareManager->sharingDisabledForUser($this->userId)) {
return false; throw new NoPermissionException('Permission denied');
} }
if ($this->userIsBoardOwner($boardId, $userId)) { 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> <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 }"> <div id="modal-inner" :class="{ 'icon-loading': loading }">
<h3>{{ title }}</h3>
<Multiselect v-model="selectedBoard" <Multiselect v-model="selectedBoard"
:placeholder="t('deck', 'Select a board')" :placeholder="t('deck', 'Select a board')"
:options="boards" :options="boards"
:disabled="loading"
label="title" 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" <Multiselect v-model="selectedCard"
:placeholder="t('deck', 'Select a card')" :placeholder="t('deck', 'Select a card')"
:options="cardsFromBoard" :options="cardsFromBoard"
:disabled="loading || selectedBoard === ''"
label="title" /> label="title" />
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="select"> <button :disabled="!isBoardAndStackChoosen" class="primary" @click="select">
{{ t('deck', 'Link to card') }} {{ action }}
</button> </button>
<button @click="close"> <button @click="close">
{{ t('deck', 'Cancel') }} {{ t('deck', 'Cancel') }}
@@ -56,6 +72,16 @@ export default {
Modal, Modal,
Multiselect, 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() { data() {
return { return {
boards: [], boards: [],
@@ -67,10 +93,7 @@ export default {
}, },
computed: { computed: {
isBoardAndStackChoosen() { isBoardAndStackChoosen() {
if (this.selectedBoard === '' || this.selectedCard === '') { return !(this.selectedBoard === '' || this.selectedCard === '')
return false
}
return true
}, },
}, },
beforeMount() { beforeMount() {
@@ -113,7 +136,12 @@ export default {
width: 90vw; width: 90vw;
max-width: 400px; max-width: 400px;
padding: 20px; padding: 20px;
height: 500px; height: 200px;
}
.multiselect {
width: 100%;
margin-bottom: 10px;
} }
ul { ul {
@@ -129,10 +157,6 @@ export default {
background-color: var(--color-background-dark); background-color: var(--color-background-dark);
} }
li.selected {
border: 1px solid var(--color-primary);
}
.board-bullet { .board-bullet {
display: inline-block; display: inline-block;
width: 12px; width: 12px;
@@ -142,12 +166,11 @@ export default {
cursor: pointer; cursor: pointer;
} }
li > span,
.avatar {
vertical-align: middle;
}
button { button {
float: right; float: right;
} }
.card-selector::v-deep .modal-container {
overflow: visible !important;
}
</style> </style>

View File

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

View File

@@ -140,7 +140,17 @@ export default {
} }
}, },
attachmentUrl() { 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() { formattedFileSize() {
return (filesize) => formatFileSize(filesize) return (filesize) => formatFileSize(filesize)
@@ -169,12 +179,15 @@ export default {
addAttachment(attachment) { addAttachment(attachment) {
const descString = this.$refs.markdownEditor.easymde.value() const descString = this.$refs.markdownEditor.easymde.value()
let embed = '' let embed = ''
if (attachment.extendedData.mimetype.includes('image')) { if ((attachment.type === 'file' && attachment.extendedData.hasPreview) || attachment.extendedData.mimetype.includes('image')) {
embed = '!' embed = '!'
} }
const attachmentString = embed + '[📎 ' + attachment.data + '](' + this.attachmentUrl(attachment) + ')' const attachmentString = embed + '[📎 ' + attachment.data + '](' + this.attachmentPreview(attachment) + ')'
this.$refs.markdownEditor.easymde.value(descString + '\n' + attachmentString) const newContent = descString + '\n' + attachmentString
this.$refs.markdownEditor.easymde.value(newContent)
this.description = newContent
this.modalShow = false this.modalShow = false
this.updateDescription()
}, },
clickedPreview(e) { clickedPreview(e) {
if (e.target.getAttribute('type') === 'checkbox') { if (e.target.getAttribute('type') === 'checkbox') {

View File

@@ -24,9 +24,8 @@ import Vue from 'vue'
import BoardSelector from './BoardSelector' import BoardSelector from './BoardSelector'
import CardSelector from './CardSelector' import CardSelector from './CardSelector'
import './../css/collections.css' import './../css/collections.css'
import FileSharingPicker from './views/FileSharingPicker'
// eslint-disable-next-line // eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken); __webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line // eslint-disable-next-line
@@ -34,7 +33,15 @@ __webpack_public_path__ = OC.linkTo('deck', 'js/');
Vue.prototype.t = t Vue.prototype.t = t
Vue.prototype.n = n 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) { ((function(OCP) {

View File

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

View File

@@ -47,10 +47,10 @@ export class AttachmentApi {
return response.data return response.data
} }
async updateAttachment({ cardId, attachmentId, formData }) { async updateAttachment({ cardId, attachment, formData }) {
const response = await axios({ const response = await axios({
method: 'POST', method: 'POST',
url: this.url(`/cards/${cardId}/attachment/${attachmentId}`), url: this.url(`/cards/${cardId}/attachment/${attachment.type}:${attachment.id}`),
data: formData, data: formData,
}) })
return response.data return response.data
@@ -59,14 +59,14 @@ export class AttachmentApi {
async deleteAttachment(attachment) { async deleteAttachment(attachment) {
await axios({ await axios({
method: 'DELETE', 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) { async restoreAttachment(attachment) {
const response = await axios({ const response = await axios({
method: 'GET', 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 return response.data
} }
@@ -74,7 +74,7 @@ export class AttachmentApi {
async displayAttachment(attachment) { async displayAttachment(attachment) {
const response = await axios({ const response = await axios({
method: 'GET', 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 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 }) { 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) { if (existingIndex !== -1) {
Vue.set(state.attachments[cardId], existingIndex, attachment) Vue.set(state.attachments[cardId], existingIndex, attachment)
} }
}, },
deleteAttachment(state, deletedAttachment) { 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) { if (existingIndex !== -1) {
state.attachments[deletedAttachment.cardId][existingIndex].deletedAt = -1 state.attachments[deletedAttachment.cardId][existingIndex].deletedAt = -1
} }
}, },
restoreAttachment(state, restoredAttachment) { 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) { if (existingIndex !== -1) {
state.attachments[restoredAttachment.cardId][existingIndex].deletedAt = 0 state.attachments[restoredAttachment.cardId][existingIndex].deletedAt = 0
} }
@@ -85,9 +92,9 @@ export default {
commit('cardIncreaseAttachmentCount', cardId) commit('cardIncreaseAttachmentCount', cardId)
}, },
async updateAttachment({ commit }, { cardId, attachmentId, formData }) { async updateAttachment({ commit }, { cardId, attachment, formData }) {
const attachment = await apiClient.updateAttachment({ cardId, attachmentId, formData }) const result = await apiClient.updateAttachment({ cardId, attachment, formData })
commit('updateAttachment', { cardId, attachment }) commit('updateAttachment', { cardId, attachment: result })
}, },
async deleteAttachment({ commit }, attachment) { async deleteAttachment({ commit }, attachment) {
@@ -96,6 +103,12 @@ export default {
commit('cardDecreaseAttachmentCount', attachment.cardId) commit('cardDecreaseAttachmentCount', attachment.cardId)
}, },
async unshareAttachment({ commit }, attachment) {
await apiClient.deleteAttachment(attachment)
commit('unshareAttachment', attachment)
commit('cardDecreaseAttachmentCount', attachment.cardId)
},
async restoreAttachment({ commit }, attachment) { async restoreAttachment({ commit }, attachment) {
const restoredAttachment = await apiClient.restoreAttachment(attachment) const restoredAttachment = await apiClient.restoreAttachment(attachment)
commit('restoreAttachment', restoredAttachment) commit('restoreAttachment', restoredAttachment)

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:
- '%paths.base%/../features/' - '%paths.base%/../features/'
contexts: contexts:
- FeatureContext: - ServerContext:
baseUrl: http://localhost:8080/index.php/ocs/ baseUrl: http://localhost:8080/index.php/ocs/
admin: admin:
- admin - 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 And group "group1" exists
Given user "user1" belongs to group "group1" 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 Scenario: Fetch the board list
Given Logging in using web as "user0" Given Logging in using web as "user0"
When Sending a "GET" to "/index.php/apps/deck/boards" with requesttoken When fetching the board list
Then the HTTP status code should be "200" Then the response should have a status code "200"
And the Content-Type should be "application/json; charset=utf-8" And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of owned board Scenario: Fetch board details of owned board
Given Logging in using web as "admin" Given Logging in using web as "admin"
And creates a board named "MyPrivateAdminBoard" with color "fafafa" And creates a board named "MyPrivateAdminBoard" with color "fafafa"
When "admin" fetches the board named "MyPrivateAdminBoard" When fetches the board named "MyPrivateAdminBoard"
Then the HTTP status code should be "200" Then the response should have a status code "200"
And the Content-Type should be "application/json; charset=utf-8" And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of an other users board Scenario: Fetch board details of an other users board
Given Logging in using web as "admin" Given Logging in using web as "admin"
And creates a board named "MyPrivateAdminBoard" with color "fafafa" And creates a board named "MyPrivateAdminBoard" with color "ff0000"
When "user0" fetches the board named "MyPrivateAdminBoard" Given Logging in using web as "user0"
Then the HTTP status code should be "403" When fetches the board named "MyPrivateAdminBoard"
And the Content-Type should be "application/json; charset=utf-8" 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 Feature: decks
Background: Background:
Given user "admin" exists
Given user "user0" exists Given user "user0" exists
Scenario: Request the main frontend page Scenario: Request the main frontend page
@@ -10,27 +11,21 @@ Feature: decks
Scenario: Fetch the board list Scenario: Fetch the board list
Given Logging in using web as "admin" Given Logging in using web as "admin"
When Sending a "GET" to "/index.php/apps/deck/boards" with requesttoken When fetching the board list
Then the HTTP status code should be "200" Then the response should have a status code "200"
And the Content-Type should be "application/json; charset=utf-8" And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of a nonexisting board Scenario: Fetch board details of a nonexisting board
Given Logging in using web as "admin" Given Logging in using web as "admin"
When Sending a "GET" to "/index.php/apps/deck/boards/13379" with requesttoken When fetching the board with id "99999999"
Then the HTTP status code should be "403" Then the response should have a status code "403"
And the Content-Type should be "application/json; charset=utf-8" And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Create a new board Scenario: Create a new board
Given Logging in using web as "admin" Given Logging in using web as "admin"
When Sending a "POST" to "/index.php/apps/deck/boards" with JSON 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"
"title": "MyBoard",
"color": "000000"
}
"""
Then the HTTP status code should be "200"
And the Content-Type should be "application/json; charset=utf-8"
And the response should be a JSON array with the following mandatory values And the response should be a JSON array with the following mandatory values
|key|value| |key|value|
|title|MyBoard| |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,11 +15,9 @@ INSTALLED=$($OCC status | grep installed: | cut -d " " -f 5)
if [ "$INSTALLED" == "true" ]; then if [ "$INSTALLED" == "true" ]; then
$OCC app:enable deck $OCC app:enable deck
else else
if [ "$SCENARIO_TO_RUN" != "setup_features/setup.feature" ]; then
echo "Nextcloud instance needs to be installed" >&2 echo "Nextcloud instance needs to be installed" >&2
exit 1 exit 1
fi fi
fi
composer install composer install
composer dump-autoload composer dump-autoload
@@ -36,7 +34,7 @@ echo $PHPPID
export TEST_SERVER_URL="http://localhost:$PORT/ocs/" export TEST_SERVER_URL="http://localhost:$PORT/ocs/"
vendor/bin/behat vendor/bin/behat $SCENARIO_TO_RUN
RESULT=$? RESULT=$?
kill $PHPPID kill $PHPPID

View File

@@ -4,18 +4,6 @@
<TypeDoesNotContainType occurrences="1"> <TypeDoesNotContainType occurrences="1">
<code>$message !== null</code> <code>$message !== null</code>
</TypeDoesNotContainType> </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>
<file src="lib/Activity/DeckProvider.php"> <file src="lib/Activity/DeckProvider.php">
<InvalidScalarArgument occurrences="1"> <InvalidScalarArgument occurrences="1">
@@ -28,23 +16,9 @@
</DuplicateClass> </DuplicateClass>
</file> </file>
<file src="lib/AppInfo/Application20.php"> <file src="lib/AppInfo/Application20.php">
<UndefinedClass occurrences="1"> <RedundantCondition occurrences="1">
<code>IBootstrap</code> <code>method_exists($shareManager, 'registerShareProvider')</code>
</UndefinedClass> </RedundantCondition>
</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>
</file> </file>
<file src="lib/Command/UserExport.php"> <file src="lib/Command/UserExport.php">
<ImplementedReturnTypeMismatch occurrences="1"> <ImplementedReturnTypeMismatch occurrences="1">
@@ -90,11 +64,9 @@
</InvalidScalarArgument> </InvalidScalarArgument>
</file> </file>
<file src="lib/Controller/PageController.php"> <file src="lib/Controller/PageController.php">
<MissingDependency occurrences="3"> <UndefinedClass occurrences="1">
<code>Application</code> <code>LoadSidebar</code>
<code>Application</code> </UndefinedClass>
<code>Application</code>
</MissingDependency>
</file> </file>
<file src="lib/Controller/StackApiController.php"> <file src="lib/Controller/StackApiController.php">
<RedundantCondition occurrences="1"> <RedundantCondition occurrences="1">
@@ -104,21 +76,6 @@
<code>Util</code> <code>Util</code>
</UndefinedClass> </UndefinedClass>
</file> </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"> <file src="lib/DAV/Calendar.php">
<UndefinedClass occurrences="1"> <UndefinedClass occurrences="1">
<code>ExternalCalendar</code> <code>ExternalCalendar</code>
@@ -140,18 +97,6 @@
<code>NotFound</code> <code>NotFound</code>
</UndefinedClass> </UndefinedClass>
</file> </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"> <file src="lib/Db/AclMapper.php">
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$aclId</code> <code>$aclId</code>
@@ -161,32 +106,12 @@
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$cardId</code> <code>$cardId</code>
</ParamNameMismatch> </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>
<file src="lib/Db/AttachmentMapper.php"> <file src="lib/Db/AttachmentMapper.php">
<UndefinedMagicMethod occurrences="2">
<code>getCardId</code>
<code>getCardId</code>
</UndefinedMagicMethod>
<UndefinedVariable occurrences="1"> <UndefinedVariable occurrences="1">
<code>$query</code> <code>$query</code>
</UndefinedVariable> </UndefinedVariable>
</file> </file>
<file src="lib/Db/Board.php">
<UndefinedMagicMethod occurrences="1">
<code>getLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/BoardMapper.php"> <file src="lib/Db/BoardMapper.php">
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$boardId</code> <code>$boardId</code>
@@ -194,53 +119,20 @@
<UndefinedClass occurrences="1"> <UndefinedClass occurrences="1">
<code>\OCA\Circles\Api\v1\Circles</code> <code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass> </UndefinedClass>
<UndefinedMagicMethod occurrences="2">
<code>setAcl</code>
<code>setLabels</code>
</UndefinedMagicMethod>
</file> </file>
<file src="lib/Db/Card.php"> <file src="lib/Db/Card.php">
<UndefinedClass occurrences="2"> <UndefinedClass occurrences="2">
<code>VCalendar</code> <code>VCalendar</code>
<code>VCalendar</code> <code>VCalendar</code>
</UndefinedClass> </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>
<file src="lib/Db/CardMapper.php"> <file src="lib/Db/CardMapper.php">
<ImplicitToStringCast occurrences="1">
<code>$qb-&gt;createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)</code>
</ImplicitToStringCast>
<InvalidScalarArgument occurrences="1"> <InvalidScalarArgument occurrences="1">
<code>$entity-&gt;getId()</code> <code>$entity-&gt;getId()</code>
</InvalidScalarArgument> </InvalidScalarArgument>
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$cardId</code> <code>$cardId</code>
</ParamNameMismatch> </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>
<file src="lib/Db/ChangeHelper.php"> <file src="lib/Db/ChangeHelper.php">
<UndefinedThisPropertyAssignment occurrences="3"> <UndefinedThisPropertyAssignment occurrences="3">
@@ -269,69 +161,26 @@
<code>\OCA\Circles\Model\Circle</code> <code>\OCA\Circles\Model\Circle</code>
</UndefinedDocblockClass> </UndefinedDocblockClass>
</file> </file>
<file src="lib/Db/Label.php">
<UndefinedMagicMethod occurrences="1">
<code>getLastModified</code>
</UndefinedMagicMethod>
</file>
<file src="lib/Db/LabelMapper.php"> <file src="lib/Db/LabelMapper.php">
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$labelId</code> <code>$labelId</code>
</ParamNameMismatch> </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>
<file src="lib/Db/Stack.php"> <file src="lib/Db/Stack.php">
<UndefinedClass occurrences="2"> <UndefinedClass occurrences="2">
<code>VCalendar</code> <code>VCalendar</code>
<code>VCalendar</code> <code>VCalendar</code>
</UndefinedClass> </UndefinedClass>
<UndefinedMagicMethod occurrences="2">
<code>getLastModified</code>
<code>getTitle</code>
</UndefinedMagicMethod>
</file> </file>
<file src="lib/Db/StackMapper.php"> <file src="lib/Db/StackMapper.php">
<ParamNameMismatch occurrences="1"> <ParamNameMismatch occurrences="1">
<code>$stackId</code> <code>$stackId</code>
</ParamNameMismatch> </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>
<file src="lib/Notification/NotificationHelper.php"> <file src="lib/Notification/NotificationHelper.php">
<InvalidScalarArgument occurrences="1"> <InvalidScalarArgument occurrences="1">
<code>$board-&gt;getId()</code> <code>$board-&gt;getId()</code>
</InvalidScalarArgument> </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>
<file src="lib/Notification/Notifier.php"> <file src="lib/Notification/Notifier.php">
<RedundantCast occurrences="7"> <RedundantCast occurrences="7">
@@ -348,42 +197,12 @@
<InvalidPropertyAssignmentValue occurrences="1"> <InvalidPropertyAssignmentValue occurrences="1">
<code>[]</code> <code>[]</code>
</InvalidPropertyAssignmentValue> </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>
<file src="lib/Service/AssignmentService.php"> <file src="lib/Service/AssignmentService.php">
<InvalidScalarArgument occurrences="2"> <InvalidScalarArgument occurrences="2">
<code>$cardId</code> <code>$cardId</code>
<code>$cardId</code> <code>$cardId</code>
</InvalidScalarArgument> </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"> <UndefinedThisPropertyAssignment occurrences="1">
<code>$this-&gt;currentUser</code> <code>$this-&gt;currentUser</code>
</UndefinedThisPropertyAssignment> </UndefinedThisPropertyAssignment>
@@ -391,86 +210,11 @@
<code>$this-&gt;currentUser</code> <code>$this-&gt;currentUser</code>
</UndefinedThisPropertyFetch> </UndefinedThisPropertyFetch>
</file> </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"> <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"> <TooManyArguments occurrences="2">
<code>findAll</code> <code>findAll</code>
<code>findAll</code> <code>findAll</code>
</TooManyArguments> </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>
<file src="lib/Service/CardService.php"> <file src="lib/Service/CardService.php">
<TooFewArguments occurrences="1"> <TooFewArguments occurrences="1">
@@ -479,52 +223,6 @@
<UndefinedDocblockClass occurrences="1"> <UndefinedDocblockClass occurrences="1">
<code>\OCP\AppFramework\Db\</code> <code>\OCP\AppFramework\Db\</code>
</UndefinedDocblockClass> </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>
<file src="lib/Service/CirclesService.php"> <file src="lib/Service/CirclesService.php">
<UndefinedClass occurrences="2"> <UndefinedClass occurrences="2">
@@ -533,13 +231,6 @@
</UndefinedClass> </UndefinedClass>
</file> </file>
<file src="lib/Service/CommentService.php"> <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"> <UndefinedThisPropertyAssignment occurrences="2">
<code>$this-&gt;cardMapper</code> <code>$this-&gt;cardMapper</code>
<code>$this-&gt;permissionService</code> <code>$this-&gt;permissionService</code>
@@ -555,25 +246,7 @@
<code>$this-&gt;permissionService</code> <code>$this-&gt;permissionService</code>
</UndefinedThisPropertyFetch> </UndefinedThisPropertyFetch>
</file> </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"> <file src="lib/Service/DefaultBoardService.php">
<MissingDependency occurrences="2">
<code>Application</code>
<code>Application</code>
</MissingDependency>
<TypeDoesNotContainNull occurrences="6"> <TypeDoesNotContainNull occurrences="6">
<code>$color === false || $color === null</code> <code>$color === false || $color === null</code>
<code>$color === null</code> <code>$color === null</code>
@@ -597,94 +270,46 @@
<code>is_resource($content)</code> <code>is_resource($content)</code>
<code>is_resource($content)</code> <code>is_resource($content)</code>
</RedundantCondition> </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>
<file src="lib/Service/FullTextSearchService.php"> <file src="lib/Service/FilesAppService.php">
<UndefinedClass occurrences="2"> <MissingDependency occurrences="4">
<code>DocumentAccess</code> <code>$this-&gt;rootFolder</code>
<code>IndexDocument</code> <code>$this-&gt;rootFolder</code>
</UndefinedClass> <code>IRootFolder</code>
<UndefinedMagicMethod occurrences="4"> <code>Share\Exceptions\ShareNotFound</code>
<code>getDescription</code> </MissingDependency>
<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> </file>
<file src="lib/Service/PermissionService.php"> <file src="lib/Service/PermissionService.php">
<UndefinedClass occurrences="2"> <UndefinedClass occurrences="2">
<code>\OCA\Circles\Api\v1\Circles</code> <code>\OCA\Circles\Api\v1\Circles</code>
<code>\OCA\Circles\Api\v1\Circles</code> <code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass> </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>
<file src="lib/Service/StackService.php"> <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"> <UndefinedClass occurrences="1">
<code>BadRquestException</code> <code>BadRquestException</code>
</UndefinedClass> </UndefinedClass>
<UndefinedMagicMethod occurrences="14"> </file>
<code>getBoardId</code> <file src="lib/Sharing/DeckShareProvider.php">
<code>getBoardId</code> <InvalidReturnStatement occurrences="1">
<code>getBoardId</code> <code>$shares</code>
<code>getBoardId</code> </InvalidReturnStatement>
<code>getOrder</code> <InvalidReturnType occurrences="1">
<code>setBoardId</code> <code>getSharesInFolder</code>
<code>setBoardId</code> </InvalidReturnType>
<code>setCards</code> <MissingDependency occurrences="7">
<code>setDeletedAt</code> <code>GenericShareException</code>
<code>setDeletedAt</code> <code>GenericShareException</code>
<code>setOrder</code> <code>ShareNotFound</code>
<code>setOrder</code> <code>ShareNotFound</code>
<code>setTitle</code> <code>ShareNotFound</code>
<code>setTitle</code> <code>ShareNotFound</code>
</UndefinedMagicMethod> <code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Sharing/Listener.php">
<InvalidArgument occurrences="1">
<code>[self::class, 'listenPreShare']</code>
</InvalidArgument>
</file> </file>
</files> </files>

View File

@@ -316,10 +316,6 @@ class ActivityManagerTest extends TestCase {
$stack->setBoardId(999); $stack->setBoardId(999);
$board = new Board(); $board = new Board();
$board->setId(999); $board->setId(999);
$this->attachmentMapper->expects($this->once())
->method('find')
->with(777)
->willReturn($attachment);
$this->cardMapper->expects($this->once()) $this->cardMapper->expects($this->once())
->method('find') ->method('find')
->with(555) ->with(555)
@@ -340,7 +336,7 @@ class ActivityManagerTest extends TestCase {
'archived' => $card->getArchived() 'archived' => $card->getArchived()
], ],
'attachment' => $attachment 'attachment' => $attachment
], $this->invokePrivate($this->activityManager, 'findDetailsForAttachment', [777])); ], $this->invokePrivate($this->activityManager, 'findDetailsForAttachment', [$attachment]));
} }
public function invokePrivate(&$object, $methodName, array $parameters = []) { public function invokePrivate(&$object, $methodName, array $parameters = []) {

View File

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

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