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
|
||||
$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()
|
||||
->whereKey($meta->subscription_id)
|
||||
->value('source');
|
||||
}
|
||||
|
||||
if (!$isRemote && $isShared) {
|
||||
$shareUrl = route('calendar.ics', ['calendarUri' => $instance->uri]);
|
||||
}
|
||||
|
||||
return $this->frame(
|
||||
'calendar.settings.calendar',
|
||||
[
|
||||
@ -175,6 +183,7 @@ class CalendarSettingsController extends Controller
|
||||
'instance' => $instance,
|
||||
'meta' => $meta,
|
||||
'icsUrl' => $icsUrl,
|
||||
'shareUrl' => $shareUrl,
|
||||
'userTz' => $user->timezone,
|
||||
]);
|
||||
}
|
||||
@ -195,6 +204,7 @@ class CalendarSettingsController extends Controller
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'timezone' => ['nullable', 'string', 'max:64'],
|
||||
'color' => ['nullable', 'regex:/^#[0-9A-F]{6}$/i'],
|
||||
'is_shared' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$timezone = filled($data['timezone'] ?? null)
|
||||
@ -204,6 +214,8 @@ class CalendarSettingsController extends Controller
|
||||
$color = $data['color'] ?? $instance->resolvedColor();
|
||||
|
||||
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)
|
||||
$instance->update([
|
||||
@ -220,6 +232,7 @@ class CalendarSettingsController extends Controller
|
||||
'title' => $data['displayname'],
|
||||
'color' => $color,
|
||||
'color_fg' => contrast_text_color($color),
|
||||
'is_shared' => $isShared,
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CalendarInstance;
|
||||
use App\Models\EventMeta;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Carbon\Carbon;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
@ -12,9 +13,17 @@ class IcsController extends Controller
|
||||
{
|
||||
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';
|
||||
|
||||
$ical = $this->generateICalendarFeed($calendar->events, $timezone);
|
||||
@ -40,7 +49,9 @@ class IcsController extends Controller
|
||||
try {
|
||||
$parsed = Reader::read($ical);
|
||||
foreach ($parsed->select('VEVENT') as $vevent) {
|
||||
$vcalendar->add(clone $vevent);
|
||||
$cloned = clone $vevent;
|
||||
$this->applyLocationProperties($cloned, $event->meta);
|
||||
$vcalendar->add($cloned);
|
||||
}
|
||||
continue;
|
||||
} catch (\Throwable $e) {
|
||||
@ -62,15 +73,76 @@ class IcsController extends Controller
|
||||
$vevent->add('DESCRIPTION', $meta->description ?? '');
|
||||
$vevent->add('DTSTART', $start, ['TZID' => $tz]);
|
||||
$vevent->add('DTEND', $end, ['TZID' => $tz]);
|
||||
$vevent->add('DTSTAMP', Carbon::parse($event->lastmodified)->utc());
|
||||
if ($meta->location) {
|
||||
$vevent->add('LOCATION', $meta->location);
|
||||
if ($event->lastmodified) {
|
||||
$vevent->add('DTSTAMP', Carbon::createFromTimestamp($event->lastmodified)->utc());
|
||||
}
|
||||
$this->applyLocationProperties($vevent, $meta);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? '');
|
||||
|
||||
@ -16,6 +16,10 @@ return [
|
||||
'create' => 'Create calendar',
|
||||
'description' => 'Description',
|
||||
'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_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',
|
||||
'description' => 'Descrizione',
|
||||
'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_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;
|
||||
|
||||
$isRemote = (bool) ($meta?->is_remote ?? false);
|
||||
$isShared = (bool) ($meta?->is_shared ?? false);
|
||||
$shareUrl = $data['shareUrl'] ?? null;
|
||||
|
||||
$color = old('color', $instance->resolvedColor());
|
||||
|
||||
@ -75,6 +77,37 @@
|
||||
</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)
|
||||
<div class="input-row input-row--1">
|
||||
<div class="input-cell">
|
||||
|
||||
@ -161,7 +161,8 @@ Route::match(
|
||||
->withoutMiddleware([VerifyCsrfToken::class]);
|
||||
|
||||
// our subscriptions
|
||||
Route::get('/ics/{calendarUri}.ics', [IcsController::class, 'download']);
|
||||
Route::get('/ics/{calendarUri}.ics', [IcsController::class, 'download'])
|
||||
->name('calendar.ics');
|
||||
|
||||
// remote subscriptions
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user