Al-HUWAITI Shell
Al-huwaiti


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/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/edevmultrx/www/terra-d-oro/api/ical-feed.php
<?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;
}

Al-HUWAITI Shell