From 3cb278f77f43a3c9f3bbad74af0d65feffe81144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sun, 16 Feb 2020 14:30:11 +0100 Subject: [PATCH 01/15] Add dav plugin to expose calendars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/info.xml | 6 +- lib/DAV/Calendar.php | 241 +++++++++++++++++++++++++++++++++++++ lib/DAV/CalendarObject.php | 152 +++++++++++++++++++++++ lib/DAV/CalendarPlugin.php | 80 ++++++++++++ lib/Db/Card.php | 14 +++ 5 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 lib/DAV/Calendar.php create mode 100644 lib/DAV/CalendarObject.php create mode 100644 lib/DAV/CalendarPlugin.php diff --git a/appinfo/info.xml b/appinfo/info.xml index ec9226b9d..db43d3f4c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -64,7 +64,11 @@ OCA\Deck\Activity\DeckProvider - + + + OCA\Deck\DAV\CalendarPlugin + + OCA\Deck\Provider\DeckProvider diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php new file mode 100644 index 000000000..5fb7ee652 --- /dev/null +++ b/lib/DAV/Calendar.php @@ -0,0 +1,241 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\Deck\DAV; + +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use OCA\DAV\CalDAV\Plugin; +use OCA\DAV\DAV\Sharing\IShareable; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\Card; +use OCA\Deck\Service\CardService; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; +use Sabre\DAV\PropPatch; + +class Calendar extends ExternalCalendar implements IShareable { + + /** @var string */ + private $principalUri; + + /** @var string */ + private $calendarUri; + + /** @var string[] */ + private $children; + /** + * @var \stdClass + */ + private $cardService; + + /** + * Calendar constructor. + * + * @param string $principalUri + * @param string $calendarUri + */ + public function __construct(string $principalUri, string $calendarUri, Board $board = null) { + parent::__construct('deck', $calendarUri); + + $this->board = $board; + + $this->principalUri = $principalUri; + $this->calendarUri = $calendarUri; + + + if ($board) { + /** @var CardService cardService */ + $cardService = \OC::$server->query(CardService::class); + $this->children = $cardService->findCalendarEntries($board->getId()); + } else { + $this->children = []; + } + } + + + /** + * @inheritDoc + */ + function getOwner() { + return $this->principalUri; + } + + /** + * @inheritDoc + */ + function getACL() { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', + 'protected' => true, + ], + ]; + } + + /** + * @inheritDoc + */ + function setACL(array $acl) { + throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + } + + /** + * @inheritDoc + */ + function getSupportedPrivilegeSet() { + return null; + } + + /** + * @inheritDoc + */ + function calendarQuery(array $filters) { + // In a real implementation this should actually filter + return array_map(function (Card $card) { + return $card->getId() . '.ics'; + }, $this->children); + } + + /** + * @inheritDoc + */ + function createFile($name, $data = null) { + return null; + } + + /** + * @inheritDoc + */ + function getChild($name) { + if ($this->childExists($name)) { + $card = array_values(array_filter( + $this->children, + function ($card) use (&$name) { + return $card->getId() . '.ics' === $name; + } + )); + if (count($card) > 0) { + return new CalendarObject($this, $name, $card[0]); + } + } + } + + /** + * @inheritDoc + */ + function getChildren() { + $childNames = array_map(function (Card $card) { + return $card->getId() . '.ics'; + }, $this->children); + + $children = []; + + foreach ($childNames as $name) { + $children[] = $this->getChild($name); + } + + return $children; + } + + /** + * @inheritDoc + */ + function childExists($name) { + return count(array_filter( + $this->children, + function ($card) use (&$name) { + return $card->getId() . '.ics' === $name; + } + )) > 0; + } + + /** + * @inheritDoc + */ + function delete() { + return null; + } + + /** + * @inheritDoc + */ + function getLastModified() { + return $this->board->getLastModified(); + } + + /** + * @inheritDoc + */ + function getGroup() { + return []; + } + + /** + * @inheritDoc + */ + function propPatch(PropPatch $propPatch) { + // We can just return here and let oc_properties handle everything + } + + /** + * @inheritDoc + */ + function getProperties($properties) { + // A backend should provide at least minimum properties + return [ + '{DAV:}displayname' => 'Deck: ' . ($this->board ? $this->board->getTitle() : 'no board object provided'), + '{http://apple.com/ns/ical/}calendar-color' => '#' . $this->board->getColor(), + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + } + + /** + * @inheritDoc + */ + function updateShares(array $add, array $remove) { + // TODO: Implement updateShares() method. + } + + /** + * @inheritDoc + */ + function getShares() { + return []; + } + + /** + * @inheritDoc + */ + public function getResourceId() { + // TODO: Implement getResourceId() method. + } +} diff --git a/lib/DAV/CalendarObject.php b/lib/DAV/CalendarObject.php new file mode 100644 index 000000000..0dc0c78aa --- /dev/null +++ b/lib/DAV/CalendarObject.php @@ -0,0 +1,152 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\Deck\DAV; + +use OCA\Deck\Db\Card; +use OCA\Deck\Service\CardService; +use Sabre\VObject\Component\VCalendar; + +class CalendarObject implements \Sabre\CalDAV\ICalendarObject, \Sabre\DAVACL\IACL { + + /** @var Calendar */ + private $calendar; + + /** @var string */ + private $name; + /** + * @var Card + */ + private $card; + + /** + * CalendarObject constructor. + * + * @param Calendar $calendar + * @param string $name + */ + public function __construct(Calendar $calendar, string $name, Card $card = null) { + $this->calendar = $calendar; + $this->name = $name; + $this->card = $card; + } + + /** + * @inheritDoc + */ + function getOwner() { + return null; + } + + /** + * @inheritDoc + */ + function getGroup() { + return null; + } + + /** + * @inheritDoc + */ + function getACL() { + return $this->calendar->getACL(); + } + + /** + * @inheritDoc + */ + function setACL(array $acl) { + throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + } + + /** + * @inheritDoc + */ + function getSupportedPrivilegeSet() { + return null; + } + + /** + * @inheritDoc + */ + function put($data) { + throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + } + + /** + * @inheritDoc + */ + function get() { + if ($this->card) { + return $this->card->getCalendarObject()->serialize(); + } + } + + /** + * @inheritDoc + */ + function getContentType() { + return 'text/calendar; charset=utf-8'; + } + + /** + * @inheritDoc + */ + function getETag() { + return '"' . md5($this->get()) . '"'; + } + + /** + * @inheritDoc + */ + function getSize() { + return strlen($this->get()); + } + + /** + * @inheritDoc + */ + function delete() { + throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + } + + /** + * @inheritDoc + */ + function getName() { + return $this->name; + } + + /** + * @inheritDoc + */ + function setName($name) { + throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + } + + /** + * @inheritDoc + */ + function getLastModified() { + return $this->card->getLastModified(); + } +} diff --git a/lib/DAV/CalendarPlugin.php b/lib/DAV/CalendarPlugin.php new file mode 100644 index 000000000..ba5226827 --- /dev/null +++ b/lib/DAV/CalendarPlugin.php @@ -0,0 +1,80 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ + +namespace OCA\Deck\DAV; + +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use OCA\DAV\CalDAV\Integration\ICalendarProvider; +use OCA\Deck\Db\Board; +use OCA\Deck\Service\BoardService; + +class CalendarPlugin implements ICalendarProvider { + + /** + * @var BoardService + */ + private $boardService; + + public function __construct(BoardService $boardService) { + $this->boardService = $boardService; + } + + /** + * @inheritDoc + */ + public function getAppId(): string { + return 'deck'; + } + + /** + * @inheritDoc + */ + public function fetchAllForCalendarHome(string $principalUri): array { + $boards = $this->boardService->findAll(); + return array_map(function (Board $board) use ($principalUri) { + return new Calendar($principalUri, 'board-' . $board->getId(), $board); + }, $boards); + } + + /** + * @inheritDoc + */ + public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool { + $boards = array_map(function(Board $board) { + return 'board-' . $board->getId(); + }, $this->boardService->findAll()); + return in_array($calendarUri, $boards, true); + } + + /** + * @inheritDoc + */ + public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar { + if ($this->hasCalendarInCalendarHome($principalUri, $calendarUri)) { + $board = $this->boardService->find(str_replace('board-', '', $calendarUri)); + return new Calendar($principalUri, $calendarUri, $board); + } + + return null; + } +} diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 36faa3dd6..107c253fd 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -24,6 +24,7 @@ namespace OCA\Deck\Db; use DateTime; +use Sabre\VObject\Component\VCalendar; class Card extends RelationalEntity { protected $title; @@ -117,4 +118,17 @@ public function jsonSerialize() { unset($json['descriptionPrev']); return $json; } + + public function getCalendarObject(): VCalendar { + $calendar = new VCalendar(); + $event = $calendar->createComponent('VEVENT'); + $event->UID = 'deck-cardevent' . $this->getId() . '@example.com'; + $event->DTSTAMP = new \DateTime($this->getDuedate()); + $event->DTSTART = new \DateTime($this->getDuedate()); + $event->DTEND = new \DateTime($this->getDuedate()); + $event->SUMMARY = $this->getTitle(); + $calendar->add($event); + return $calendar; + } + } From fc58439d2e8549b3af9a19eb8d7c44f36f1135fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sun, 16 Feb 2020 14:30:50 +0100 Subject: [PATCH 02/15] Migrate CardMapper to query builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 93 ++++++++++++++++++++++--------------- lib/Db/DeckMapper.php | 3 +- lib/Service/CardService.php | 6 +++ 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 453b987cb..a929bec3b 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -23,7 +23,9 @@ namespace OCA\Deck\Db; +use Exception; use OCP\AppFramework\Db\Entity; + use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -81,16 +83,20 @@ public function update(Entity $entity, $updateModified = true): Entity { // make sure we only reset the notification flag if the duedate changes if (in_array('duedate', $entity->getUpdatedFields(), true)) { - $existing = $this->find($entity->getId()); - if ($existing->getDuedate() !== $entity->getDuedate()) { - $entity->setNotified(false); + /** @var Card $existing */ + try { + $existing = $this->find($entity->getId()); + if ($existing && $entity->getDuedate() !== $existing->getDuedate()) { + $entity->setNotified(false); + } + // remove pending notifications + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp('deck') + ->setObject('card', $entity->getId()); + $this->notificationManager->markProcessed($notification); + } catch (Exception $e) { } - // remove pending notifications - $notification = $this->notificationManager->createNotification(); - $notification - ->setApp('deck') - ->setObject('card', $entity->getId()); - $this->notificationManager->markProcessed($notification); } return parent::update($entity); } @@ -102,19 +108,13 @@ public function markNotified(Card $card): Entity { return parent::update($cardUpdate); } - /** - * @param $id - * @return RelationalEntity if not found - * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException - * @throws \OCP\AppFramework\Db\DoesNotExistException - */ - public function find($id): Entity { + public function find($id): Card { $qb = $this->db->getQueryBuilder(); - $qb->select('*')->from('deck_cards') + $qb->select('*') + ->from('deck_cards') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->orderBy('order') ->addOrderBy('id'); - /** @var Card $card */ $card = $this->findEntity($qb); $labels = $this->labelMapper->findAssignedLabelsForCard($card->id); @@ -131,10 +131,9 @@ public function findAll($stackId, $limit = null, $offset = null, $since = -1) { ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT))) + ->orderBy('order', 'id') ->setMaxResults($limit) - ->setFirstResult($offset) - ->orderBy('order') - ->addOrderBy('id'); + ->setFirstResult($offset); return $this->findEntities($qb); } @@ -153,17 +152,35 @@ public function queryCardsByBoards(array $boardIds): IQueryBuilder { ->from('deck_cards', 'c') ->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id')) ->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY))); - return $qb; } public function findDeleted($boardId, $limit = null, $offset = null) { - $qb = $this->queryCardsByBoard($boardId); - $qb->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_cards', 'c') + ->join('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id')) + ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId))) + ->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(true))) + ->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0))) + ->orderBy('c.order') ->setMaxResults($limit) - ->setFirstResult($offset) - ->orderBy('order') - ->addOrderBy('id'); + ->setFirstResult($offset); + return $this->findEntities($qb); + } + + public function findCalendarEntries($boardId, $limit = null, $offset = null) { + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->from('deck_cards', 'c') + ->join('c', 'deck_stacks', 's', 's.id = c.stack_id') + ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId))) + ->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(true))) + ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter('0'))) + ->andWhere($qb->expr()->isNotNull('c.duedate')) + ->orderBy('c.duedate') + ->setMaxResults($limit) + ->setFirstResult($offset); return $this->findEntities($qb); } @@ -278,19 +295,21 @@ public function deleteByStack($stackId) { } public function assignLabel($card, $label) { - $sql = 'INSERT INTO `*PREFIX*deck_assigned_labels` (`label_id`,`card_id`) VALUES (?,?)'; - $stmt = $this->db->prepare($sql); - $stmt->bindParam(1, $label, \PDO::PARAM_INT); - $stmt->bindParam(2, $card, \PDO::PARAM_INT); - $stmt->execute(); + $qb = $this->db->getQueryBuilder(); + $qb->insert('deck_assigned_labels') + ->values([ + 'label_id' => $qb->createNamedParameter($label, IQueryBuilder::PARAM_INT), + 'card_id' => $qb->createNamedParameter($card, IQueryBuilder::PARAM_INT), + ]); + $qb->execute(); } public function removeLabel($card, $label) { - $sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE card_id = ? AND label_id = ?'; - $stmt = $this->db->prepare($sql); - $stmt->bindParam(1, $card, \PDO::PARAM_INT); - $stmt->bindParam(2, $label, \PDO::PARAM_INT); - $stmt->execute(); + $qb = $this->db->getQueryBuilder(); + $qb->delete('deck_assigned_labels') + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($card))) + ->andWhere($qb->expr()->eq('label_id', $qb->createNamedParameter($label))); + $qb->execute(); } public function isOwner($userId, $cardId) { diff --git a/lib/Db/DeckMapper.php b/lib/Db/DeckMapper.php index e3a9b4a05..2dd0dd990 100644 --- a/lib/Db/DeckMapper.php +++ b/lib/Db/DeckMapper.php @@ -29,10 +29,11 @@ * Class DeckMapper * * @package OCA\Deck\Db + * @deprecated use QBMapper * * TODO: Move to QBMapper once Nextcloud 14 is a minimum requirement */ -abstract class DeckMapper extends Mapper { +class DeckMapper extends Mapper { /** * @param $id diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 50504bc07..31df0c48b 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -144,6 +144,12 @@ public function find($cardId) { return $card; } + public function findCalendarEntries($boardId) { + $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); + + return $this->cardMapper->findCalendarEntries($boardId); + } + /** * @param $title * @param $stackId From 08097ea65f3341c16dafd830b0cdedefe6043e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sat, 7 Mar 2020 11:48:35 +0100 Subject: [PATCH 03/15] Map stacks to VTODO and link them as parent entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/DAV/Calendar.php | 23 +++++++++++++++-------- lib/DAV/CalendarObject.php | 12 ++++++------ lib/Db/Card.php | 22 ++++++++++++++++++++-- lib/Db/CardMapper.php | 2 -- lib/Db/Stack.php | 16 ++++++++++++++++ lib/Service/CardService.php | 7 +++++-- lib/Service/StackService.php | 5 +++++ 7 files changed, 67 insertions(+), 20 deletions(-) diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php index 5fb7ee652..8563c3679 100644 --- a/lib/DAV/Calendar.php +++ b/lib/DAV/Calendar.php @@ -27,7 +27,9 @@ use OCA\DAV\DAV\Sharing\IShareable; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; +use OCA\Deck\Db\Stack; use OCA\Deck\Service\CardService; +use OCA\Deck\Service\StackService; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV\PropPatch; @@ -62,9 +64,14 @@ public function __construct(string $principalUri, string $calendarUri, Board $bo if ($board) { - /** @var CardService cardService */ + /** @var CardService $cardService */ $cardService = \OC::$server->query(CardService::class); - $this->children = $cardService->findCalendarEntries($board->getId()); + /** @var StackService $stackService */ + $stackService = \OC::$server->query(StackService::class); + $this->children = array_merge( + $cardService->findCalendarEntries($board->getId()), + $stackService->findCalendarEntries($board->getId()) + ); } else { $this->children = []; } @@ -120,8 +127,8 @@ function getSupportedPrivilegeSet() { */ function calendarQuery(array $filters) { // In a real implementation this should actually filter - return array_map(function (Card $card) { - return $card->getId() . '.ics'; + return array_map(function ($card) { + return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics'; }, $this->children); } @@ -140,7 +147,7 @@ function getChild($name) { $card = array_values(array_filter( $this->children, function ($card) use (&$name) { - return $card->getId() . '.ics' === $name; + return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics' === $name; } )); if (count($card) > 0) { @@ -153,8 +160,8 @@ function ($card) use (&$name) { * @inheritDoc */ function getChildren() { - $childNames = array_map(function (Card $card) { - return $card->getId() . '.ics'; + $childNames = array_map(function ($card) { + return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics'; }, $this->children); $children = []; @@ -173,7 +180,7 @@ function childExists($name) { return count(array_filter( $this->children, function ($card) use (&$name) { - return $card->getId() . '.ics' === $name; + return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics' === $name; } )) > 0; } diff --git a/lib/DAV/CalendarObject.php b/lib/DAV/CalendarObject.php index 0dc0c78aa..82633dd30 100644 --- a/lib/DAV/CalendarObject.php +++ b/lib/DAV/CalendarObject.php @@ -36,7 +36,7 @@ class CalendarObject implements \Sabre\CalDAV\ICalendarObject, \Sabre\DAVACL\IAC /** * @var Card */ - private $card; + private $sourceItem; /** * CalendarObject constructor. @@ -44,10 +44,10 @@ class CalendarObject implements \Sabre\CalDAV\ICalendarObject, \Sabre\DAVACL\IAC * @param Calendar $calendar * @param string $name */ - public function __construct(Calendar $calendar, string $name, Card $card = null) { + public function __construct(Calendar $calendar, string $name, $sourceItem = null) { $this->calendar = $calendar; $this->name = $name; - $this->card = $card; + $this->sourceItem = $sourceItem; } /** @@ -96,8 +96,8 @@ function put($data) { * @inheritDoc */ function get() { - if ($this->card) { - return $this->card->getCalendarObject()->serialize(); + if ($this->sourceItem) { + return $this->sourceItem->getCalendarObject()->serialize(); } } @@ -147,6 +147,6 @@ function setName($name) { * @inheritDoc */ function getLastModified() { - return $this->card->getLastModified(); + return $this->sourceItem->getLastModified(); } } diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 107c253fd..f91092a84 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -121,14 +121,32 @@ public function jsonSerialize() { public function getCalendarObject(): VCalendar { $calendar = new VCalendar(); - $event = $calendar->createComponent('VEVENT'); - $event->UID = 'deck-cardevent' . $this->getId() . '@example.com'; + $event = $calendar->createComponent('VTODO'); + $event->UID = 'deck-card-' . $this->getId(); $event->DTSTAMP = new \DateTime($this->getDuedate()); $event->DTSTART = new \DateTime($this->getDuedate()); $event->DTEND = new \DateTime($this->getDuedate()); + $event->add('RELATED-TO', 'deck-stack-' . $this->getStackId()); + + // For write support: CANCELLED / IN-PROCESS handling + $event->STATUS = $this->getArchived() ? "COMPLETED" : "NEEDS-ACTION"; + if ($this->getArchived()) { + $date = new DateTime(); + $date->setTimestamp($this->getLastModified()); + $event->COMPLETED = $date; + } + if (count($this->getLabels()) > 0) { + $event->CATEGORIES = array_map(function ($label) { + return $label->getTitle(); + }, $this->getLabels()); + } $event->SUMMARY = $this->getTitle(); $calendar->add($event); return $calendar; } + public function getCalendarPrefix(): string { + return 'card'; + } + } diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index a929bec3b..549aadfe0 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -175,9 +175,7 @@ public function findCalendarEntries($boardId, $limit = null, $offset = null) { ->from('deck_cards', 'c') ->join('c', 'deck_stacks', 's', 's.id = c.stack_id') ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId))) - ->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(true))) ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter('0'))) - ->andWhere($qb->expr()->isNotNull('c.duedate')) ->orderBy('c.duedate') ->setMaxResults($limit) ->setFirstResult($offset); diff --git a/lib/Db/Stack.php b/lib/Db/Stack.php index 85d9c4553..7dc9310ba 100644 --- a/lib/Db/Stack.php +++ b/lib/Db/Stack.php @@ -23,6 +23,8 @@ namespace OCA\Deck\Db; +use Sabre\VObject\Component\VCalendar; + class Stack extends RelationalEntity { protected $title; protected $boardId; @@ -50,4 +52,18 @@ public function jsonSerialize() { } return $json; } + + public function getCalendarObject(): VCalendar { + $calendar = new VCalendar(); + $event = $calendar->createComponent('VTODO'); + $event->UID = 'deck-stack-' . $this->getId(); + $event->SUMMARY = '[Stack]: ' . $this->getTitle(); + $calendar->add($event); + return $calendar; + } + + public function getCalendarPrefix(): string { + return 'stack'; + } + } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 31df0c48b..bd9c11e0c 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -146,8 +146,11 @@ public function find($cardId) { public function findCalendarEntries($boardId) { $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); - - return $this->cardMapper->findCalendarEntries($boardId); + $cards = $this->cardMapper->findCalendarEntries($boardId); + foreach ($cards as $card) { + $this->enrich($card); + } + return $cards; } /** diff --git a/lib/Service/StackService.php b/lib/Service/StackService.php index 7df627e06..a31d13854 100644 --- a/lib/Service/StackService.php +++ b/lib/Service/StackService.php @@ -146,6 +146,11 @@ public function findAll($boardId, $since = -1) { return $stacks; } + public function findCalendarEntries($boardId) { + $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ); + return $this->stackMapper->findAll($boardId); + } + public function fetchDeleted($boardId) { $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); $stacks = $this->stackMapper->findDeleted($boardId); From a67a80f6a7418beb1f41d0bb3ee20eaec43a513f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sat, 7 Mar 2020 12:16:57 +0100 Subject: [PATCH 04/15] Map assigned users to ATTENDEE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/Card.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Db/Card.php b/lib/Db/Card.php index f91092a84..8870202fd 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -128,7 +128,7 @@ public function getCalendarObject(): VCalendar { $event->DTEND = new \DateTime($this->getDuedate()); $event->add('RELATED-TO', 'deck-stack-' . $this->getStackId()); - // For write support: CANCELLED / IN-PROCESS handling + // FIXME: For write support: CANCELLED / IN-PROCESS handling $event->STATUS = $this->getArchived() ? "COMPLETED" : "NEEDS-ACTION"; if ($this->getArchived()) { $date = new DateTime(); @@ -140,6 +140,13 @@ public function getCalendarObject(): VCalendar { return $label->getTitle(); }, $this->getLabels()); } + foreach ($this->getAssignedUsers() as $user) { + $participant = $user->resolveParticipant(); + // FIXME use proper uri + $event->add('ATTENDEE', 'https://localhost/remote.php/dav/principals/users/:' . $participant->getUID(), [ 'CN' => $participant->getDisplayName()]); + } + + $event->SUMMARY = $this->getTitle(); $calendar->add($event); return $calendar; From f4201392f6e6c76ca6bd84cc517f3758fb18e8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sun, 14 Jun 2020 14:49:46 +0200 Subject: [PATCH 05/15] Fix VTODO and issues during rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/Card.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 8870202fd..0b157fd56 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -123,9 +123,12 @@ public function getCalendarObject(): VCalendar { $calendar = new VCalendar(); $event = $calendar->createComponent('VTODO'); $event->UID = 'deck-card-' . $this->getId(); - $event->DTSTAMP = new \DateTime($this->getDuedate()); - $event->DTSTART = new \DateTime($this->getDuedate()); - $event->DTEND = new \DateTime($this->getDuedate()); + if ($this->getDuedate()) { + $event->DTSTAMP = new \DateTime(); + $event->DTSTART = new \DateTime($this->getDuedate()); + $event->DTEND = new \DateTime($this->getDuedate()); + $event->DURATION = "PT1H"; + } $event->add('RELATED-TO', 'deck-stack-' . $this->getStackId()); // FIXME: For write support: CANCELLED / IN-PROCESS handling @@ -134,6 +137,7 @@ public function getCalendarObject(): VCalendar { $date = new DateTime(); $date->setTimestamp($this->getLastModified()); $event->COMPLETED = $date; + //$event->add('PERCENT-COMPLETE', 100); } if (count($this->getLabels()) > 0) { $event->CATEGORIES = array_map(function ($label) { @@ -142,7 +146,7 @@ public function getCalendarObject(): VCalendar { } foreach ($this->getAssignedUsers() as $user) { $participant = $user->resolveParticipant(); - // FIXME use proper uri + // FIXME use proper uri $event->add('ATTENDEE', 'https://localhost/remote.php/dav/principals/users/:' . $participant->getUID(), [ 'CN' => $participant->getDisplayName()]); } From 42640e242838c00ccc8de82df578e6e54af55860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 20 Aug 2020 21:37:48 +0200 Subject: [PATCH 06/15] Some more restructuring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/DAV/Calendar.php | 177 ++++++++++++-------------------- lib/DAV/CalendarObject.php | 110 ++++++-------------- lib/DAV/CalendarPlugin.php | 36 ++----- lib/DAV/DeckCalendarBackend.php | 84 +++++++++++++++ lib/Db/Card.php | 1 - lib/Db/Stack.php | 1 - 6 files changed, 192 insertions(+), 217 deletions(-) create mode 100644 lib/DAV/DeckCalendarBackend.php diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php index 8563c3679..6c97e8446 100644 --- a/lib/DAV/Calendar.php +++ b/lib/DAV/Calendar.php @@ -24,72 +24,44 @@ use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Plugin; -use OCA\DAV\DAV\Sharing\IShareable; +use OCA\Deck\Db\Acl; use OCA\Deck\Db\Board; -use OCA\Deck\Db\Card; -use OCA\Deck\Db\Stack; -use OCA\Deck\Service\CardService; -use OCA\Deck\Service\StackService; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; +use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\PropPatch; -class Calendar extends ExternalCalendar implements IShareable { +class Calendar extends ExternalCalendar { /** @var string */ private $principalUri; - - /** @var string */ - private $calendarUri; - /** @var string[] */ private $children; - /** - * @var \stdClass - */ - private $cardService; + /** @var DeckCalendarBackend */ + private $backend; + /** @var Board */ + private $board; - /** - * Calendar constructor. - * - * @param string $principalUri - * @param string $calendarUri - */ - public function __construct(string $principalUri, string $calendarUri, Board $board = null) { + public function __construct(string $principalUri, string $calendarUri, Board $board, DeckCalendarBackend $backend) { parent::__construct('deck', $calendarUri); + $this->backend = $backend; $this->board = $board; $this->principalUri = $principalUri; - $this->calendarUri = $calendarUri; - if ($board) { - /** @var CardService $cardService */ - $cardService = \OC::$server->query(CardService::class); - /** @var StackService $stackService */ - $stackService = \OC::$server->query(StackService::class); - $this->children = array_merge( - $cardService->findCalendarEntries($board->getId()), - $stackService->findCalendarEntries($board->getId()) - ); + $this->children = $this->backend->getChildren($board->getId()); } else { $this->children = []; } } - - /** - * @inheritDoc - */ - function getOwner() { + public function getOwner() { return $this->principalUri; } - /** - * @inheritDoc - */ - function getACL() { - return [ + public function getACL() { + $acl = [ [ 'privilege' => '{DAV:}read', 'principal' => $this->getOwner(), @@ -106,43 +78,41 @@ function getACL() { 'protected' => true, ], ]; + if ($this->backend->checkBoardPermission($this->board->getId(), Acl::PERMISSION_EDIT)) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + } + return $acl; } - /** - * @inheritDoc - */ - function setACL(array $acl) { - throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + public function setACL(array $acl) { + throw new Forbidden('Setting ACL is not supported on this node'); } - /** - * @inheritDoc - */ - function getSupportedPrivilegeSet() { + public function getSupportedPrivilegeSet() { return null; } - /** - * @inheritDoc - */ - function calendarQuery(array $filters) { - // In a real implementation this should actually filter + public function calendarQuery(array $filters) { + // FIXME: In a real implementation this should actually filter return array_map(function ($card) { return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics'; }, $this->children); } - /** - * @inheritDoc - */ - function createFile($name, $data = null) { - return null; + public function createFile($name, $data = null) { + throw new \Sabre\DAV\Exception\Forbidden('Creating a new entry is not implemented'); } - /** - * @inheritDoc - */ - function getChild($name) { + public function getChild($name) { if ($this->childExists($name)) { $card = array_values(array_filter( $this->children, @@ -151,15 +121,12 @@ function ($card) use (&$name) { } )); if (count($card) > 0) { - return new CalendarObject($this, $name, $card[0]); + return new CalendarObject($this, $name, $card[0], $this->backend); } } } - /** - * @inheritDoc - */ - function getChildren() { + public function getChildren() { $childNames = array_map(function ($card) { return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics'; }, $this->children); @@ -173,10 +140,7 @@ function getChildren() { return $children; } - /** - * @inheritDoc - */ - function childExists($name) { + public function childExists($name) { return count(array_filter( $this->children, function ($card) use (&$name) { @@ -185,64 +149,51 @@ function ($card) use (&$name) { )) > 0; } - /** - * @inheritDoc - */ - function delete() { + + public function delete() { return null; } - /** - * @inheritDoc - */ - function getLastModified() { + public function getLastModified() { return $this->board->getLastModified(); } - /** - * @inheritDoc - */ - function getGroup() { + public function getGroup() { return []; } - /** - * @inheritDoc - */ - function propPatch(PropPatch $propPatch) { + public function propPatch(PropPatch $propPatch) { + $properties = [ + '{DAV:}displayname', + '{http://apple.com/ns/ical/}calendar-color' + ]; + $propPatch->handle($properties, function ($properties) { + foreach ($properties as $key => $value) { + switch ($key) { + case '{DAV:}displayname': + if (mb_substr($value, 0, strlen('Deck: '))) { + $value = mb_substr($value, strlen('Deck: ')); + } + $this->board->setTitle($value); + break; + case '{http://apple.com/ns/ical/}calendar-color': + $this->board->setColor(substr($value, 1)); + break; + } + } + return $this->backend->updateBoard($this->board); + }); // We can just return here and let oc_properties handle everything } /** * @inheritDoc */ - function getProperties($properties) { - // A backend should provide at least minimum properties + public function getProperties($properties) { return [ '{DAV:}displayname' => 'Deck: ' . ($this->board ? $this->board->getTitle() : 'no board object provided'), '{http://apple.com/ns/ical/}calendar-color' => '#' . $this->board->getColor(), - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']), ]; } - - /** - * @inheritDoc - */ - function updateShares(array $add, array $remove) { - // TODO: Implement updateShares() method. - } - - /** - * @inheritDoc - */ - function getShares() { - return []; - } - - /** - * @inheritDoc - */ - public function getResourceId() { - // TODO: Implement getResourceId() method. - } } diff --git a/lib/DAV/CalendarObject.php b/lib/DAV/CalendarObject.php index 82633dd30..2a4d3e041 100644 --- a/lib/DAV/CalendarObject.php +++ b/lib/DAV/CalendarObject.php @@ -23,130 +23,88 @@ namespace OCA\Deck\DAV; use OCA\Deck\Db\Card; -use OCA\Deck\Service\CardService; +use OCA\Deck\Db\Stack; +use Sabre\CalDAV\ICalendarObject; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAVACL\IACL; use Sabre\VObject\Component\VCalendar; -class CalendarObject implements \Sabre\CalDAV\ICalendarObject, \Sabre\DAVACL\IACL { +class CalendarObject implements ICalendarObject, IACL { /** @var Calendar */ private $calendar; - /** @var string */ private $name; - /** - * @var Card - */ + /** @var Card|Stack */ private $sourceItem; + /** @var DeckCalendarBackend */ + private $backend; + /** @var VCalendar */ + private $calendarObject; - /** - * CalendarObject constructor. - * - * @param Calendar $calendar - * @param string $name - */ - public function __construct(Calendar $calendar, string $name, $sourceItem = null) { + public function __construct(Calendar $calendar, string $name, $sourceItem = null, DeckCalendarBackend $backend) { $this->calendar = $calendar; $this->name = $name; $this->sourceItem = $sourceItem; + $this->backend = $backend; + $this->calendarObject = $this->sourceItem->getCalendarObject(); } - /** - * @inheritDoc - */ - function getOwner() { + public function getOwner() { return null; } - /** - * @inheritDoc - */ - function getGroup() { + public function getGroup() { return null; } - /** - * @inheritDoc - */ - function getACL() { + public function getACL() { return $this->calendar->getACL(); } - /** - * @inheritDoc - */ - function setACL(array $acl) { - throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + public function setACL(array $acl) { + throw new Forbidden('Setting ACL is not supported on this node'); } - /** - * @inheritDoc - */ - function getSupportedPrivilegeSet() { + public function getSupportedPrivilegeSet() { return null; } - /** - * @inheritDoc - */ - function put($data) { - throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + public function put($data) { + throw new Forbidden('This calendar-object is read-only'); } - /** - * @inheritDoc - */ - function get() { + public function get() { if ($this->sourceItem) { - return $this->sourceItem->getCalendarObject()->serialize(); + return $this->calendarObject->serialize(); } } - /** - * @inheritDoc - */ - function getContentType() { + public function getContentType() { return 'text/calendar; charset=utf-8'; } - /** - * @inheritDoc - */ - function getETag() { - return '"' . md5($this->get()) . '"'; + public function getETag() { + return '"' . md5($this->sourceItem->getLastModified()) . '"'; } - /** - * @inheritDoc - */ - function getSize() { - return strlen($this->get()); + public function getSize() { + return mb_strlen($this->calendarObject->serialize()); } - /** - * @inheritDoc - */ - function delete() { - throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + public function delete() { + throw new Forbidden('This calendar-object is read-only'); } - /** - * @inheritDoc - */ - function getName() { + public function getName() { return $this->name; } - /** - * @inheritDoc - */ - function setName($name) { - throw new \Sabre\DAV\Exception\Forbidden('This calendar-object is read-only'); + public function setName($name) { + throw new Forbidden('This calendar-object is read-only'); } - /** - * @inheritDoc - */ - function getLastModified() { + public function getLastModified() { return $this->sourceItem->getLastModified(); } } diff --git a/lib/DAV/CalendarPlugin.php b/lib/DAV/CalendarPlugin.php index ba5226827..e0f614995 100644 --- a/lib/DAV/CalendarPlugin.php +++ b/lib/DAV/CalendarPlugin.php @@ -26,53 +26,37 @@ use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\Deck\Db\Board; -use OCA\Deck\Service\BoardService; class CalendarPlugin implements ICalendarProvider { - /** - * @var BoardService - */ - private $boardService; + /** @var DeckCalendarBackend */ + private $backend; - public function __construct(BoardService $boardService) { - $this->boardService = $boardService; + public function __construct(DeckCalendarBackend $backend) { + $this->backend = $backend; } - /** - * @inheritDoc - */ public function getAppId(): string { return 'deck'; } - /** - * @inheritDoc - */ public function fetchAllForCalendarHome(string $principalUri): array { - $boards = $this->boardService->findAll(); return array_map(function (Board $board) use ($principalUri) { - return new Calendar($principalUri, 'board-' . $board->getId(), $board); - }, $boards); + return new Calendar($principalUri, 'board-' . $board->getId(), $board, $this->backend); + }, $this->backend->getBoards()); } - /** - * @inheritDoc - */ public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool { - $boards = array_map(function(Board $board) { + $boards = array_map(static function (Board $board) { return 'board-' . $board->getId(); - }, $this->boardService->findAll()); + }, $this->backend->getBoards()); return in_array($calendarUri, $boards, true); } - /** - * @inheritDoc - */ public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar { if ($this->hasCalendarInCalendarHome($principalUri, $calendarUri)) { - $board = $this->boardService->find(str_replace('board-', '', $calendarUri)); - return new Calendar($principalUri, $calendarUri, $board); + $board = $this->backend->getBoard((int)str_replace('board-', '', $calendarUri)); + return new Calendar($principalUri, $calendarUri, $board, $this->backend); } return null; diff --git a/lib/DAV/DeckCalendarBackend.php b/lib/DAV/DeckCalendarBackend.php new file mode 100644 index 000000000..2e001cc08 --- /dev/null +++ b/lib/DAV/DeckCalendarBackend.php @@ -0,0 +1,84 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\DAV; + +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\CardService; +use OCA\Deck\Service\PermissionService; +use OCA\Deck\Service\StackService; + +class DeckCalendarBackend { + + /** @var BoardService */ + private $boardService; + /** @var StackService */ + private $stackService; + /** @var CardService */ + private $cardService; + /** @var PermissionService */ + private $permissionService; + /** @var BoardMapper */ + private $boardMapper; + + public function __construct( + BoardService $boardService, StackService $stackService, CardService $cardService, PermissionService $permissionService, + BoardMapper $boardMapper + ) { + $this->boardService = $boardService; + $this->stackService = $stackService; + $this->cardService = $cardService; + $this->permissionService = $permissionService; + $this->boardMapper = $boardMapper; + } + + public function getBoards(): array { + return $this->boardService->findAll(); + } + + public function getBoard(int $id): Board { + return $this->boardService->find($id); + } + + public function checkBoardPermission(int $id, int $permission): bool { + $permissions = $this->permissionService->getPermissions($id); + return isset($permissions[$permission]) ? $permissions[$permission] : false; + } + + public function updateBoard(Board $board): bool { + $this->boardMapper->update($board); + return true; + } + + public function getChildren(int $id): array { + return array_merge( + $this->cardService->findCalendarEntries($id), + $this->stackService->findCalendarEntries($id) + ); + } +} diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 0b157fd56..1bb927d41 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -159,5 +159,4 @@ public function getCalendarObject(): VCalendar { public function getCalendarPrefix(): string { return 'card'; } - } diff --git a/lib/Db/Stack.php b/lib/Db/Stack.php index 7dc9310ba..1cc679240 100644 --- a/lib/Db/Stack.php +++ b/lib/Db/Stack.php @@ -65,5 +65,4 @@ public function getCalendarObject(): VCalendar { public function getCalendarPrefix(): string { return 'stack'; } - } From 6502657b72a08401539165226b65cbc3f0a417ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 11:06:36 +0200 Subject: [PATCH 07/15] Fix info.xml to fit schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/info.xml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index db43d3f4c..23aad36f7 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -64,15 +64,9 @@ OCA\Deck\Activity\DeckProvider - - - OCA\Deck\DAV\CalendarPlugin - - OCA\Deck\Provider\DeckProvider - Deck @@ -81,5 +75,9 @@ 10 - + + + OCA\Deck\DAV\CalendarPlugin + + From 341a9628e9039e224f8901d4659a082d4e42cd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 14:04:06 +0200 Subject: [PATCH 08/15] Further cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/DAV/Calendar.php | 29 ++++++++--------------------- lib/DAV/CalendarObject.php | 2 +- lib/DAV/CalendarPlugin.php | 10 +++++++--- lib/DAV/DeckCalendarBackend.php | 7 ++++++- lib/Db/Card.php | 7 +------ lib/Db/Stack.php | 2 +- lib/Service/BoardService.php | 6 +++--- 7 files changed, 27 insertions(+), 36 deletions(-) diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php index 6c97e8446..9edae7f03 100644 --- a/lib/DAV/Calendar.php +++ b/lib/DAV/Calendar.php @@ -28,6 +28,7 @@ use OCA\Deck\Db\Board; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; class Calendar extends ExternalCalendar { @@ -66,24 +67,9 @@ public function getACL() { 'privilege' => '{DAV:}read', 'principal' => $this->getOwner(), 'protected' => true, - ], - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner() . '/calendar-proxy-write', - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner() . '/calendar-proxy-read', - 'protected' => true, - ], + ] ]; - if ($this->backend->checkBoardPermission($this->board->getId(), Acl::PERMISSION_EDIT)) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $this->getOwner(), - 'protected' => true, - ]; + if ($this->backend->checkBoardPermission($this->board->getId(), Acl::PERMISSION_MANAGE)) { $acl[] = [ 'privilege' => '{DAV:}write-properties', 'principal' => $this->getOwner(), @@ -109,7 +95,7 @@ public function calendarQuery(array $filters) { } public function createFile($name, $data = null) { - throw new \Sabre\DAV\Exception\Forbidden('Creating a new entry is not implemented'); + throw new Forbidden('Creating a new entry is not implemented'); } public function getChild($name) { @@ -121,9 +107,10 @@ function ($card) use (&$name) { } )); if (count($card) > 0) { - return new CalendarObject($this, $name, $card[0], $this->backend); + return new CalendarObject($this, $name, $this->backend, $card[0]); } } + throw new NotFound('Node not found'); } public function getChildren() { @@ -151,7 +138,7 @@ function ($card) use (&$name) { public function delete() { - return null; + throw new Forbidden('Deleting an entry is not implemented'); } public function getLastModified() { @@ -171,7 +158,7 @@ public function propPatch(PropPatch $propPatch) { foreach ($properties as $key => $value) { switch ($key) { case '{DAV:}displayname': - if (mb_substr($value, 0, strlen('Deck: '))) { + if (mb_strpos($value, 'Deck: ') === 0) { $value = mb_substr($value, strlen('Deck: ')); } $this->board->setTitle($value); diff --git a/lib/DAV/CalendarObject.php b/lib/DAV/CalendarObject.php index 2a4d3e041..53b55731e 100644 --- a/lib/DAV/CalendarObject.php +++ b/lib/DAV/CalendarObject.php @@ -42,7 +42,7 @@ class CalendarObject implements ICalendarObject, IACL { /** @var VCalendar */ private $calendarObject; - public function __construct(Calendar $calendar, string $name, $sourceItem = null, DeckCalendarBackend $backend) { + public function __construct(Calendar $calendar, string $name, DeckCalendarBackend $backend, $sourceItem) { $this->calendar = $calendar; $this->name = $name; $this->sourceItem = $sourceItem; diff --git a/lib/DAV/CalendarPlugin.php b/lib/DAV/CalendarPlugin.php index e0f614995..7e24fc1f3 100644 --- a/lib/DAV/CalendarPlugin.php +++ b/lib/DAV/CalendarPlugin.php @@ -26,6 +26,7 @@ use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\Deck\Db\Board; +use Sabre\DAV\Exception\NotFound; class CalendarPlugin implements ICalendarProvider { @@ -55,10 +56,13 @@ public function hasCalendarInCalendarHome(string $principalUri, string $calendar public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar { if ($this->hasCalendarInCalendarHome($principalUri, $calendarUri)) { - $board = $this->backend->getBoard((int)str_replace('board-', '', $calendarUri)); - return new Calendar($principalUri, $calendarUri, $board, $this->backend); + try { + $board = $this->backend->getBoard((int)str_replace('board-', '', $calendarUri)); + return new Calendar($principalUri, $calendarUri, $board, $this->backend); + } catch (NotFound $e) { + // We can just return null if we have no matching board + } } - return null; } } diff --git a/lib/DAV/DeckCalendarBackend.php b/lib/DAV/DeckCalendarBackend.php index 2e001cc08..6d3ca0239 100644 --- a/lib/DAV/DeckCalendarBackend.php +++ b/lib/DAV/DeckCalendarBackend.php @@ -32,6 +32,7 @@ use OCA\Deck\Service\CardService; use OCA\Deck\Service\PermissionService; use OCA\Deck\Service\StackService; +use Sabre\DAV\Exception\NotFound; class DeckCalendarBackend { @@ -62,7 +63,11 @@ public function getBoards(): array { } public function getBoard(int $id): Board { - return $this->boardService->find($id); + try { + return $this->boardService->find($id); + } catch (\Exception $e) { + throw new NotFound('Board with id ' . $id . ' not found'); + } } public function checkBoardPermission(int $id, int $permission): bool { diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 1bb927d41..bef664fc6 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -144,14 +144,9 @@ public function getCalendarObject(): VCalendar { return $label->getTitle(); }, $this->getLabels()); } - foreach ($this->getAssignedUsers() as $user) { - $participant = $user->resolveParticipant(); - // FIXME use proper uri - $event->add('ATTENDEE', 'https://localhost/remote.php/dav/principals/users/:' . $participant->getUID(), [ 'CN' => $participant->getDisplayName()]); - } - $event->SUMMARY = $this->getTitle(); + $event->DESCRIPTION = $this->getDescription(); $calendar->add($event); return $calendar; } diff --git a/lib/Db/Stack.php b/lib/Db/Stack.php index 1cc679240..9790e6b7c 100644 --- a/lib/Db/Stack.php +++ b/lib/Db/Stack.php @@ -57,7 +57,7 @@ public function getCalendarObject(): VCalendar { $calendar = new VCalendar(); $event = $calendar->createComponent('VTODO'); $event->UID = 'deck-stack-' . $this->getId(); - $event->SUMMARY = '[Stack]: ' . $this->getTitle(); + $event->SUMMARY = 'List : ' . $this->getTitle(); $calendar->add($event); return $calendar; } diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index df7ee1c4c..1d67c9514 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -348,7 +348,7 @@ public function delete($id) { throw new BadRequestException('board id must be a number'); } - $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE); $board = $this->find($id); if ($board->getDeletedAt() > 0) { throw new BadRequestException('This board has already been deleted'); @@ -377,7 +377,7 @@ public function deleteUndo($id) { throw new BadRequestException('board id must be a number'); } - $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE); $board = $this->find($id); $board->setDeletedAt(0); $board = $this->boardMapper->update($board); @@ -404,7 +404,7 @@ public function deleteForce($id) { throw new BadRequestException('id must be a number'); } - $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE); $board = $this->find($id); $delete = $this->boardMapper->delete($board); From c2a4f946b419ea4fc1d2f361a652d59fcec6734f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 14:10:42 +0200 Subject: [PATCH 09/15] Properly validate hex colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/DAV/Calendar.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php index 9edae7f03..c2b02bc0b 100644 --- a/lib/DAV/Calendar.php +++ b/lib/DAV/Calendar.php @@ -30,6 +30,7 @@ use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; +use Sabre\VObject\InvalidDataException; class Calendar extends ExternalCalendar { @@ -164,7 +165,11 @@ public function propPatch(PropPatch $propPatch) { $this->board->setTitle($value); break; case '{http://apple.com/ns/ical/}calendar-color': - $this->board->setColor(substr($value, 1)); + $color = substr($value, 1, 6); + if (!preg_match('/[a-f0-9]{6}/i', $color)) { + throw new InvalidDataException('No valid color provided'); + } + $this->board->setColor($color); break; } } From 3f7966a6d4e25c5fc3f92d33e404fd918ca75f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 15:43:37 +0200 Subject: [PATCH 10/15] Fix rebaseing issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 549aadfe0..1eff81145 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -83,8 +83,8 @@ public function update(Entity $entity, $updateModified = true): Entity { // make sure we only reset the notification flag if the duedate changes if (in_array('duedate', $entity->getUpdatedFields(), true)) { - /** @var Card $existing */ try { + /** @var Card $existing */ $existing = $this->find($entity->getId()); if ($existing && $entity->getDuedate() !== $existing->getDuedate()) { $entity->setNotified(false); @@ -131,9 +131,10 @@ public function findAll($stackId, $limit = null, $offset = null, $since = -1) { ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT))) - ->orderBy('order', 'id') ->setMaxResults($limit) - ->setFirstResult($offset); + ->setFirstResult($offset) + ->orderBy('order') + ->addOrderBy('id'); return $this->findEntities($qb); } @@ -156,16 +157,12 @@ public function queryCardsByBoards(array $boardIds): IQueryBuilder { } public function findDeleted($boardId, $limit = null, $offset = null) { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('deck_cards', 'c') - ->join('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id')) - ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId))) - ->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(true))) - ->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0))) - ->orderBy('c.order') + $qb = $this->queryCardsByBoard($boardId); + $qb->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->setMaxResults($limit) - ->setFirstResult($offset); + ->setFirstResult($offset) + ->orderBy('order') + ->addOrderBy('id'); return $this->findEntities($qb); } @@ -305,8 +302,8 @@ public function assignLabel($card, $label) { public function removeLabel($card, $label) { $qb = $this->db->getQueryBuilder(); $qb->delete('deck_assigned_labels') - ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($card))) - ->andWhere($qb->expr()->eq('label_id', $qb->createNamedParameter($label))); + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($card, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('label_id', $qb->createNamedParameter($label, IQueryBuilder::PARAM_INT))); $qb->execute(); } From 6ef1c32cf8df932ec159fd53afb48ec4003e06ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 15 Sep 2020 08:43:40 +0200 Subject: [PATCH 11/15] Implement calendarQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/DAV/Calendar.php | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/DAV/Calendar.php b/lib/DAV/Calendar.php index c2b02bc0b..30d6b4024 100644 --- a/lib/DAV/Calendar.php +++ b/lib/DAV/Calendar.php @@ -26,11 +26,13 @@ use OCA\DAV\CalDAV\Plugin; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Board; +use Sabre\CalDAV\CalendarQueryValidator; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Reader; class Calendar extends ExternalCalendar { @@ -89,10 +91,28 @@ public function getSupportedPrivilegeSet() { } public function calendarQuery(array $filters) { - // FIXME: In a real implementation this should actually filter - return array_map(function ($card) { - return $card->getCalendarPrefix() . '-' . $card->getId() . '.ics'; - }, $this->children); + $result = []; + $objects = $this->getChildren(); + + foreach ($objects as $object) { + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object->getName(); + } + } + + return $result; + } + + protected function validateFilterForObject($object, array $filters) { + $vObject = Reader::read($object->get()); + + $validator = new CalendarQueryValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; } public function createFile($name, $data = null) { From 2f44532b75569b68ae5b9d196c60084400e5d6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 15 Sep 2020 08:44:02 +0200 Subject: [PATCH 12/15] Fix internal board cache keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/BoardService.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 1d67c9514..91c0999bf 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -130,6 +130,7 @@ public function findAll($since = -1, $details = null) { return $this->boardsCache; } $complete = $this->getUserBoards($since); + $result = []; /** @var Board $item */ foreach ($complete as &$item) { $this->boardMapper->mapOwner($item); @@ -152,7 +153,7 @@ public function findAll($since = -1, $details = null) { ]); $result[$item->getId()] = $item; } - $this->boardsCache = $complete; + $this->boardsCache = $result; return array_values($result); } @@ -189,6 +190,7 @@ public function find($boardId) { 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false ]); $this->enrichWithUsers($board); + $this->boardsCache[$board->getId()] = $board; return $board; } From 1b16dbacf5acb7046ae0478c34be2643ec9b13da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 15 Sep 2020 08:45:22 +0200 Subject: [PATCH 13/15] Add calendar setting and move to more generic config ocs routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/routes.php | 9 +- lib/Controller/ConfigController.php | 72 ++--------- lib/Controller/PageController.php | 10 +- lib/DAV/CalendarPlugin.php | 18 ++- lib/Service/ConfigService.php | 129 ++++++++++++++++++++ src/components/navigation/AppNavigation.vue | 68 +++++++---- src/store/main.js | 21 ++++ 7 files changed, 228 insertions(+), 99 deletions(-) create mode 100644 lib/Service/ConfigService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 6a1482cd8..4b273c464 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -26,9 +26,6 @@ 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], - ['name' => 'Config#get', 'url' => '/config', 'verb' => 'GET'], - ['name' => 'Config#setValue', 'url' => '/config/{key}', 'verb' => 'POST'], - // boards ['name' => 'board#index', 'url' => '/boards', 'verb' => 'GET'], ['name' => 'board#create', 'url' => '/boards', 'verb' => 'POST'], @@ -125,17 +122,17 @@ ['name' => 'attachment_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'DELETE'], ['name' => 'attachment_api#restore', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'], - - ['name' => 'board_api#preflighted_cors', 'url' => '/api/v1.0/{path}','verb' => 'OPTIONS', 'requirements' => ['path' => '.+']], ], 'ocs' => [ + ['name' => 'Config#get', 'url' => '/api/v1.0/config', 'verb' => 'GET'], + ['name' => 'Config#setValue', 'url' => '/api/v1.0/config/{key}', 'verb' => 'POST'], + ['name' => 'comments_api#list', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'GET'], ['name' => 'comments_api#create', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'POST'], ['name' => 'comments_api#update', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'], ['name' => 'comments_api#delete', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'], - // dashboard ['name' => 'overview_api#upcomingCards', 'url' => '/api/v1.0/overview/upcoming', 'verb' => 'GET'], ] ]; diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index 342ac3fea..b64e23106 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -23,90 +23,42 @@ namespace OCA\Deck\Controller; +use OCA\Deck\Service\ConfigService; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\NotFoundResponse; -use OCP\IConfig; -use OCP\IGroup; -use OCP\IGroupManager; +use OCP\AppFramework\OCSController; use OCP\IRequest; -use OCP\AppFramework\Controller; -class ConfigController extends Controller { - private $config; - private $userId; - private $groupManager; +class ConfigController extends OCSController { + private $configService; public function __construct( $AppName, IRequest $request, - IConfig $config, - IGroupManager $groupManager, - $userId + ConfigService $configService ) { parent::__construct($AppName, $request); - $this->userId = $userId; - $this->groupManager = $groupManager; - $this->config = $config; + $this->configService = $configService; } /** * @NoCSRFRequired + * @NoAdminRequired */ - public function get() { - $data = [ - 'groupLimit' => $this->getGroupLimit(), - ]; - return new DataResponse($data); + public function get(): DataResponse { + return new DataResponse($this->configService->getAll()); } /** * @NoCSRFRequired + * @NoAdminRequired */ - public function setValue($key, $value) { - switch ($key) { - case 'groupLimit': - $result = $this->setGroupLimit($value); - break; - } + public function setValue(string $key, $value) { + $result = $this->configService->set($key, $value); if ($result === null) { return new NotFoundResponse(); } return new DataResponse($result); } - - private function setGroupLimit($value) { - $groups = []; - foreach ($value as $group) { - $groups[] = $group['id']; - } - $data = implode(',', $groups); - $this->config->setAppValue($this->appName, 'groupLimit', $data); - return $groups; - } - - private function getGroupLimitList() { - $value = $this->config->getAppValue($this->appName, 'groupLimit', ''); - $groups = explode(',', $value); - if ($value === '') { - return []; - } - return $groups; - } - - private function getGroupLimit() { - $groups = $this->getGroupLimitList(); - $groups = array_map(function ($groupId) { - /** @var IGroup $groups */ - $group = $this->groupManager->get($groupId); - if ($group === null) { - return null; - } - return [ - 'id' => $group->getGID(), - 'displayname' => $group->getDisplayName(), - ]; - }, $groups); - return array_filter($groups); - } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 2e6a32e6e..68b603d10 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -24,34 +24,33 @@ namespace OCA\Deck\Controller; use OCA\Deck\AppInfo\Application; +use OCA\Deck\Service\ConfigService; use OCA\Deck\Service\PermissionService; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\IInitialStateService; use OCP\IRequest; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Controller; -use OCP\IL10N; class PageController extends Controller { private $permissionService; private $userId; private $l10n; private $initialState; + private $configService; public function __construct( $AppName, IRequest $request, PermissionService $permissionService, IInitialStateService $initialStateService, - IL10N $l10n, - $userId + ConfigService $configService ) { parent::__construct($AppName, $request); - $this->userId = $userId; $this->permissionService = $permissionService; $this->initialState = $initialStateService; - $this->l10n = $l10n; + $this->configService = $configService; } /** @@ -64,6 +63,7 @@ public function __construct( public function index() { $this->initialState->provideInitialState(Application::APP_ID, 'maxUploadSize', (int)\OCP\Util::uploadLimit()); $this->initialState->provideInitialState(Application::APP_ID, 'canCreate', $this->permissionService->canCreate()); + $this->initialState->provideInitialState(Application::APP_ID, 'config', $this->configService->getAll()); $response = new TemplateResponse('deck', 'main'); diff --git a/lib/DAV/CalendarPlugin.php b/lib/DAV/CalendarPlugin.php index 7e24fc1f3..76da1929a 100644 --- a/lib/DAV/CalendarPlugin.php +++ b/lib/DAV/CalendarPlugin.php @@ -26,15 +26,19 @@ use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\Deck\Db\Board; +use OCA\Deck\Service\ConfigService; use Sabre\DAV\Exception\NotFound; class CalendarPlugin implements ICalendarProvider { /** @var DeckCalendarBackend */ private $backend; + /** @var bool */ + private $calendarIntegrationEnabled; - public function __construct(DeckCalendarBackend $backend) { + public function __construct(DeckCalendarBackend $backend, ConfigService $configService) { $this->backend = $backend; + $this->calendarIntegrationEnabled = $configService->get('calendar'); } public function getAppId(): string { @@ -42,12 +46,20 @@ public function getAppId(): string { } public function fetchAllForCalendarHome(string $principalUri): array { + if (!$this->calendarIntegrationEnabled) { + return []; + } + return array_map(function (Board $board) use ($principalUri) { return new Calendar($principalUri, 'board-' . $board->getId(), $board, $this->backend); }, $this->backend->getBoards()); } public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool { + if (!$this->calendarIntegrationEnabled) { + return false; + } + $boards = array_map(static function (Board $board) { return 'board-' . $board->getId(); }, $this->backend->getBoards()); @@ -55,6 +67,10 @@ public function hasCalendarInCalendarHome(string $principalUri, string $calendar } public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar { + if (!$this->calendarIntegrationEnabled) { + return null; + } + if ($this->hasCalendarInCalendarHome($principalUri, $calendarUri)) { try { $board = $this->backend->getBoard((int)str_replace('board-', '', $calendarUri)); diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php new file mode 100644 index 000000000..baf1e4b8f --- /dev/null +++ b/lib/Service/ConfigService.php @@ -0,0 +1,129 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Service; + +use OCA\Deck\AppInfo\Application; +use OCA\Deck\NoPermissionException; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; + +class ConfigService { + private $config; + private $userId; + private $groupManager; + + public function __construct( + IConfig $config, + IGroupManager $groupManager, + $userId + ) { + $this->userId = $userId; + $this->groupManager = $groupManager; + $this->config = $config; + } + + public function getAll(): array { + $data = [ + 'calendar' => $this->get('calendar') + ]; + if ($this->groupManager->isAdmin($this->userId)) { + $data = [ + 'groupLimit' => $this->get('groupLimit'), + ]; + } + return $data; + } + + public function get($key) { + $result = null; + switch ($key) { + case 'groupLimit': + if (!$this->groupManager->isAdmin($this->userId)) { + throw new NoPermissionException('You must be admin to get the group limit'); + } + $result = $this->getGroupLimit(); + break; + case 'calendar': + $result = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true); + break; + } + return $result; + } + + public function set($key, $value) { + $result = null; + switch ($key) { + case 'groupLimit': + if (!$this->groupManager->isAdmin($this->userId)) { + throw new NoPermissionException('You must be admin to set the group limit'); + } + $result = $this->setGroupLimit($value); + break; + case 'calendar': + $this->config->setUserValue($this->userId, Application::APP_ID, 'calendar', (int)$value); + $result = $value; + break; + } + return $result; + } + + private function setGroupLimit($value) { + $groups = []; + foreach ($value as $group) { + $groups[] = $group['id']; + } + $data = implode(',', $groups); + $this->config->setAppValue(Application::APP_ID, 'groupLimit', $data); + return $groups; + } + + private function getGroupLimitList() { + $value = $this->config->getAppValue(Application::APP_ID, 'groupLimit', ''); + $groups = explode(',', $value); + if ($value === '') { + return []; + } + return $groups; + } + + private function getGroupLimit() { + $groups = $this->getGroupLimitList(); + $groups = array_map(function ($groupId) { + /** @var IGroup $groups */ + $group = $this->groupManager->get($groupId); + if ($group === null) { + return null; + } + return [ + 'id' => $group->getGID(), + 'displayname' => $group->getDisplayName(), + ]; + }, $groups); + return array_filter($groups); + } +} diff --git a/src/components/navigation/AppNavigation.vue b/src/components/navigation/AppNavigation.vue index 99f85f15b..8d7cd47ea 100644 --- a/src/components/navigation/AppNavigation.vue +++ b/src/components/navigation/AppNavigation.vue @@ -52,14 +52,28 @@ @@ -84,7 +100,8 @@ import { AppNavigation as AppNavigationVue, AppNavigationItem, AppNavigationSett import AppNavigationAddBoard from './AppNavigationAddBoard' import AppNavigationBoardCategory from './AppNavigationBoardCategory' import { loadState } from '@nextcloud/initial-state' -import { generateUrl, generateOcsUrl } from '@nextcloud/router' +import { generateOcsUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' const canCreateState = loadState('deck', 'canCreate') @@ -123,8 +140,7 @@ export default { 'sharedBoards', ]), isAdmin() { - // eslint-disable-next-line - return OC.isUserAdmin() + return !!getCurrentUser()?.isAdmin }, cardDetailsInModal: { get() { @@ -134,15 +150,19 @@ export default { this.$store.dispatch('setCardDetailsInModal', newValue) }, }, + configCalendar: { + get() { + return this.$store.getters.config('calendar') + }, + set(newValue) { + this.$store.dispatch('setConfig', { calendar: newValue }) + }, + }, }, beforeMount() { if (this.isAdmin) { - axios.get(generateUrl('apps/deck/config')).then((response) => { - this.groupLimit = response.data.groupLimit - this.groupLimitDisabled = false - }, (error) => { - console.error('Error while loading groupLimit', error.response) - }) + this.groupLimit = this.$store.getters.config('groupLimit') + this.groupLimitDisabled = false axios.get(generateOcsUrl('cloud', 2) + 'groups').then((response) => { this.groups = response.data.ocs.data.groups.reduce((obj, item) => { obj.push({ @@ -157,15 +177,9 @@ export default { } }, methods: { - updateConfig() { - this.groupLimitDisabled = true - axios.post(generateUrl('apps/deck/config/groupLimit'), { - value: this.groupLimit, - }).then(() => { - this.groupLimitDisabled = false - }, (error) => { - console.error('Error while saving groupLimit', error.response) - }) + async updateConfig() { + await this.$store.dispatch('setConfig', { groupLimit: this.groupLimit }) + this.groupLimitDisabled = false }, }, } diff --git a/src/store/main.js b/src/store/main.js index 99b313e05..dc25160ab 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -22,6 +22,7 @@ import 'url-search-params-polyfill' +import { loadState } from '@nextcloud/initial-state' import Vue from 'vue' import Vuex from 'vuex' import axios from '@nextcloud/axios' @@ -56,6 +57,7 @@ export default new Vuex.Store({ }, strict: debug, state: { + config: loadState('deck', 'config', {}), showArchived: false, navShown: true, compactMode: localStorage.getItem('deck.compactMode') === 'true', @@ -73,6 +75,9 @@ export default new Vuex.Store({ filter: { tags: [], users: [], due: '' }, }, getters: { + config: state => (key) => { + return state.config[key] + }, cardDetailsInModal: state => { return state.cardDetailsInModal }, @@ -133,6 +138,9 @@ export default new Vuex.Store({ }, }, mutations: { + SET_CONFIG(state, { key, value }) { + Vue.set(state.config, key, value) + }, setSearchQuery(state, searchQuery) { state.searchQuery = searchQuery }, @@ -287,6 +295,19 @@ export default new Vuex.Store({ }, actions: { + async setConfig({ commit }, config) { + for (const key in config) { + try { + await axios.post(generateOcsUrl(`apps/deck/api/v1.0/config`) + key, { + value: config[key], + }) + commit('SET_CONFIG', { key, value: config[key] }) + } catch (e) { + console.error(`Error while saving ${key}`, e.response) + throw e + } + } + }, setFilter({ commit }, filter) { commit('SET_FILTER', filter) }, From 19cdd31c40b4d729754991e3d045c435c78350d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 15 Sep 2020 08:54:03 +0200 Subject: [PATCH 14/15] Fix PageController tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- tests/unit/controller/PageControllerTest.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/unit/controller/PageControllerTest.php b/tests/unit/controller/PageControllerTest.php index d84724e4a..4cc9e0c8b 100644 --- a/tests/unit/controller/PageControllerTest.php +++ b/tests/unit/controller/PageControllerTest.php @@ -24,40 +24,33 @@ namespace OCA\Deck\Controller; +use OCA\Deck\Service\ConfigService; use OCA\Deck\Service\PermissionService; use OCP\IInitialStateService; use OCP\IL10N; use OCP\IRequest; -use OCA\Deck\Db\Board; -use OCP\IConfig; class PageControllerTest extends \Test\TestCase { private $controller; private $request; private $l10n; - private $userId = 'john'; private $permissionService; private $initialState; - private $config; + private $configService; public function setUp(): void { $this->l10n = $this->createMock(IL10N::class); $this->request = $this->createMock(IRequest::class); $this->permissionService = $this->createMock(PermissionService::class); - $this->config = $this->createMock(IConfig::class); + $this->configService = $this->createMock(ConfigService::class); $this->initialState = $this->createMock(IInitialStateService::class); $this->controller = new PageController( - 'deck', $this->request, $this->permissionService, $this->initialState, $this->l10n, $this->userId + 'deck', $this->request, $this->permissionService, $this->initialState, $this->configService ); } public function testIndex() { - $board = new Board(); - $board->setTitle('Personal'); - $board->setOwner($this->userId); - $board->setColor('317CCC'); - $this->permissionService->expects($this->any()) ->method('canCreate') ->willReturn(true); From e460879f4108056992312d26f8155e34c9e5024b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 15 Sep 2020 10:27:42 +0200 Subject: [PATCH 15/15] Set DUE instead of DTSTART and DTEND MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/Card.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/Db/Card.php b/lib/Db/Card.php index bef664fc6..4e24719ba 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -24,6 +24,7 @@ namespace OCA\Deck\Db; use DateTime; +use DateTimeZone; use Sabre\VObject\Component\VCalendar; class Card extends RelationalEntity { @@ -124,10 +125,10 @@ public function getCalendarObject(): VCalendar { $event = $calendar->createComponent('VTODO'); $event->UID = 'deck-card-' . $this->getId(); if ($this->getDuedate()) { - $event->DTSTAMP = new \DateTime(); - $event->DTSTART = new \DateTime($this->getDuedate()); - $event->DTEND = new \DateTime($this->getDuedate()); - $event->DURATION = "PT1H"; + $creationDate = new DateTime(); + $creationDate->setTimestamp($this->createdAt); + $event->DTSTAMP = $creationDate; + $event->DUE = new DateTime($this->getDuedate(true), new DateTimeZone('UTC')); } $event->add('RELATED-TO', 'deck-stack-' . $this->getStackId());