{"record":{"id":"9d28263045fd3edb","repo":"nextcloud/server","slug":"this-calendar-object-is-read-only","errorCode":null,"errorMessage":"This calendar-object is read-only","messagePattern":"This calendar-object is read-only","errorType":"exception","errorClass":"Sabre\\DAV\\Exception\\Forbidden","httpStatus":403,"severity":"error","filePath":"apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php","lineNumber":77,"sourceCode":"\t#[\\Override]\n\tpublic function setACL(array $acl): void {\n\t\tthrow new Forbidden('Setting ACL is not supported on this node');\n\t}\n\n\t#[\\Override]\n\tpublic function getSupportedPrivilegeSet(): ?array {\n\t\treturn null;\n\t}\n\n\t#[\\Override]\n\tpublic function put($data): void {\n\t\tif ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {\n\t\t\tif (is_resource($data)) {\n\t\t\t\t$data = stream_get_contents($data) ?: '';\n\t\t\t}\n\t\t\t$this->backend->createFromString($this->getName(), $data);\n\t\t} else {\n\t\t\tthrow new Forbidden('This calendar-object is read-only');\n\t\t}\n\t}\n\n\t#[\\Override]\n\tpublic function get(): string {\n\t\treturn $this->vobject->serialize();\n\t}\n\n\t#[\\Override]\n\tpublic function getContentType(): string {\n\t\treturn 'text/calendar; charset=utf-8';\n\t}\n\n\t#[\\Override]\n\tpublic function getETag(): ?string {\n\t\treturn null;\n\t}\n","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php#L59-L95","documentation":"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.","triggerScenarios":"PUT to calendars/<user>/<app-calendar>/<object>.ics when the provider is read-only (only ICalendar) or its getPermissions() lacks the PERMISSION_UPDATE bit.","commonSituations":"Two-way CalDAV sync against read-only app calendars (holiday, embedded feeds); providers implementing ICreateFromString but returning permission bits without PERMISSION_UPDATE.","solutions":["App developers: implement ICreateFromString on the provider AND include PERMISSION_UPDATE in getPermissions()","Clients: check write permission (getPermissions() & PERMISSION_UPDATE, or the DAV ACL write-content privilege) before sending PUT","End users: edit such events in the app that owns the calendar"],"exampleFix":"// before\npublic function getPermissions(): int { return Constants::PERMISSION_READ; }\n\n// after\npublic function getPermissions(): int { return Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_CREATE; } // with ICreateFromString implemented","handlingStrategy":"type-guard","validationCode":"// Only PUT when the backend allows updates\n$writable = $provider instanceof \\OCP\\Calendar\\ICreateFromString\n    && ($provider->getPermissions() & \\OCP\\Constants::PERMISSION_UPDATE) !== 0;\nif (!$writable) {\n    // skip PUT, keep read-only sync\n}","typeGuard":"function canUpdateCalendarObject(\\OCP\\Calendar\\ICalendar $c): bool {\n    return $c instanceof \\OCP\\Calendar\\ICreateFromString\n        && ($c->getPermissions() & \\OCP\\Constants::PERMISSION_UPDATE) !== 0;\n}","tryCatchPattern":"try {\n    $object->put($data);\n} catch (Sabre\\DAV\\Exception\\Forbidden $e) {\n    // read-only object; switch this calendar to read-only sync mode\n}","preventionTips":["Providers: implement ICreateFromString and set PERMISSION_UPDATE to enable PUT","Clients: read the {DAV:}write-content privilege from getACL() before writing","Treat 403 as a stable capability, not a transient failure"],"tags":["php","webdav","caldav","nextcloud","app-calendar","readonly","http-403"],"backgroundTag":"read-only-calendar","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}