ICS controller cleanup and improvements to location metadata in VEVENT
This commit is contained in:
parent
5f4cffd5aa
commit
ef658d1c04
@ -161,12 +161,20 @@ class CalendarSettingsController extends Controller
|
|||||||
|
|
||||||
// if it's remote
|
// if it's remote
|
||||||
$icsUrl = null;
|
$icsUrl = null;
|
||||||
if (($meta?->is_remote ?? false) && $meta?->subscription_id) {
|
$shareUrl = null;
|
||||||
|
$isRemote = (bool) ($meta?->is_remote ?? false);
|
||||||
|
$isShared = (bool) ($meta?->is_shared ?? false);
|
||||||
|
|
||||||
|
if ($isRemote && $meta?->subscription_id) {
|
||||||
$icsUrl = Subscription::query()
|
$icsUrl = Subscription::query()
|
||||||
->whereKey($meta->subscription_id)
|
->whereKey($meta->subscription_id)
|
||||||
->value('source');
|
->value('source');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$isRemote && $isShared) {
|
||||||
|
$shareUrl = route('calendar.ics', ['calendarUri' => $instance->uri]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->frame(
|
return $this->frame(
|
||||||
'calendar.settings.calendar',
|
'calendar.settings.calendar',
|
||||||
[
|
[
|
||||||
@ -175,6 +183,7 @@ class CalendarSettingsController extends Controller
|
|||||||
'instance' => $instance,
|
'instance' => $instance,
|
||||||
'meta' => $meta,
|
'meta' => $meta,
|
||||||
'icsUrl' => $icsUrl,
|
'icsUrl' => $icsUrl,
|
||||||
|
'shareUrl' => $shareUrl,
|
||||||
'userTz' => $user->timezone,
|
'userTz' => $user->timezone,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -195,6 +204,7 @@ class CalendarSettingsController extends Controller
|
|||||||
'description' => ['nullable', 'string', 'max:500'],
|
'description' => ['nullable', 'string', 'max:500'],
|
||||||
'timezone' => ['nullable', 'string', 'max:64'],
|
'timezone' => ['nullable', 'string', 'max:64'],
|
||||||
'color' => ['nullable', 'regex:/^#[0-9A-F]{6}$/i'],
|
'color' => ['nullable', 'regex:/^#[0-9A-F]{6}$/i'],
|
||||||
|
'is_shared' => ['nullable', 'boolean'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$timezone = filled($data['timezone'] ?? null)
|
$timezone = filled($data['timezone'] ?? null)
|
||||||
@ -204,6 +214,8 @@ class CalendarSettingsController extends Controller
|
|||||||
$color = $data['color'] ?? $instance->resolvedColor();
|
$color = $data['color'] ?? $instance->resolvedColor();
|
||||||
|
|
||||||
DB::transaction(function () use ($instance, $data, $timezone, $color) {
|
DB::transaction(function () use ($instance, $data, $timezone, $color) {
|
||||||
|
$isRemote = (bool) ($instance->meta?->is_remote ?? false);
|
||||||
|
$isShared = $isRemote ? false : (bool) ($data['is_shared'] ?? false);
|
||||||
|
|
||||||
// update sabre calendar instance (dav-facing)
|
// update sabre calendar instance (dav-facing)
|
||||||
$instance->update([
|
$instance->update([
|
||||||
@ -220,6 +232,7 @@ class CalendarSettingsController extends Controller
|
|||||||
'title' => $data['displayname'],
|
'title' => $data['displayname'],
|
||||||
'color' => $color,
|
'color' => $color,
|
||||||
'color_fg' => contrast_text_color($color),
|
'color_fg' => contrast_text_color($color),
|
||||||
|
'is_shared' => $isShared,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\CalendarInstance;
|
use App\Models\CalendarInstance;
|
||||||
|
use App\Models\EventMeta;
|
||||||
use Illuminate\Support\Facades\Response;
|
use Illuminate\Support\Facades\Response;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Sabre\VObject\Component\VCalendar;
|
use Sabre\VObject\Component\VCalendar;
|
||||||
@ -12,9 +13,17 @@ class IcsController extends Controller
|
|||||||
{
|
{
|
||||||
public function download(string $calendarUri)
|
public function download(string $calendarUri)
|
||||||
{
|
{
|
||||||
$instance = CalendarInstance::where('uri', $calendarUri)->firstOrFail();
|
$instance = CalendarInstance::where('uri', $calendarUri)
|
||||||
|
->with('meta')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
$calendar = $instance->calendar()->with(['events.meta'])->firstOrFail();
|
$isRemote = (bool) ($instance->meta?->is_remote ?? false);
|
||||||
|
$isShared = (bool) ($instance->meta?->is_shared ?? false);
|
||||||
|
if ($isRemote || !$isShared) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$calendar = $instance->calendar()->with(['events.meta.venue'])->firstOrFail();
|
||||||
$timezone = $instance->timezone ?? 'UTC';
|
$timezone = $instance->timezone ?? 'UTC';
|
||||||
|
|
||||||
$ical = $this->generateICalendarFeed($calendar->events, $timezone);
|
$ical = $this->generateICalendarFeed($calendar->events, $timezone);
|
||||||
@ -40,7 +49,9 @@ class IcsController extends Controller
|
|||||||
try {
|
try {
|
||||||
$parsed = Reader::read($ical);
|
$parsed = Reader::read($ical);
|
||||||
foreach ($parsed->select('VEVENT') as $vevent) {
|
foreach ($parsed->select('VEVENT') as $vevent) {
|
||||||
$vcalendar->add(clone $vevent);
|
$cloned = clone $vevent;
|
||||||
|
$this->applyLocationProperties($cloned, $event->meta);
|
||||||
|
$vcalendar->add($cloned);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@ -62,15 +73,76 @@ class IcsController extends Controller
|
|||||||
$vevent->add('DESCRIPTION', $meta->description ?? '');
|
$vevent->add('DESCRIPTION', $meta->description ?? '');
|
||||||
$vevent->add('DTSTART', $start, ['TZID' => $tz]);
|
$vevent->add('DTSTART', $start, ['TZID' => $tz]);
|
||||||
$vevent->add('DTEND', $end, ['TZID' => $tz]);
|
$vevent->add('DTEND', $end, ['TZID' => $tz]);
|
||||||
$vevent->add('DTSTAMP', Carbon::parse($event->lastmodified)->utc());
|
if ($event->lastmodified) {
|
||||||
if ($meta->location) {
|
$vevent->add('DTSTAMP', Carbon::createFromTimestamp($event->lastmodified)->utc());
|
||||||
$vevent->add('LOCATION', $meta->location);
|
|
||||||
}
|
}
|
||||||
|
$this->applyLocationProperties($vevent, $meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $vcalendar->serialize();
|
return $vcalendar->serialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function applyLocationProperties($vevent, ?EventMeta $meta): void
|
||||||
|
{
|
||||||
|
if (!$meta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$venue = $meta->venue;
|
||||||
|
$label = trim((string) ($meta->location_label ?? $meta->location ?? $venue?->display_name ?? ''));
|
||||||
|
|
||||||
|
$address = trim((string) ($venue?->raw_address ?? ''));
|
||||||
|
if ($address === '') {
|
||||||
|
$address = trim(implode("\n", array_filter([
|
||||||
|
$venue?->street,
|
||||||
|
trim(implode(' ', array_filter([$venue?->city, $venue?->state, $venue?->postal]))),
|
||||||
|
$venue?->country,
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
$locationText = trim(implode("\n", array_filter([
|
||||||
|
$label !== '' ? $label : null,
|
||||||
|
$address !== '' ? $address : null,
|
||||||
|
])));
|
||||||
|
|
||||||
|
if (!isset($vevent->LOCATION) && $locationText !== '') {
|
||||||
|
$vevent->add('LOCATION', $locationText);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = $venue?->lat;
|
||||||
|
$lon = $venue?->lon;
|
||||||
|
if (!is_numeric($lat) || !is_numeric($lon)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($vevent->GEO)) {
|
||||||
|
$vevent->add('GEO', sprintf('%.6f;%.6f', $lat, $lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($vevent->{'X-APPLE-STRUCTURED-LOCATION'})) {
|
||||||
|
$params = [
|
||||||
|
'VALUE' => 'URI',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($address !== '') {
|
||||||
|
$params['X-ADDRESS'] = $address;
|
||||||
|
}
|
||||||
|
if ($label !== '') {
|
||||||
|
$params['X-TITLE'] = $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($params['X-ADDRESS']) || !empty($params['X-TITLE'])) {
|
||||||
|
$params['X-APPLE-REFERENCEFRAME'] = '1';
|
||||||
|
$params['X-APPLE-RADIUS'] = '150';
|
||||||
|
$vevent->add(
|
||||||
|
'X-APPLE-STRUCTURED-LOCATION',
|
||||||
|
sprintf('geo:%.6f,%.6f', $lat, $lon),
|
||||||
|
$params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function escape(?string $text): string
|
protected function escape(?string $text): string
|
||||||
{
|
{
|
||||||
return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? '');
|
return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? '');
|
||||||
|
|||||||
@ -16,6 +16,10 @@ return [
|
|||||||
'create' => 'Create calendar',
|
'create' => 'Create calendar',
|
||||||
'description' => 'Description',
|
'description' => 'Description',
|
||||||
'ics' => [
|
'ics' => [
|
||||||
|
'share' => 'Share this calendar publicly',
|
||||||
|
'share_help' => 'Anyone with this link can subscribe to your calendar.',
|
||||||
|
'public_url' => 'Public subscription URL',
|
||||||
|
'public_url_help' => 'Copy this URL into a third-party calendar app to subscribe.',
|
||||||
'url' => 'ICS URL',
|
'url' => 'ICS URL',
|
||||||
'url_help' => 'You can\'t edit a public calendar URL. If you need to make a change, unsubscribe and add it again.',
|
'url_help' => 'You can\'t edit a public calendar URL. If you need to make a change, unsubscribe and add it again.',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -16,6 +16,10 @@ return [
|
|||||||
'create' => 'Crea calendario',
|
'create' => 'Crea calendario',
|
||||||
'description' => 'Descrizione',
|
'description' => 'Descrizione',
|
||||||
'ics' => [
|
'ics' => [
|
||||||
|
'share' => 'Condividi questo calendario pubblicamente',
|
||||||
|
'share_help' => 'Chiunque abbia questo link puo iscriversi al tuo calendario.',
|
||||||
|
'public_url' => 'URL pubblico di iscrizione',
|
||||||
|
'public_url_help' => 'Copia questo URL in un app calendario di terze parti per iscriverti.',
|
||||||
'url' => 'URL ICS',
|
'url' => 'URL ICS',
|
||||||
'url_help' => 'Non puoi modificare un URL di calendario pubblico. Se devi fare una modifica, annulla l iscrizione e aggiungilo di nuovo.',
|
'url_help' => 'Non puoi modificare un URL di calendario pubblico. Se devi fare una modifica, annulla l iscrizione e aggiungilo di nuovo.',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
$meta = $data['meta'] ?? null;
|
$meta = $data['meta'] ?? null;
|
||||||
|
|
||||||
$isRemote = (bool) ($meta?->is_remote ?? false);
|
$isRemote = (bool) ($meta?->is_remote ?? false);
|
||||||
|
$isShared = (bool) ($meta?->is_shared ?? false);
|
||||||
|
$shareUrl = $data['shareUrl'] ?? null;
|
||||||
|
|
||||||
$color = old('color', $instance->resolvedColor());
|
$color = old('color', $instance->resolvedColor());
|
||||||
|
|
||||||
@ -75,6 +77,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!$isRemote)
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<input type="hidden" name="is_shared" value="0">
|
||||||
|
<x-input.checkbox-label
|
||||||
|
name="is_shared"
|
||||||
|
value="1"
|
||||||
|
:checked="old('is_shared', $isShared)"
|
||||||
|
:label="__('calendar.ics.share')"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">{{ __('calendar.ics.share_help') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($shareUrl)
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.text-label
|
||||||
|
:label="__('calendar.ics.public_url')"
|
||||||
|
id="ics_public_url"
|
||||||
|
name="ics_public_url"
|
||||||
|
type="url"
|
||||||
|
:value="$shareUrl"
|
||||||
|
disabled="true"
|
||||||
|
:description="__('calendar.ics.public_url_help')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
@if ($isRemote)
|
@if ($isRemote)
|
||||||
<div class="input-row input-row--1">
|
<div class="input-row input-row--1">
|
||||||
<div class="input-cell">
|
<div class="input-cell">
|
||||||
|
|||||||
@ -161,7 +161,8 @@ Route::match(
|
|||||||
->withoutMiddleware([VerifyCsrfToken::class]);
|
->withoutMiddleware([VerifyCsrfToken::class]);
|
||||||
|
|
||||||
// our subscriptions
|
// our subscriptions
|
||||||
Route::get('/ics/{calendarUri}.ics', [IcsController::class, 'download']);
|
Route::get('/ics/{calendarUri}.ics', [IcsController::class, 'download'])
|
||||||
|
->name('calendar.ics');
|
||||||
|
|
||||||
// remote subscriptions
|
// remote subscriptions
|
||||||
Route::middleware(['auth'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user