Write access over CalDAV#7655
Conversation
36747a6 to
31b37dd
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36747a6fd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
584dcf5 to
8f1393f
Compare
grnd-alt
left a comment
There was a problem hiding this comment.
Hey, thanks for your contribution, would be nice to get this feature in. I have some smaller code-style remarks, that are partly up for discussion.
The most important remark is that one though: https://github.com/nextcloud/deck/pull/7655/changes#r2840797696
I think this should be oriented to work with tasks as much as possible before merging.
| $lastModified->setTimestamp($lastModifiedTs); | ||
| $event->DTSTAMP = $lastModified; | ||
| $event->{'LAST-MODIFIED'} = $lastModified; | ||
| $event->STATUS = 'NEEDS-ACTION'; |
There was a problem hiding this comment.
why is it always needs-action? Isn't it fine to have no status as the default?
There was a problem hiding this comment.
My concern was that leaving the synthetic list VTODOs without an explicit status could lead some clients to infer or write back their own status, which would make the behavior less consistent across clients. I did not test that in depth though, so this was mainly a defensive choice.
| @@ -95,45 +115,82 @@ protected function validateFilterForObject($object, array $filters) { | |||
| } | |||
|
|
|||
| public function createFile($name, $data = null) { | |||
There was a problem hiding this comment.
I am not entirely fluent with the webdav protocol, but when testing this with nextcloud tasks and thunderbird the behavior was not as I would've expected it.
I could not investigate thunderbirds requests, but for nc tasks the createFile was called with a filename and after creation the same file was re-requested to display the task, the name seems not to be respected so it is not served again under that name leading to a 404.
There was a problem hiding this comment.
Thanks for the review and this good catch! Somehow missed that in my testings thanks to tolerant clients.
Fixed in f95476c47:
Created cards now persist the client-provided DAV href for non-canonical names, so follow-up requests to the same resource name no longer fail with a 404 after creation. Existing cards still fall back to the canonical card-<id>.ics naming.
Fixed in dea36cf8c:
While testing that change with Thunderbird moves, it also became clear that in per_list_calendar mode Thunderbird can send the target calendar URL while still keeping the old RELATED-TO in the payload. The follow-up fix now prefers the target list from the destination calendar in that mode, so the trailing source delete no longer removes the moved card.
I also added test coverage for stored href resolution, custom-href creation, and same-board stack moves via the target calendar.
An alternative would have been to keep a fully server-generated canonical href model and add extra mapping/alias persistence for client-provided object names. That would preserve uniform resource names, but it also adds noticeably more persistence and lookup complexity.
For this PR I preferred the smaller approach of persisting a single stable DAV href per card. That means object names can be client-shaped rather than fully uniform, but it keeps the DAV object identity stable without introducing a separate DAV object layer for Deck.
This is also closer to how Nextcloud's CalDAV backend handles object URIs in general: object hrefs are persisted as part of the DAV object state, rather than being recomputed into a server-generated card-<id>.ics style name on every request. The main difference is that Deck stores that URI on the card itself instead of using a dedicated calendarobjects persistence layer like core CalDAV does.
There was a problem hiding this comment.
As a small interoperability follow-up fixed in 656466f6c:
direct GET/HEAD requests to stale source hrefs after same-board list moves now return 404 instead of a placeholder object. That avoids Thunderbird treating the old source entry as a still-readable changed item, while collection/report fallback handling remains in place.
|
Hello there, We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 Thank you for contributing to Nextcloud and we hope to hear from you soon! (If you believe you should not receive this message, you can add yourself to the blocklist.) |
45ec9aa to
83626ce
Compare
Implement first working write path for Deck CalDAV objects. - allow DAV write privilege on Deck calendars - handle PUT on existing card/stack ICS objects and map VTODO fields to Deck services - add NC32 compatibility fixes in BoardMapper for empty orX() usage - add unit tests for calendar update mapping Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Follow-up fixes after real-world macOS Reminders tests. - convert COMPLETED timestamps to DateTime expected by Deck entities - provide calendar object owner/group to avoid DELETE scheduling crashes - add backend tests for delete path and COMPLETED-without-STATUS mapping Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
…bility Stabilize bidirectional task sync for Apple Reminders and Thunderbird. - implement createFile support with stack resolution and alias normalization - support delete and robust completed mapping (STATUS/COMPLETED/PERCENT-COMPLETE) - add UID/resource-name upsert logic to avoid duplicates on move-back - handle board-to-board moves by updating existing deck-card IDs - export richer VTODO metadata (DTSTAMP/CREATED/LAST-MODIFIED/PERCENT-COMPLETE) - add extensive unit tests for update/create/delete and fallback paths Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
…Labels" This reverts commit 3d0af5c. Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
- fix Deck settings CalDAV mode selector rendering - make ETag/last-modified depend on selected list mapping mode - map list priority as left=9, right=1 for Thunderbird/Apple Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
…ateFile Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
Signed-off-by: Jaggob <37583151+Jaggob@users.noreply.github.com>
fc98830 to
a2dce36
Compare
grnd-alt
left a comment
There was a problem hiding this comment.
I did not manage to grasp everything in here, but this feels quite spaghetti and hard to read or follow tbh. I do think this can be implemented in a more readable manner, but might as well be a skill issue on code reading on my side. However it would be nice if this could be split up to e.g. not have the list_modes in there as well and get this to be easier to review
| } | ||
|
|
||
| private function isSabreVCalendar($value): bool { | ||
| /** @psalm-suppress UndefinedClass */ |
There was a problem hiding this comment.
wrapper function only to suppress psalm? same for VTodo
| private function extractStackIdFromRelatedTo($todo): ?int { | ||
| $parentCandidates = []; | ||
| $otherCandidates = []; | ||
| foreach ($todo->children() as $child) { |
There was a problem hiding this comment.
this for loop can be something like this using the sabre types to not do the property handling etc yourself, then you could also remove some of the helper functions.
$relatedToArray = $todo->select("RELATED-TO");
foreach($relatedToArray as $relatedTo) {
$reltype = $relatedTo["RELTYPE"] ?? null;
if ($reltype instanceof \Sabre\VObject\Parameter) {
$reltypeValue = $reltype->getValue();
if ($reltypeValue === 'PARENT') {
$parentCandidates[] = (string)$relatedTo;
} else {
$otherCandidates[] = (string)$relatedTo;
}
}
}
| } elseif ($targetBoardId !== null && $currentBoardId !== $targetBoardId) { | ||
| $stackId = $this->getDefaultStackIdForBoard($targetBoardId); | ||
| } else { | ||
| $stackId = $card->getStackId(); |
There was a problem hiding this comment.
this entire stackId handling is not very easily readable, and seems overly complex, it does not make clear why the stackId is selected the way it is.
| } | ||
| } | ||
|
|
||
| private function normalizeDavUriForStorage(?string $name): ?string { |
There was a problem hiding this comment.
weird naming, does not seem to normalize anything but rather validate.
There was a problem hiding this comment.
Strange to have the parameter nullable, but OK. Personally, I would expect an exception thrown in the negative case.
Is this user controlled, though? Is the check sufficient?
| return $card; | ||
| } | ||
|
|
||
| public function findByDavUriLite(string $davUri, ?int $boardId = null, ?int $stackId = null, bool $includeDeleted = true): Card { |
| } | ||
| $stack = $this->stackMapper->find($stackId); | ||
| $boardId = $stack->getBoardId(); | ||
| $board = $this->boardMapper->find($boardId); |
There was a problem hiding this comment.
I don't think those have any benefit as the activitymanager gets board and stack data
| if ($knownBoardId !== null) { | ||
| $this->permissionService->checkPermission($this->boardMapper, $knownBoardId, Acl::PERMISSION_EDIT); | ||
| } else { | ||
| $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT, allowDeletedCard: true); |
There was a problem hiding this comment.
only doing this check if knownBoardId is not passed feels like a security issue. If KnownBoardId does not match the board the card's on it's possible to move a card from a board where a user has only read permission. It looks like this is verified for in getBackendChildren() but that is multiple functions deep and very implicit.
blizzz
left a comment
There was a problem hiding this comment.
I am confused about wide parts of this PR. Some aspects maybe makes sense, but do not really belong here, about others I am really puzzled. Maybe we can extract the necessary parts and have a very specific and lean as possible change set? Aspects that are not directly related to the write access can still be implemented in a separate PR.
| :clearable="false" | ||
| label="label" | ||
| track-by="id" | ||
| :input-label="t('deck', 'CalDAV list mapping mode')" /> |
There was a problem hiding this comment.
as a technical user, I do not know what this means. Seeing the possible value the meaning gets clearer, the main label should still be easier to understand.
Also, isn't this out of scope for the writable caldav feature and should be split into a separate PR?
| protected string $title = ''; | ||
| protected $description; | ||
| protected $descriptionPrev; | ||
| protected $davUri = null; |
| $calendar = new VCalendar(); | ||
| $event = $calendar->createComponent('VTODO'); | ||
| $event->UID = 'deck-card-' . $this->getId(); | ||
| $event->{'X-NC-DECK-CARD-ID'} = (string)$this->getId(); |
There was a problem hiding this comment.
this is already part of the UID?
| if ($this->getDone()) { | ||
| $event->COMPLETED = $this->getDone(); | ||
| } else { | ||
| $event->COMPLETED = $lastModified; |
| } | ||
| } else { | ||
| $event->STATUS = 'NEEDS-ACTION'; | ||
| $event->{'PERCENT-COMPLETE'} = 0; |
| $qb->orderBy('order') | ||
| ->addOrderBy('id'); |
There was a problem hiding this comment.
The return code later suggest only one specific entry is returned, then why sorting?
| } | ||
| } | ||
|
|
||
| private function normalizeDavUriForStorage(?string $name): ?string { |
There was a problem hiding this comment.
Strange to have the parameter nullable, but OK. Personally, I would expect an exception thrown in the negative case.
Is this user controlled, though? Is the check sufficient?
| $card = $this->cardMapper->find($cardId); | ||
| $stack = $this->stackMapper->find($card->getStackId()); | ||
| $board = $this->boardMapper->find($stack->getBoardId()); | ||
| private function findDetailsForCard($cardOrId, ?string $subject = null, ?Stack $knownStack = null, ?array $knownBoard = null): array { |
There was a problem hiding this comment.
why is this extended? It does not seem that this is related to the feature?
Summary
This PR adds CalDAV write support for Deck, enabling CalDAV clients to create, update, complete/uncomplete, and delete Deck cards.
It also introduces per-user CalDAV list mapping modes in Deck settings, so each user can choose how Deck lists are represented in CalDAV clients:
RELATED-TO)CATEGORIESPRIORITYAdditional changes include DAV hardening and compatibility improvements (notably for Thunderbird and Apple clients), plus backend cleanup/performance improvements in the CalDAV handling path.
Done in this PR
CATEGORIES) including Thunderbird interoperabilityFuture TODO
Checklist