Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion lib/CalDAV/OrganizerValidationPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function initialize(Server $server) {
$this->server = $server;
$server->on('calendarObjectChange', [$this, 'calendarObjectChange'], Plugin::PRIORITY_BEFORE_SCHEDULING - 10);
$server->on('beforeMove', [$this, 'beforeMove'], 45);
$server->on('beforeCopy', [$this, 'beforeCopy'], 45);
}

function getPluginName() {
Expand Down Expand Up @@ -55,8 +56,15 @@ function calendarObjectChange(
}

function beforeMove($sourcePath, $destinationPath) {
// A team calendar lives in its own calendar home, so a legitimate MOVE into one always
// crosses homes. Exempt exactly that destination from the guard, because it is the one
// the ORGANIZER validation below covers instead.
list($calendarPath,) = Utils::splitEventPath('/' . ltrim($destinationPath, '/'));
if (!$calendarPath || !$this->isTeamCalendarPath($calendarPath)) return;
if (!$calendarPath || !$this->isTeamCalendarPath($calendarPath)) {
$this->assertSameCalendarHome($sourcePath, $destinationPath);

return;
}

try {
$source = $this->server->tree->getNodeForPath($sourcePath);
Expand All @@ -80,6 +88,38 @@ function beforeMove($sourcePath, $destinationPath) {
}
}

function beforeCopy($sourcePath, $destinationPath, $depth = null) {
$this->assertSameCalendarHome($sourcePath, $destinationPath);
}

/**
* SabreDAV runs COPY and MOVE through Tree::copy()/Tree::move(), which never emit
* calendarObjectChange and therefore skip the ORGANIZER validation a PUT goes through.
* Confining a transfer to a single calendar home keeps an event that a PUT would have
* rejected from being smuggled into someone else's calendar.
Comment on lines +98 to +99

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No As explaine I should be able to MOVE / COPY an event GIVEN the ORGANIZER is valid for the destination.

PLEASE implement that logic.

@hungphan227 hungphan227 Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validating organizer on destination would make this test in CalDavContract fail:

attendeeCanMoveInvitedEventToAnotherOwnCalendar

and this bug linagora/twake-calendar-frontend#690 will also reappear

Is it acceptable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please meet after your vacation with @vttranlina to find a logic that conciliate both legitimate use case.

That's definitely possible

*/
private function assertSameCalendarHome($sourcePath, $destinationPath): void {
$sourceHome = $this->calendarHomeOf($sourcePath);
$destinationHome = $this->calendarHomeOf($destinationPath);

// Not a calendar object on either end: collection level transfers are left to the ACL plugin.
if ($sourceHome === null || $destinationHome === null) return;

if ($sourceHome !== $destinationHome) {
throw new Forbidden('COPY and MOVE are only allowed within the same calendar home.');
}
}

private function calendarHomeOf($eventPath): ?string {
list($calendarPath,) = Utils::splitEventPath('/' . ltrim($eventPath, '/'));
if (!$calendarPath) {
return null;
}

// $calendarPath is 'calendars/{baseId}/{calendarUri}'.
return explode('/', $calendarPath)[1];
}

private function validateCalendarOrganizer(VCalendar $calendar, $calendarPath, ?string $existingOrganizerUri = null): void {
$vevents = $calendar->select('VEVENT');
if (empty($vevents) || !($organizerUri = $this->extractOrganizerUri($vevents))) return;
Expand Down
1 change: 1 addition & 0 deletions run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ trap cleanup EXIT # finally: sera exécuté *quoi qu'il arrive*
javatest() {
git clone https://github.com/linagora/twake-calendar-integration-tests.git it-tests || exit 1
cd it-tests || exit 1
git checkout test-sabre-450 || exit 1
bash pre-build.sh esn_sabre_test || exit 1
mvn clean install -Dapi.version=1.43 -Dtest=com.linagora.dav.sabrev4_7.** -Damqp.scheduling.enabled=true || exit 1
}
Expand Down