nextcloud/server · error · Sabre\DAV\Exception\Forbidden

This calendar-object is read-only

Error message

This calendar-object is read-only

What it means

CalendarObject::put() accepts new content only when the backend provider implements ICreateFromString AND the calendar grants PERMISSION_UPDATE; otherwise the object is read-only and PUT fails with Sabre\DAV\Exception\Forbidden (HTTP 403). Note AppCalendar::getPermissions() returns only PERMISSION_READ when the provider lacks ICreateFromString, so the ACL check blocks the write early.

Source

Thrown at apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php:77

	#[\Override]
	public function setACL(array $acl): void {
		throw new Forbidden('Setting ACL is not supported on this node');
	}

	#[\Override]
	public function getSupportedPrivilegeSet(): ?array {
		return null;
	}

	#[\Override]
	public function put($data): void {
		if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
			if (is_resource($data)) {
				$data = stream_get_contents($data) ?: '';
			}
			$this->backend->createFromString($this->getName(), $data);
		} else {
			throw new Forbidden('This calendar-object is read-only');
		}
	}

	#[\Override]
	public function get(): string {
		return $this->vobject->serialize();
	}

	#[\Override]
	public function getContentType(): string {
		return 'text/calendar; charset=utf-8';
	}

	#[\Override]
	public function getETag(): ?string {
		return null;
	}

View on GitHub (pinned to ecdeb153ff)

Solutions

  1. App developers: implement ICreateFromString on the provider AND include PERMISSION_UPDATE in getPermissions()
  2. Clients: check write permission (getPermissions() & PERMISSION_UPDATE, or the DAV ACL write-content privilege) before sending PUT
  3. End users: edit such events in the app that owns the calendar

Example fix

// before
public function getPermissions(): int { return Constants::PERMISSION_READ; }

// after
public function getPermissions(): int { return Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_CREATE; } // with ICreateFromString implemented
Defensive patterns

Strategy: type-guard

Validate before calling

// Only PUT when the backend allows updates
$writable = $provider instanceof \OCP\Calendar\ICreateFromString
    && ($provider->getPermissions() & \OCP\Constants::PERMISSION_UPDATE) !== 0;
if (!$writable) {
    // skip PUT, keep read-only sync
}

Type guard

function canUpdateCalendarObject(\OCP\Calendar\ICalendar $c): bool {
    return $c instanceof \OCP\Calendar\ICreateFromString
        && ($c->getPermissions() & \OCP\Constants::PERMISSION_UPDATE) !== 0;
}

Try / catch

try {
    $object->put($data);
} catch (Sabre\DAV\Exception\Forbidden $e) {
    // read-only object; switch this calendar to read-only sync mode
}

Prevention

When it happens

Trigger: PUT to calendars/<user>/<app-calendar>/<object>.ics when the provider is read-only (only ICalendar) or its getPermissions() lacks the PERMISSION_UPDATE bit.

Common situations: Two-way CalDAV sync against read-only app calendars (holiday, embedded feeds); providers implementing ICreateFromString but returning permission bits without PERMISSION_UPDATE.

Related errors


AI-assisted analysis of nextcloud/server@ecdeb153ff (2026-08-17). Data as JSON: /api/errors/9d28263045fd3edb. Report an issue: GitHub.