diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b0409bc4c7..082a84949d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,6 +15,7 @@ use OCA\Calendar\Listener\UserDeletedListener; use OCA\Calendar\Notification\Notifier; use OCA\Calendar\Profile\AppointmentsAction; +use OCA\Calendar\Reference\EventReferenceProvider; use OCA\Calendar\Reference\ReferenceProvider; use OCA\Calendar\UserMigration\Migrator; use OCP\AppFramework\App; @@ -52,6 +53,8 @@ public function register(IRegistrationContext $context): void { $context->registerProfileLinkAction(AppointmentsAction::class); + $context->registerReferenceProvider(EventReferenceProvider::class); + $context->registerReferenceProvider(ReferenceProvider::class); $context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class); diff --git a/lib/Reference/EventReferenceProvider.php b/lib/Reference/EventReferenceProvider.php new file mode 100644 index 0000000000..ba2b96fb81 --- /dev/null +++ b/lib/Reference/EventReferenceProvider.php @@ -0,0 +1,212 @@ +l10n->t('Calendar event'); + } + + #[\Override] + public function getOrder(): int { + return 21; + } + + #[\Override] + public function getIconUrl(): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'calendar-dark.svg') + ); + } + + #[\Override] + public function matchReference(string $referenceText): bool { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + foreach ([$start, $startIndex] as $base) { + $quoted = preg_quote($base, '/'); + + // URL pattern 1: .../apps/calendar/edit/{objectId} + // URL pattern 2: .../apps/calendar/edit/{objectId}/{recurrenceId} + if (preg_match('/^' . $quoted . '\/edit\/[^\/?#]+$/i', $referenceText) === 1) { + return true; + } + + // URL pattern 3: .../apps/calender/{view}/{timeRange}/edit/{mode}/{objectId}/{recurrenceId} + $views = 'timeGridDay|timeGridWeek|dayGridMonth|multiMonthYear|listMonth'; + if (preg_match('/^' . $quoted . '\/(?:' . $views . ')\/[^\/]+\/edit\/(?:popover|full)\/[^\/?#]+/i', $referenceText) === 1) { + return true; + } + } + + return false; + } + + #[\Override] + public function resolveReference(string $referenceText): ?IReference { + if ($this->userId === null || !$this->matchReference($referenceText)) { + return null; + } + + $objectId = $this->getObjectIdFromUrl($referenceText); + if ($objectId === null) { + return null; + } + + // objectId is base64(davUrl) + $davUrl = base64_decode($objectId, true); + if ($davUrl === false) { + return null; + } + + // DAV URL format: /remote.php/dav/calendars/{userid}/{calendarUri}/{eventFile}.ics + $parts = explode('/', trim($davUrl, '/')); + if (count($parts) < 2) { + return null; + } + $eventFile = array_pop($parts); // e.g. 'event.ics' + $calendarUri = array_pop($parts); // e.g. 'personal' + if (empty($calendarUri) || empty($eventFile)) { + return null; + } + + $calendar = $this->getCalendar($calendarUri); + if ($calendar->isDeleted()) { + return null; + } + + $eventData = $this->getEventData($calendar, $eventFile); + if ($eventData === null) { + return null; + } + + $reference = new Reference($referenceText); + $reference->setTitle($eventData['title']); + $reference->setDescription($eventData['date'] ?? $calendar->getDisplayName()); + $reference->setRichObject( + 'calendar_event', + [ + 'title' => $eventData['title'], + 'calendarName' => $calendar->getDisplayName(), + 'calendarColor' => $calendar->getDisplayColor(), + 'date' => $eventData['date'], + 'startTimestamp' => $eventData['startTimestamp'], + 'endTimestamp' => $eventData['endTimestamp'], + 'url' => $referenceText, + ] + ); + return $reference; + } + + private function getObjectIdFromUrl(string $url): ?string { + // URL pattern 1+2: .../apps/calendar/edit/{objectId}[/{recurrenceId}] + if (preg_match('/\/edit\/([^\/?#]+)/i', $url, $matches) === 1) { + if (in_array($matches[1], ['popover', 'full'], true)) { + // URL pattern 3: .../apps/calender/{view}/{timeRange}/edit/{mode}/{objectId}/{recurrenceId} + if (preg_match('/\/edit\/(?:popover|full)\/([^\/?#]+)/i', $url, $m2)) { + return $m2[1]; + } + return null; + } + return $matches[1]; + } + return null; + } + + private function getCalendar(string $calendarUri): ICalendar { + $principalUri = 'principals/users/' . $this->userId; + $calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$calendarUri]); + return $calendars[0]; + } + + private function getEventData(ICalendar $calendar, string $eventFile): ?array { + $event = null; + foreach ($calendar->search('') as $result) { + if (($result['uri'] ?? null) === $eventFile) { + $event = $result; + break; + } + } + if ($event === null) { + return null; + } + + $object = $event['objects'][0] ?? null; + if ($object === null) { + return null; + + } + + $date = null; + $startTimestamp = null; + /** @var \DateTimeInterface|null $dtStart */ + $dtStart = $object['DTSTART'][0] ?? null; + if ($dtStart instanceof \DateTimeInterface) { + $date = $this->dateTimeFormatter->formatTimeSpan(\DateTime::createFromInterface($dtStart)); + $startTimestamp = $dtStart->getTimestamp(); + } + + $endTimestamp = null; + /** @var \DateTimeInterface|null $dtEnd */ + $dtEnd = $object['DTEND'][0] ?? null; + if ($dtEnd instanceof \DateTimeInterface) { + $endTimestamp = $dtEnd->getTimestamp(); + } elseif ($startTimestamp !== null) { + $duration = $object['DURATION'][0] ?? null; + if ($duration instanceof \DateInterval) { + $endTimestamp = (new \DateTime())->setTimestamp($startTimestamp)->add($duration)->getTimestamp(); + } + } + + return [ + 'title' => $object['SUMMARY'][0] ?? $this->l10n->t('Untitled event'), + 'date' => $date, + 'startTimestamp' => $startTimestamp, + 'endTimestamp' => $endTimestamp, + 'location' => $object['LOCATION'][0] ?? null, + ]; + } + + #[\Override] + public function getCachePrefix(string $referenceId): string { + return $this->userId ?? ''; + } + + #[\Override] + public function getCacheKey(string $referenceId): string { + return $referenceId; + } +} diff --git a/psalm.xml b/psalm.xml index 5080548c66..ec0ab1de0b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -37,6 +37,7 @@ + @@ -45,6 +46,7 @@ + diff --git a/src/reference.js b/src/reference.js index 23b70af59c..be8fcf1715 100644 --- a/src/reference.js +++ b/src/reference.js @@ -39,3 +39,16 @@ registerWidget('calendar_widget', async (el, { richObjectType, richObject, acces }, (el, renderResult) => { renderResult.object.$destroy() }, true) + +registerWidget('calendar_event', async (el, { richObject }) => { + const { createApp } = await import('vue') + const { default: EventReferenceWidget } = await import('./views/EventReferenceWidget.vue') + + const app = createApp(EventReferenceWidget, { + richObject, + }) + app.mount(el) + return new NcCustomPickerRenderResult(el, app) +}, (el, renderResult) => { + renderResult.object.$destroy() +}) diff --git a/src/views/EventReferenceWidget.vue b/src/views/EventReferenceWidget.vue new file mode 100644 index 0000000000..f4876185ae --- /dev/null +++ b/src/views/EventReferenceWidget.vue @@ -0,0 +1,163 @@ + + + + + + + + + + + {{ richObject.title }} + • + {{ t('calendar', 'In calendar') }} + + {{ richObject.calendarName }} + + + {{ t('calendar', 'Start: {date}', { date: richObject.date }) }} + + • + {{ duration }} + + + + + + + + +