exec(" CREATE TABLE IF NOT EXISTS user_calendar_integrations ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, provider VARCHAR(50) NOT NULL DEFAULT 'icloud_caldav', username VARCHAR(255) NOT NULL, secret_encrypted TEXT NOT NULL, dav_principal_url VARCHAR(1024) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL, status VARCHAR(30) DEFAULT 'connected', last_sync_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_user_provider (user_id, provider) )"); $pdo->exec(" CREATE TABLE IF NOT EXISTS pf_calendar_events ( id INT AUTO_INCREMENT PRIMARY KEY, family_id INT NOT NULL, created_by_user_id INT NOT NULL, title VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, location VARCHAR(255) DEFAULT NULL, start_at DATETIME NOT NULL, end_at DATETIME NOT NULL, is_all_day TINYINT(1) DEFAULT 0, timezone VARCHAR(64) DEFAULT 'Europe/Paris', rrule VARCHAR(500) DEFAULT NULL, status VARCHAR(50) DEFAULT 'confirmed', external_uid VARCHAR(255) DEFAULT NULL, sync_state VARCHAR(30) DEFAULT 'pending_push', deleted_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_external_uid (external_uid) )"); $pdo->exec(" CREATE TABLE IF NOT EXISTS pf_calendar_event_links ( id INT AUTO_INCREMENT PRIMARY KEY, calendar_event_id INT NOT NULL, external_uid VARCHAR(255) NOT NULL, external_etag VARCHAR(255) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_calendar_event (calendar_event_id), UNIQUE KEY uq_external_link (external_uid) )"); function ios_ok($data): void { echo json_encode(['ok' => true, 'data' => $data], JSON_UNESCAPED_UNICODE); exit; } function ios_err(string $message, int $status = 400): void { http_response_code($status); echo json_encode(['ok' => false, 'error' => $message]); exit; } function ios_body(): array { return json_decode(file_get_contents('php://input'), true) ?? []; } function ios_require_csrf(): void { $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null; if (!verify_csrf($token)) { ios_err('Token CSRF invalide', 403); } } $integrationStmt = $meta_pdo->prepare("SELECT * FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'"); $integrationStmt->execute([$userId]); $integration = $integrationStmt->fetch(); if ($action === 'events' && $method === 'GET') { $rows = $pdo->query("SELECT * FROM pf_calendar_events ORDER BY start_at ASC")->fetchAll(); ios_ok($rows); } if ($action === 'events' && $method === 'POST') { ios_require_csrf(); $d = ios_body(); if (empty($d['title']) || empty($d['start_at']) || empty($d['end_at'])) { ios_err('Champs requis manquants'); } $stmt = $pdo->prepare(" INSERT INTO pf_calendar_events (family_id, created_by_user_id, title, description, location, start_at, end_at, timezone, sync_state) VALUES (?, ?, ?, ?, ?, ?, ?, 'Europe/Paris', 'pending_push') "); $stmt->execute([$familyId, $userId, trim($d['title']), $d['description'] ?? null, $d['location'] ?? null, $d['start_at'], $d['end_at']]); ios_ok(['id' => (int)$pdo->lastInsertId()]); } if ($action === 'events' && $method === 'PUT') { ios_require_csrf(); $d = ios_body(); $id = (int)($d['id'] ?? 0); if ($id <= 0) ios_err('ID invalide'); $stmt = $pdo->prepare(" UPDATE pf_calendar_events SET title=?, description=?, location=?, start_at=?, end_at=?, updated_at=NOW(), sync_state='pending_push' WHERE id=? "); $stmt->execute([trim($d['title'] ?? ''), $d['description'] ?? null, $d['location'] ?? null, $d['start_at'] ?? null, $d['end_at'] ?? null, $id]); ios_ok(['updated' => true]); } if ($action === 'events' && $method === 'DELETE') { ios_require_csrf(); $d = ios_body(); $id = (int)($d['id'] ?? 0); if ($id <= 0) ios_err('ID invalide'); $row = $pdo->prepare("SELECT id FROM pf_calendar_events WHERE id=?"); $row->execute([$id]); if (!$row->fetch()) ios_err('Événement introuvable', 404); $pdo->prepare("UPDATE pf_calendar_events SET deleted_at=NOW(), sync_state='pending_delete' WHERE id=?")->execute([$id]); ios_ok(['deleted' => true]); } if ($action === 'sync_status' && $method === 'GET') { if (!$integration) { ios_ok(['message' => 'Aucune intégration iCloud configurée.']); } $msg = 'Connecté iCloud. Dernière synchro: ' . ($integration['last_sync_at'] ?? 'jamais'); ios_ok(['message' => $msg]); } if ($action === 'sync' && $method === 'POST') { ios_require_csrf(); if (!$integration) ios_err('Connexion iCloud non configurée.', 400); $remoteEvents = ios_fetch_remote_events($integration); $remoteByUid = []; foreach ($remoteEvents as $re) $remoteByUid[$re['external_uid']] = $re; $localStmt = $pdo->query("SELECT * FROM pf_calendar_events WHERE deleted_at IS NULL"); $localEvents = $localStmt->fetchAll(); foreach ($localEvents as $evt) { if ($evt['sync_state'] === 'pending_push' || empty($evt['external_uid'])) { $push = ios_push_event_to_remote($integration, $evt); if ($push['code'] >= 200 && $push['code'] < 300) { $uid = $evt['external_uid'] ?: ('hh-' . $evt['id'] . '@househub'); $pdo->prepare("UPDATE pf_calendar_events SET external_uid=?, sync_state='synced', updated_at=NOW() WHERE id=?")->execute([$uid, $evt['id']]); $pdo->prepare("INSERT INTO pf_calendar_event_links (calendar_event_id, external_uid, calendar_url, external_etag) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE external_etag=VALUES(external_etag), calendar_url=VALUES(calendar_url), updated_at=NOW()") ->execute([$evt['id'], $uid, $integration['calendar_url'], null]); } } } $pendingDelete = $pdo->query("SELECT id, external_uid FROM pf_calendar_events WHERE deleted_at IS NOT NULL AND sync_state='pending_delete'")->fetchAll(); foreach ($pendingDelete as $evt) { if (!empty($evt['external_uid'])) { ios_delete_remote_event($integration, $evt['external_uid']); } $pdo->prepare("DELETE FROM pf_calendar_event_links WHERE calendar_event_id=?")->execute([$evt['id']]); $pdo->prepare("DELETE FROM pf_calendar_events WHERE id=?")->execute([$evt['id']]); } foreach ($remoteEvents as $remote) { $existing = $pdo->prepare("SELECT id, updated_at FROM pf_calendar_events WHERE external_uid=? LIMIT 1"); $existing->execute([$remote['external_uid']]); $row = $existing->fetch(); if (!$row) { $pdo->prepare(" INSERT INTO pf_calendar_events (family_id, created_by_user_id, title, description, location, start_at, end_at, timezone, external_uid, sync_state) VALUES (?, ?, ?, ?, ?, ?, ?, 'Europe/Paris', ?, 'synced') ")->execute([$familyId, $userId, $remote['title'], $remote['description'], $remote['location'], $remote['start_at'], $remote['end_at'], $remote['external_uid']]); $newId = (int)$pdo->lastInsertId(); $pdo->prepare("INSERT INTO pf_calendar_event_links (calendar_event_id, external_uid, calendar_url, external_etag) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE updated_at=NOW()") ->execute([$newId, $remote['external_uid'], $integration['calendar_url'], null]); } } $meta_pdo->prepare("UPDATE user_calendar_integrations SET last_sync_at=NOW(), status='connected' WHERE id=?")->execute([$integration['id']]); ios_ok(['message' => 'Synchronisation terminée.']); } ios_err('Action inconnue', 404);