Server : Apache System : Linux webd003.cluster111.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64 User : edevmultrx ( 811899) PHP Version : 7.4.33 Disable Function : _dyuweyrj4,_dyuweyrj4r,dl Directory : /home/edevmultrx/www/terra-d-oro/api/ |
<?php
/**
* Terra D'Oro — iCal Feed Endpoint (Public)
* Route: /api/ical-feed?unit=villa-folacca
* /feed/folacca.ics (via .htaccess alias — prettier URL for OTAs)
*
* Serves a RFC 5545-compliant .ics calendar for a given unit.
* Airbnb and Booking.com poll this URL to know which dates are already booked.
*
* PARAMETERS (GET):
* ?unit=SLUG e.g. ?unit=villa-folacca (preferred)
* ?unit_id=ID e.g. ?unit_id=1 (alternative)
*
* This endpoint is intentionally public — OTAs need unauthenticated access.
* The feed content reveals only SUMMARY:"Réservé / Booked", not guest names.
*
* CACHING:
* Generated output is cached in data/ical_feeds/ for ICAL_CACHE_TTL seconds
* (15 minutes). The cache is invalidated on booking creation/update via
* api/booking.php (implemented in Phase 7).
*
* CURL EXAMPLE (test from CLI):
* curl "https://terradoro.com/api/ical-feed?unit=villa-folacca" -o test.ics
*/
declare(strict_types=1);
// ── Bootstrap ─────────────────────────────────────────────────────────────────
if (!defined('ROOT_PATH')) {
define('ROOT_PATH', dirname(__DIR__));
}
require_once ROOT_PATH . '/core/config.php';
require_once ROOT_PATH . '/core/db.php';
require_once ROOT_PATH . '/core/helpers.php';
require_once ROOT_PATH . '/core/ical/ICalGenerator.php';
// ── Error response helper ─────────────────────────────────────────────────────
function ical_error(int $status, string $message): never
{
http_response_code($status);
header('Content-Type: text/plain; charset=utf-8');
echo "Error {$status}: {$message}";
exit;
}
// ── Resolve unit ──────────────────────────────────────────────────────────────
$slug = trim($_GET['unit'] ?? '');
$unit_id = (int) ($_GET['unit_id'] ?? 0);
if ($slug === '' && $unit_id === 0) {
ical_error(400, 'Missing parameter: ?unit=SLUG or ?unit_id=ID required.');
}
// Look up by slug OR by id
if ($slug !== '') {
$stmt = $db->prepare(
'SELECT id, slug, name FROM `' . TBL_UNITS . '` WHERE slug = :slug AND is_active = 1 LIMIT 1'
);
$stmt->execute([':slug' => $slug]);
} else {
$stmt = $db->prepare(
'SELECT id, slug, name FROM `' . TBL_UNITS . '` WHERE id = :id AND is_active = 1 LIMIT 1'
);
$stmt->execute([':id' => $unit_id]);
}
$unit = $stmt->fetch();
if (!$unit) {
ical_error(404, 'Unit not found or inactive.');
}
// ── Cache check ───────────────────────────────────────────────────────────────
$cacheDir = ICAL_FEEDS_PATH; // data/ical_feeds/
$cacheFile = $cacheDir . '/' . $unit['slug'] . '.ics';
// Ensure cache directory exists
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0750, true);
}
$useCache = file_exists($cacheFile)
&& (time() - filemtime($cacheFile)) < ICAL_CACHE_TTL;
if ($useCache) {
$icsContent = file_get_contents($cacheFile);
if ($icsContent !== false) {
outputIcs($icsContent, $unit['slug']);
// outputIcs exits — code below only runs on cache miss
}
}
// ── Generate fresh feed ───────────────────────────────────────────────────────
try {
$generator = new ICalGenerator($db, SITE_TIMEZONE);
$icsContent = $generator->generate((int) $unit['id']);
// Write to cache (non-fatal if it fails — we still serve the response)
@file_put_contents($cacheFile, $icsContent, LOCK_EX);
} catch (\Throwable $e) {
$detail = DEBUG ? $e->getMessage() : 'Failed to generate calendar feed.';
ical_error(500, $detail);
}
outputIcs($icsContent, $unit['slug']);
// ── Output function ───────────────────────────────────────────────────────────
/**
* Send the .ics content with appropriate headers and exit.
*
* Filename: terradoro-villa-folacca.ics (human-readable for OTA imports).
*/
function outputIcs(string $content, string $slug): never
{
$filename = 'terradoro-' . preg_replace('/[^a-z0-9\-]/', '', $slug) . '.ics';
$length = strlen($content);
// RFC 2445 content-type
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: inline; filename="' . $filename . '"');
header('Content-Length: ' . $length);
header('Cache-Control: public, max-age=' . ICAL_CACHE_TTL);
header('Vary: Accept-Encoding');
header('X-Content-Type-Options: nosniff');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
echo $content;
exit;
}