Modification des entrées du site. Il est passé sur wordpress. Problème pour l'agenda, c'est que ics -> tranformé en rss
This commit is contained in:
+161
-78
@@ -1,105 +1,188 @@
|
||||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
const SITE = 'https://www.silly.be';
|
||||
const EVENTS_API = SITE . '/agenda/@results';
|
||||
const PAGE_SIZE = 20;
|
||||
const MAX_PAGES = 2;
|
||||
|
||||
function fetch_all_events(): array
|
||||
{
|
||||
$items = [];
|
||||
for ($page = 0; $page < MAX_PAGES; $page++) {
|
||||
$bStart = $page * PAGE_SIZE;
|
||||
$url = EVENTS_API . '?b_size=' . PAGE_SIZE . '&b_start=' . $bStart;
|
||||
$data = json_decode(http_get($url), true);
|
||||
$pageItems = $data['items'] ?? [];
|
||||
if (empty($pageItems)) {
|
||||
break;
|
||||
}
|
||||
$items = array_merge($items, $pageItems);
|
||||
if (count($items) >= ($data['items_total'] ?? 0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
/**
|
||||
* Agenda : conversion de l'iCal officiel de silly.be en flux RSS.
|
||||
* Le calendrier annonce un TTL de 12 h (X-PUBLISHED-TTL:PT12H), on cache
|
||||
* donc aussi 12 h. Il ne contient que les evenements a venir (quelques
|
||||
* uns) ; l'API REST /wp-json/wp/v2/evenement liste plus d'evenements mais
|
||||
* sans aucune date (champ acf vide), donc inutilisable pour un agenda.
|
||||
* Les regles de recurrence (RRULE) ne sont pas developpees.
|
||||
*/
|
||||
const ICAL_URL = 'https://www.silly.be/agenda/?ics=1';
|
||||
const AGENDA_SELF_URL = 'https://sillycone-village.be/silly-rss/agenda.php';
|
||||
const AGENDA_TTL_SECONDS = 43200; // 12 heures
|
||||
const DISPLAY_TZ = 'Europe/Brussels';
|
||||
|
||||
/**
|
||||
* Contrairement aux actualites, l'API renvoie text_fr/items (texte complet
|
||||
* et pieces jointes) a null dans la liste paginee des evenements - il faut
|
||||
* une requete par UID pour les recuperer (c'est ce que fait le site
|
||||
* lui-meme quand on ouvre un evenement). Un evenement recurrent partage un
|
||||
* seul UID sur toutes ses occurrences, donc un cache statique evite de
|
||||
* refaire la requete pour chaque occurrence.
|
||||
* Parse un texte iCal : deplie les lignes (RFC 5545 §3.1) et renvoie la
|
||||
* liste des VEVENT, chacun sous la forme NOM => [['params' => [...], 'value' => ...], ...].
|
||||
*/
|
||||
function fetch_event_detail(string $uid): ?array
|
||||
function parse_ical_events(string $ical): array
|
||||
{
|
||||
static $cache = [];
|
||||
if (array_key_exists($uid, $cache)) {
|
||||
return $cache[$uid];
|
||||
$ical = str_replace(["\r\n", "\r"], "\n", $ical);
|
||||
$ical = preg_replace('/\n[ \t]/', '', $ical);
|
||||
|
||||
$events = [];
|
||||
$current = null;
|
||||
foreach (explode("\n", $ical) as $line) {
|
||||
$line = rtrim($line);
|
||||
if ($line === 'BEGIN:VEVENT') {
|
||||
$current = [];
|
||||
} elseif ($line === 'END:VEVENT') {
|
||||
if ($current !== null) {
|
||||
$events[] = $current;
|
||||
}
|
||||
$current = null;
|
||||
} elseif ($current !== null && $line !== '') {
|
||||
// NOM[;PARAM=VAL...]:valeur (les valeurs de parametres peuvent
|
||||
// etre entre guillemets et contenir ':' ; on coupe sur le 1er
|
||||
// ':' hors guillemets)
|
||||
if (!preg_match('/^([A-Za-z0-9-]+)((?:;(?:[^:;"]|"[^"]*")*)*):(.*)$/s', $line, $m)) {
|
||||
continue;
|
||||
}
|
||||
$params = [];
|
||||
foreach (explode(';', ltrim($m[2], ';')) as $p) {
|
||||
if (strpos($p, '=') !== false) {
|
||||
[$k, $v] = explode('=', $p, 2);
|
||||
$params[strtoupper($k)] = trim($v, '"');
|
||||
}
|
||||
}
|
||||
$current[strtoupper($m[1])][] = ['params' => $params, 'value' => $m[3]];
|
||||
}
|
||||
}
|
||||
try {
|
||||
$url = EVENTS_API . '?UID=' . rawurlencode($uid) . '&fullobjects=1';
|
||||
$data = json_decode(http_get($url), true);
|
||||
$cache[$uid] = $data['items'][0] ?? null;
|
||||
} catch (Throwable $e) {
|
||||
$cache[$uid] = null;
|
||||
}
|
||||
return $cache[$uid];
|
||||
return $events;
|
||||
}
|
||||
|
||||
function format_event_meta(array $item): string
|
||||
/** Deséchappe une valeur TEXT iCal : \\ \, \; \n. */
|
||||
function ical_unescape(string $text): string
|
||||
{
|
||||
$parts = [];
|
||||
$start = !empty($item['start']) ? new DateTime($item['start']) : null;
|
||||
$end = !empty($item['end']) ? new DateTime($item['end']) : null;
|
||||
return preg_replace_callback('/\\\\([nN,;\\\\])/', function ($m) {
|
||||
return ($m[1] === 'n' || $m[1] === 'N') ? "\n" : $m[1];
|
||||
}, $text);
|
||||
}
|
||||
|
||||
if ($start) {
|
||||
$parts[] = !empty($item['whole_day'])
|
||||
? 'Le ' . $start->format('d/m/Y')
|
||||
: 'Le ' . $start->format('d/m/Y à H:i');
|
||||
function ical_text(array $event, string $name): string
|
||||
{
|
||||
return isset($event[$name][0]) ? trim(ical_unescape($event[$name][0]['value'])) : '';
|
||||
}
|
||||
|
||||
/** Renvoie [DateTimeImmutable|null, bool $allDay] pour DTSTART/DTEND/CREATED. */
|
||||
function ical_date(array $event, string $name): array
|
||||
{
|
||||
if (!isset($event[$name][0])) {
|
||||
return [null, false];
|
||||
}
|
||||
if ($end && (!$start || $end->format('Y-m-d') !== $start->format('Y-m-d'))) {
|
||||
$parts[] = "jusqu'au " . $end->format('d/m/Y H:i');
|
||||
$prop = $event[$name][0];
|
||||
$value = trim($prop['value']);
|
||||
$display = new DateTimeZone(DISPLAY_TZ);
|
||||
try {
|
||||
if (($prop['params']['VALUE'] ?? '') === 'DATE' || preg_match('/^\d{8}$/', $value)) {
|
||||
return [new DateTimeImmutable($value, $display), true];
|
||||
}
|
||||
if (substr($value, -1) === 'Z') {
|
||||
$dt = new DateTimeImmutable(substr($value, 0, -1), new DateTimeZone('UTC'));
|
||||
} else {
|
||||
$tz = new DateTimeZone($prop['params']['TZID'] ?? DISPLAY_TZ);
|
||||
$dt = new DateTimeImmutable($value, $tz);
|
||||
}
|
||||
return [$dt->setTimezone($display), false];
|
||||
} catch (Exception $e) {
|
||||
return [null, false];
|
||||
}
|
||||
$address = implode(', ', array_filter([$item['street'] ?? null, $item['zipcode'] ?? null, $item['city'] ?? null]));
|
||||
if ($address) {
|
||||
$parts[] = "Lieu : $address";
|
||||
}
|
||||
|
||||
function format_event_dates(?DateTimeImmutable $start, ?DateTimeImmutable $end, bool $allDay): string
|
||||
{
|
||||
if (!$start) {
|
||||
return '';
|
||||
}
|
||||
return implode(' — ', $parts);
|
||||
if ($allDay) {
|
||||
// En iCal, DTEND d'un evenement "journee entiere" est exclusif.
|
||||
$last = $end ? $end->modify('-1 day') : $start;
|
||||
if ($last <= $start) {
|
||||
return 'Le ' . $start->format('d/m/Y');
|
||||
}
|
||||
return 'Du ' . $start->format('d/m/Y') . ' au ' . $last->format('d/m/Y');
|
||||
}
|
||||
if (!$end || $end == $start) {
|
||||
return 'Le ' . $start->format('d/m/Y à H:i');
|
||||
}
|
||||
if ($end->format('Y-m-d') === $start->format('Y-m-d')) {
|
||||
return 'Le ' . $start->format('d/m/Y') . ' de ' . $start->format('H:i') . ' à ' . $end->format('H:i');
|
||||
}
|
||||
return 'Du ' . $start->format('d/m/Y à H:i') . ' au ' . $end->format('d/m/Y à H:i');
|
||||
}
|
||||
|
||||
/** Texte brut -> HTML : paragraphes sur lignes vides, <br> sur retours simples, URLs cliquables. */
|
||||
function text_to_html(string $text): string
|
||||
{
|
||||
$html = '';
|
||||
foreach (preg_split('/\n{2,}/', trim($text)) as $para) {
|
||||
$para = trim($para);
|
||||
if ($para === '') {
|
||||
continue;
|
||||
}
|
||||
$safe = htmlspecialchars($para, ENT_QUOTES, 'UTF-8');
|
||||
$safe = preg_replace('#(https?://[^\s<]+)#', '<a href="$1">$1</a>', $safe);
|
||||
$html .= '<p>' . nl2br($safe, false) . '</p>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
function build_events_feed(): string
|
||||
{
|
||||
$items = [];
|
||||
foreach (fetch_all_events() as $item) {
|
||||
$id = basename(rtrim($item['@id'] ?? '', '/'));
|
||||
$title = $item['title_fr'] ?? $item['title'] ?? '(sans titre)';
|
||||
$uid = $item['UID'] ?? $id;
|
||||
$meta = format_event_meta($item);
|
||||
$detail = fetch_event_detail($uid);
|
||||
$ical = http_get(ICAL_URL, ['Accept: text/calendar, */*']);
|
||||
if (strpos($ical, 'BEGIN:VCALENDAR') === false) {
|
||||
throw new RuntimeException('Reponse inattendue : pas un calendrier iCal');
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
foreach (parse_ical_events($ical) as $event) {
|
||||
[$start, $allDay] = ical_date($event, 'DTSTART');
|
||||
[$end] = ical_date($event, 'DTEND');
|
||||
[$created] = ical_date($event, 'CREATED');
|
||||
|
||||
$title = ical_text($event, 'SUMMARY') ?: '(sans titre)';
|
||||
$uid = ical_text($event, 'UID') ?: $title . '-' . ($start ? $start->format('c') : '');
|
||||
$url = ical_text($event, 'URL');
|
||||
$location = ical_text($event, 'LOCATION');
|
||||
|
||||
$guid = $uid;
|
||||
if (isset($seen[$guid])) { // occurrence d'un evenement repete : meme UID
|
||||
$guid .= '-' . ($start ? $start->format('YmdHi') : count($seen));
|
||||
}
|
||||
$seen[$guid] = true;
|
||||
|
||||
$meta = format_event_dates($start, $end, $allDay);
|
||||
if ($location !== '') {
|
||||
$meta .= ($meta !== '' ? ' — ' : '') . 'Lieu : ' . $location;
|
||||
}
|
||||
$description = ($meta !== '' ? '<p>' . htmlspecialchars($meta, ENT_QUOTES, 'UTF-8') . '</p>' : '')
|
||||
. full_body_html($detail ?? $item, $title);
|
||||
// Un evenement recurrent partage le meme UID sur toutes ses
|
||||
// occurrences (memes UID, dates differentes) : on suffixe le guid
|
||||
// avec la date de debut pour rester unique par occurrence, comme
|
||||
// l'exige la spec RSS.
|
||||
$items[] = [
|
||||
'title' => $title,
|
||||
'link' => SITE . '/agenda/' . slugify($title) . '?u=' . rawurlencode($uid),
|
||||
'guid' => $uid . '-' . ($item['start'] ?? ''),
|
||||
'pubDate' => $item['created'] ?? $item['start'] ?? null,
|
||||
'description' => $description,
|
||||
. text_to_html(ical_text($event, 'DESCRIPTION'));
|
||||
|
||||
$pub = $created ?? $start;
|
||||
$entries[] = [
|
||||
'sort' => $start ? $start->getTimestamp() : PHP_INT_MAX,
|
||||
'item' => [
|
||||
'title' => $title,
|
||||
'link' => $url !== '' ? $url : 'https://www.silly.be/agenda/',
|
||||
'guid' => $guid,
|
||||
'pubDate' => $pub ? $pub->format('c') : null,
|
||||
'description' => $description,
|
||||
],
|
||||
];
|
||||
}
|
||||
usort($entries, fn($a, $b) => $a['sort'] <=> $b['sort']);
|
||||
|
||||
return build_rss(
|
||||
'Silly.be - Agenda',
|
||||
SITE . '/agenda',
|
||||
"Agenda / événements de la commune de Silly (Belgique)",
|
||||
$items,
|
||||
'https://sillycone-village.be/silly-rss/agenda.php'
|
||||
'https://www.silly.be/agenda/',
|
||||
'Agenda / événements de la commune de Silly (Belgique)',
|
||||
array_column($entries, 'item'),
|
||||
AGENDA_SELF_URL
|
||||
);
|
||||
}
|
||||
|
||||
serve_cached_feed('agenda', 'build_events_feed');
|
||||
serve_cached_feed('agenda', 'build_events_feed', AGENDA_TTL_SECONDS);
|
||||
|
||||
Reference in New Issue
Block a user