Globinours/app/Services/CalendarFeedService.php

124 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
final class CalendarFeedService
{
public static function generateToken(int $userId): string
{
$token = bin2hex(random_bytes(32));
DB::pdo()
->prepare(
"UPDATE users SET agenda_feed_token_hash=:hash,agenda_feed_created_at=datetime('now'),updated_at=datetime('now') WHERE id=:id AND active=1",
)
->execute([':hash' => hash('sha256', $token), ':id' => $userId]);
return $token;
}
public static function revoke(int $userId): void
{
DB::pdo()
->prepare(
"UPDATE users SET agenda_feed_token_hash=NULL,agenda_feed_created_at=NULL,updated_at=datetime('now') WHERE id=?",
)
->execute([$userId]);
}
public static function userForToken(string $token): ?array
{
if (!preg_match('/^[a-f0-9]{64}$/', $token)) {
return null;
}
$s = DB::pdo()->prepare('SELECT id,role FROM users WHERE agenda_feed_token_hash=? AND active=1 LIMIT 1');
$s->execute([hash('sha256', $token)]);
$user = $s->fetch(PDO::FETCH_ASSOC) ?: null;
if (!$user || !PermissionService::can('agenda', 'view', (string) $user['role'])) {
return null;
}
return $user;
}
public static function absoluteUrl(string $path): string
{
$configured = rtrim((string) getenv('APP_URL'), '/');
if ($configured !== '' && preg_match('~^https?://~i', $configured)) {
return $configured . $path;
}
$proto = (string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '');
if (!in_array($proto, ['http', 'https'], true)) {
$proto = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http';
}
$host = (string) ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? ($_SERVER['HTTP_HOST'] ?? 'localhost'));
$host = trim(explode(',', $host)[0]);
if (!preg_match('/^[a-z0-9.:-]+$/i', $host)) {
$host = 'localhost';
}
return $proto . '://' . $host . $path;
}
public static function render(array $items, string $name): string
{
$lines = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Globinours//Agenda refuge//FR',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'X-WR-CALNAME:' . self::escape($name),
'X-WR-TIMEZONE:Europe/Paris',
];
$stamp = gmdate('Ymd\THis\Z');
foreach ($items as $item) {
$source = (string) ($item['source_type'] ?? 'agenda');
$sourceId = (int) ($item['source_id'] ?? $item['id']);
$instance = (string) $item['instance_starts_at'];
$uid = $source . '-' . $sourceId . '-' . substr(hash('sha256', $instance), 0, 12) . '@globinours';
$lines[] = 'BEGIN:VEVENT';
$lines[] = 'UID:' . $uid;
$lines[] = 'DTSTAMP:' . $stamp;
if ((int) $item['all_day'] === 1) {
$start = new DateTimeImmutable(substr($instance, 0, 10));
$lines[] = 'DTSTART;VALUE=DATE:' . $start->format('Ymd');
$lines[] = 'DTEND;VALUE=DATE:' . $start->modify('+1 day')->format('Ymd');
} else {
$start = new DateTimeImmutable($instance, new DateTimeZone('Europe/Paris'));
$end = !empty($item['instance_ends_at'])
? new DateTimeImmutable($item['instance_ends_at'], new DateTimeZone('Europe/Paris'))
: $start->modify('+1 hour');
$lines[] = 'DTSTART;TZID=Europe/Paris:' . $start->format('Ymd\THis');
$lines[] = 'DTEND;TZID=Europe/Paris:' . $end->format('Ymd\THis');
}
$lines[] = 'SUMMARY:' . self::escape((string) $item['title']);
$description = trim((string) ($item['description'] ?? ''));
if (!empty($item['animal_name'])) {
$description = trim($description . "\n" . t('animal.animal') . ' : ' . $item['animal_name']);
}
if ($description !== '') {
$lines[] = 'DESCRIPTION:' . self::escape($description);
}
if (!empty($item['location'])) {
$lines[] = 'LOCATION:' . self::escape((string) $item['location']);
}
if (!empty($item['animal_id'])) {
$lines[] = 'URL:' . self::escape(self::absoluteUrl('/animal?id=' . (int) $item['animal_id']));
}
$lines[] = 'STATUS:' . (($item['status'] ?? 'planned') === 'completed' ? 'COMPLETED' : 'CONFIRMED');
$lines[] = 'END:VEVENT';
}
$lines[] = 'END:VCALENDAR';
return implode("\r\n", array_map([self::class, 'fold'], $lines)) . "\r\n";
}
private static function escape(string $value): string
{
return str_replace(['\\', "\r\n", "\n", "\r", ';', ','], ['\\\\', '\\n', '\\n', '\\n', '\\;', '\\,'], $value);
}
private static function fold(string $line): string
{
$out = '';
while (strlen($line) > 73) {
$cut = 73;
while ($cut > 0 && (ord($line[$cut]) & 0xc0) === 0x80) {
$cut--;
}
$out .= substr($line, 0, $cut) . "\r\n ";
$line = substr($line, $cut);
}
return $out . $line;
}
}