231 lines
7.1 KiB
PHP
231 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\NuqloudMasterCalendar\DAV;
|
|
|
|
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
|
|
use OCA\DAV\CalDAV\Plugin;
|
|
use OCA\NuqloudMasterCalendar\AppInfo\Application;
|
|
use OCA\NuqloudMasterCalendar\Calendar\SourceCalendarService;
|
|
use OCP\IL10N;
|
|
use Sabre\CalDAV\CalendarQueryValidator;
|
|
use Sabre\CalDAV\ICalendarObject;
|
|
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
|
use Sabre\DAV\Exception\Forbidden;
|
|
use Sabre\DAV\Exception\NotFound;
|
|
use Sabre\DAV\PropPatch;
|
|
use Sabre\VObject\Component;
|
|
use Sabre\VObject\Component\VCalendar;
|
|
use Sabre\VObject\Reader;
|
|
|
|
class MasterCalendar extends ExternalCalendar {
|
|
public const URI = 'nuqloud-master-calendar';
|
|
|
|
public function __construct(
|
|
private string $principalUri,
|
|
private string $mode,
|
|
private SourceCalendarService $sourceCalendars,
|
|
private IL10N $l,
|
|
) {
|
|
parent::__construct(Application::APP_ID, self::URI);
|
|
}
|
|
|
|
public function getOwner(): string {
|
|
return $this->principalUri;
|
|
}
|
|
|
|
public function getGroup(): ?string {
|
|
return null;
|
|
}
|
|
|
|
public function getACL(): array {
|
|
return [[
|
|
'privilege' => '{DAV:}read',
|
|
'principal' => $this->principalUri,
|
|
'protected' => true,
|
|
], [
|
|
'privilege' => '{DAV:}write-properties',
|
|
'principal' => $this->principalUri,
|
|
'protected' => true,
|
|
]];
|
|
}
|
|
|
|
public function setACL(array $acl): void {
|
|
throw new Forbidden('Setting ACL is not supported on this calendar');
|
|
}
|
|
|
|
public function getSupportedPrivilegeSet(): ?array {
|
|
return null;
|
|
}
|
|
|
|
public function getLastModified(): ?int {
|
|
return null;
|
|
}
|
|
|
|
public function delete(): void {
|
|
throw new Forbidden('Deleting this calendar is not supported');
|
|
}
|
|
|
|
public function createFile($name, $data = null): void {
|
|
throw new Forbidden('Creating events in this calendar is not supported');
|
|
}
|
|
|
|
public function getProperties($properties): array {
|
|
return [
|
|
'{DAV:}displayname' => $this->l->t('NuQloud Master Calendar'),
|
|
'{http://owncloud.org/ns}calendar-enabled' => false,
|
|
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VTODO', 'VJOURNAL']),
|
|
'{http://sabredav.org/ns}read-only' => true,
|
|
];
|
|
}
|
|
|
|
public function propPatch(PropPatch $propPatch): void {
|
|
}
|
|
|
|
public function childExists($name): bool {
|
|
try {
|
|
$this->getChild($name);
|
|
return true;
|
|
} catch (NotFound) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function getChild($name): MasterCalendarObject {
|
|
if (!is_string($name) || $name === '' || str_contains($name, '/')) {
|
|
throw new NotFound('Calendar object not found');
|
|
}
|
|
|
|
$components = array_values(array_filter(
|
|
$this->getComponents(),
|
|
static fn (Component $component): bool => isset($component->{'X-FILENAME'})
|
|
&& (string)$component->{'X-FILENAME'} === $name,
|
|
));
|
|
if ($components === []) {
|
|
throw new NotFound('Calendar object not found');
|
|
}
|
|
return new MasterCalendarObject($this, new VCalendar($components));
|
|
}
|
|
|
|
/** @return MasterCalendarObject[] */
|
|
public function getChildren(): array {
|
|
$groups = [];
|
|
foreach ($this->getComponents() as $component) {
|
|
$uid = (string)$component->UID;
|
|
$groups[$uid][] = $component;
|
|
}
|
|
return array_map(fn (array $components): MasterCalendarObject => new MasterCalendarObject($this, new VCalendar($components)), $groups);
|
|
}
|
|
|
|
public function calendarQuery(array $filters): array {
|
|
$result = [];
|
|
$validator = new CalendarQueryValidator();
|
|
foreach ($this->getChildren() as $object) {
|
|
$calendarData = Reader::read($object->get());
|
|
if ($validator->validate($calendarData, $filters)) {
|
|
$result[] = $object->getName();
|
|
}
|
|
$calendarData->destroy();
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/** @return Component[] */
|
|
private function getComponents(string $pattern = '', array $searchProperties = []): array {
|
|
$components = [];
|
|
$identifier = str_ends_with($pattern, '.ics') ? substr($pattern, 0, -4) : $pattern;
|
|
$decoded = $this->decodeMasterIdentifier($identifier);
|
|
|
|
foreach ($this->sourceCalendars->getCalendars($this->principalUri, $this->mode) as $calendar) {
|
|
if ($decoded !== null && $decoded['source'] !== $this->sourceCalendars->identifier($calendar)) {
|
|
continue;
|
|
}
|
|
$sourcePattern = $decoded['uid'] ?? $pattern;
|
|
$sourceProperties = $decoded === null ? $searchProperties : ['UID'];
|
|
foreach ($calendar->search($sourcePattern, $sourceProperties) as $result) {
|
|
$name = strtoupper((string)($result['type'] ?? 'VEVENT'));
|
|
if (!in_array($name, ['VEVENT', 'VTODO', 'VJOURNAL'], true)) {
|
|
continue;
|
|
}
|
|
foreach (($result['objects'] ?? [$result]) as $object) {
|
|
if (!is_array($object) || !isset($object['UID'][0])) {
|
|
continue;
|
|
}
|
|
$sourceUid = (string)$object['UID'][0];
|
|
$uid = $this->encodeMasterIdentifier($this->sourceCalendars->identifier($calendar), $sourceUid);
|
|
$object['X-NUQLOUD-SOURCE-UID'] = [$sourceUid, []];
|
|
$object['UID'] = [$uid, []];
|
|
$object['X-FILENAME'] = [$uid . '.ics', []];
|
|
$component = $this->toComponent($name, $object);
|
|
if ($component->select('UID') !== []) {
|
|
$components[] = $component;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $components;
|
|
}
|
|
|
|
/** @param array<string, mixed> $data */
|
|
private function toComponent(string $name, array $data): Component {
|
|
$calendar = new VCalendar();
|
|
$component = $calendar->add($name);
|
|
$this->addData($component, $data);
|
|
return $component;
|
|
}
|
|
|
|
/** @param array<string, mixed> $data */
|
|
private function addData(Component $component, array $data): void {
|
|
foreach ($data as $name => $value) {
|
|
if ($this->isPropertyTuple($value)) {
|
|
$component->add($name, $value[0], $value[1]);
|
|
} elseif (is_array($value) && $this->isPropertyTuple($value[0] ?? null)) {
|
|
foreach ($value as $property) {
|
|
$component->add($name, $property[0], $property[1]);
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $childData) {
|
|
if (is_array($childData)) {
|
|
$child = $component->add($name);
|
|
$this->addData($child, $childData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function isPropertyTuple(mixed $value): bool {
|
|
return is_array($value) && array_key_exists(0, $value) && array_key_exists(1, $value)
|
|
&& !is_array($value[0]) && is_array($value[1]);
|
|
}
|
|
|
|
private function encodeMasterIdentifier(string $source, string $uid): string {
|
|
return 'nuqloud-master:' . $this->base64UrlEncode($source) . ':' . $this->base64UrlEncode($uid);
|
|
}
|
|
|
|
/** @return array{source: string, uid: string}|null */
|
|
private function decodeMasterIdentifier(string $identifier): ?array {
|
|
if (!str_starts_with($identifier, 'nuqloud-master:')) {
|
|
return null;
|
|
}
|
|
$parts = explode(':', $identifier, 3);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
$source = $this->base64UrlDecode($parts[1]);
|
|
$uid = $this->base64UrlDecode($parts[2]);
|
|
return $source === null || $uid === null ? null : ['source' => $source, 'uid' => $uid];
|
|
}
|
|
|
|
private function base64UrlEncode(string $value): string {
|
|
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
|
}
|
|
|
|
private function base64UrlDecode(string $value): ?string {
|
|
$padding = strlen($value) % 4;
|
|
$decoded = base64_decode(strtr($value . str_repeat('=', $padding === 0 ? 0 : 4 - $padding), '-_', '+/'), true);
|
|
return $decoded === false ? null : $decoded;
|
|
}
|
|
}
|