@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
$pageTitle = 'Calendrier iOS — HouseHub';
|
||||
$activePage = 'calendar-ios';
|
||||
$mainClass = 'pf-calendar-ios-page';
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="/modules/calendar-ios/assets/calendar-ios.css">
|
||||
|
||||
<div class="pf-container ios-calendar-wrap">
|
||||
<section class="pf-panel-card">
|
||||
<div class="ios-calendar-head">
|
||||
<h1>📱 <?= tr('menu_calendar_ios') ?></h1>
|
||||
<div class="ios-calendar-head-actions">
|
||||
<button class="pf-btn btn-secondary" id="ios-sync-btn">Synchroniser</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-muted-note">Créez vos événements ici, puis synchronisez-les avec iCloud CalDAV.</p>
|
||||
</section>
|
||||
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">Ajouter / Modifier un événement</h2>
|
||||
<form id="ios-event-form" class="ios-form-grid">
|
||||
<input type="hidden" id="ios-event-id">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Titre</label>
|
||||
<input class="pf-input" id="ios-title" required>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Lieu</label>
|
||||
<input class="pf-input" id="ios-location">
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Début</label>
|
||||
<input class="pf-input" id="ios-start" type="datetime-local" required>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Fin</label>
|
||||
<input class="pf-input" id="ios-end" type="datetime-local" required>
|
||||
</div>
|
||||
<div class="pf-form-group ios-form-full">
|
||||
<label class="pf-label">Description</label>
|
||||
<textarea class="pf-input" id="ios-description"></textarea>
|
||||
</div>
|
||||
<div class="ios-form-full ios-form-actions">
|
||||
<button class="pf-btn" type="submit">Enregistrer</button>
|
||||
<button class="pf-btn btn-secondary" type="button" id="ios-form-reset">Réinitialiser</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">Événements</h2>
|
||||
<div id="ios-sync-status" class="pf-muted-note"></div>
|
||||
<div id="ios-events-list" class="ios-events-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/modules/calendar-ios/assets/calendar-ios.js"></script>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/includes/meta_db.php';
|
||||
|
||||
$rows = $meta_pdo->query("SELECT user_id FROM user_calendar_integrations WHERE provider='icloud_caldav' AND status='connected'")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$userId = (int)$row['user_id'];
|
||||
$url = 'http://localhost/modules/calendar-ios/api.php?action=sync';
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'X-Requested-With: XMLHttpRequest',
|
||||
// Le cron réel devra utiliser un mécanisme d'auth/session technique.
|
||||
],
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
@@ -26,6 +26,22 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
FOREIGN KEY (family_id) REFERENCES families(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
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),
|
||||
CONSTRAINT fk_calendar_integration_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Donner au user applicatif le droit de créer des DBs famille
|
||||
GRANT ALL PRIVILEGES ON `househub_f%`.* TO 'househub'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
@@ -344,3 +344,37 @@ CREATE TABLE IF NOT EXISTS pf_todos (
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
+4
-1
@@ -45,7 +45,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<a href="/index.php" class="pf-logo">HouseHub</a>
|
||||
|
||||
<?php if (isset($_SESSION['user'])): ?>
|
||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts']; ?>
|
||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios']; ?>
|
||||
<nav class="pf-nav">
|
||||
<a href="/index.php" class="pf-nav-link <?= $activePage === 'home' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_home') ?></a>
|
||||
<?php if (in_array('calendar', $mods)): ?><a href="/family-calendar.php" class="pf-nav-link <?= $activePage === 'family-calendar' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar') ?></a><?php endif; ?>
|
||||
@@ -55,6 +55,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-nav-link <?= $activePage === 'garage' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_garage') ?></a><?php endif; ?>
|
||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-nav-link <?= $activePage === 'memo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_memo') ?></a><?php endif; ?>
|
||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-nav-link <?= $activePage === 'todo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_todo') ?></a><?php endif; ?>
|
||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-nav-link <?= $activePage === 'calendar-ios' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -102,6 +103,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-mobile-nav-link">🚗 <?= tr('menu_garage') ?></a><?php endif; ?>
|
||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-mobile-nav-link">📝 <?= tr('menu_memo') ?></a><?php endif; ?>
|
||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-mobile-nav-link">✅ <?= tr('menu_todo') ?></a><?php endif; ?>
|
||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-mobile-nav-link">📱 <?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||
<a href="/settings.php" class="pf-mobile-nav-link">⚙️ Paramètres</a>
|
||||
<?php if (!empty($_SESSION['user']['is_admin'])): ?>
|
||||
<a href="/admin/" class="pf-mobile-nav-link" style="color:#2563eb">🛡️ Admin</a>
|
||||
@@ -125,6 +127,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
|
||||
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>'
|
||||
};
|
||||
window.CSRF_TOKEN = "<?= htmlspecialchars(function_exists('csrf_token') ? csrf_token() : '') ?>";
|
||||
|
||||
function tr(key) {
|
||||
return window.I18N[key] || key;
|
||||
|
||||
@@ -32,3 +32,25 @@ function require_login(?string $loginPage = '/login.php'): void
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function verify_csrf(?string $token): bool
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (empty($_SESSION['csrf_token']) || !$token) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
function hh_encrypt_secret(string $plain): string
|
||||
{
|
||||
$keyMaterial = getenv('APP_SECRET_KEY') ?: '';
|
||||
if ($keyMaterial === '') {
|
||||
throw new RuntimeException('APP_SECRET_KEY manquant');
|
||||
}
|
||||
$key = hash('sha256', $keyMaterial, true);
|
||||
$iv = random_bytes(16);
|
||||
$cipher = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||
if ($cipher === false) {
|
||||
throw new RuntimeException('Chiffrement impossible');
|
||||
}
|
||||
return base64_encode($iv . $cipher);
|
||||
}
|
||||
|
||||
function hh_decrypt_secret(string $encrypted): string
|
||||
{
|
||||
$keyMaterial = getenv('APP_SECRET_KEY') ?: '';
|
||||
if ($keyMaterial === '') {
|
||||
throw new RuntimeException('APP_SECRET_KEY manquant');
|
||||
}
|
||||
$raw = base64_decode($encrypted, true);
|
||||
if ($raw === false || strlen($raw) <= 16) {
|
||||
throw new RuntimeException('Secret invalide');
|
||||
}
|
||||
$key = hash('sha256', $keyMaterial, true);
|
||||
$iv = substr($raw, 0, 16);
|
||||
$cipher = substr($raw, 16);
|
||||
$plain = openssl_decrypt($cipher, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||
if ($plain === false) {
|
||||
throw new RuntimeException('Déchiffrement impossible');
|
||||
}
|
||||
return $plain;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ return [
|
||||
'menu_budget' => 'Pressupost',
|
||||
'menu_holidays' => 'Viatges',
|
||||
'menu_gifts' => 'Regals',
|
||||
'menu_calendar_ios' => 'Calendari iOS',
|
||||
'nav_home' => 'Inici',
|
||||
'nav_calendar' => 'Calendari',
|
||||
'nav_budget' => 'Pressupost',
|
||||
@@ -95,6 +96,8 @@ return [
|
||||
'menu_memo' => 'Notes',
|
||||
'mod_todo_name' => 'Todo',
|
||||
'mod_todo_desc' => 'Gestioneu les vostres tasques, llistes de la compra i recordatoris familiars.',
|
||||
'mod_calendar_ios_name' => 'Calendari iOS',
|
||||
'mod_calendar_ios_desc' => 'Sincronitzeu els esdeveniments de HouseHub amb el calendari de l\'iPhone (iCloud CalDAV).',
|
||||
'menu_todo' => 'Todo',
|
||||
'cta_open' => 'Obrir',
|
||||
'cta_explore' => 'Explorar',
|
||||
@@ -103,6 +106,7 @@ return [
|
||||
'cta_service' => 'Revisar',
|
||||
'cta_jot' => 'Apuntar',
|
||||
'cta_check' => 'Marcar',
|
||||
'cta_sync' => 'Sincronitzar',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
|
||||
@@ -23,6 +23,7 @@ return [
|
||||
'menu_budget' => 'Budget',
|
||||
'menu_holidays' => 'Trips',
|
||||
'menu_gifts' => 'Gifts',
|
||||
'menu_calendar_ios' => 'iOS Calendar',
|
||||
'nav_home' => 'Home',
|
||||
'nav_calendar' => 'Calendar',
|
||||
'nav_budget' => 'Budget',
|
||||
@@ -96,6 +97,8 @@ return [
|
||||
'menu_memo' => 'Notes',
|
||||
'mod_todo_name' => 'Todo',
|
||||
'mod_todo_desc' => 'Manage tasks, shopping lists and family reminders.',
|
||||
'mod_calendar_ios_name' => 'iOS Calendar',
|
||||
'mod_calendar_ios_desc' => 'Sync HouseHub events with your iPhone calendar (iCloud CalDAV).',
|
||||
'menu_todo' => 'Todo',
|
||||
'cta_open' => 'Open',
|
||||
'cta_explore' => 'Explore',
|
||||
@@ -104,6 +107,7 @@ return [
|
||||
'cta_service' => 'Service',
|
||||
'cta_jot' => 'Jot down',
|
||||
'cta_check' => 'Check off',
|
||||
'cta_sync' => 'Sync',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
'menu_budget' => 'Budget',
|
||||
'menu_holidays' => 'Voyages',
|
||||
'menu_gifts' => 'Cadeaux',
|
||||
'menu_calendar_ios' => 'Calendrier iOS',
|
||||
'nav_home' => 'Accueil',
|
||||
'nav_calendar' => 'Calendrier',
|
||||
'nav_budget' => 'Budget',
|
||||
@@ -98,6 +99,8 @@ return [
|
||||
'menu_memo' => 'Notes',
|
||||
'mod_todo_name' => 'Todo',
|
||||
'mod_todo_desc' => 'Gérez vos tâches, listes de courses et rappels en famille.',
|
||||
'mod_calendar_ios_name' => 'Calendrier iOS',
|
||||
'mod_calendar_ios_desc' => 'Synchronisez vos événements HouseHub avec votre calendrier iPhone (iCloud CalDAV).',
|
||||
'menu_todo' => 'Todo',
|
||||
'cta_open' => 'Ouvrir',
|
||||
'cta_explore' => 'Explorer',
|
||||
@@ -106,6 +109,7 @@ return [
|
||||
'cta_service' => 'Réviser',
|
||||
'cta_jot' => 'Griffonner',
|
||||
'cta_check' => 'Cocher',
|
||||
'cta_sync' => 'Synchroniser',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
|
||||
@@ -43,7 +43,7 @@ if ($_has_custom_bg): ?>
|
||||
<section class="pf-section">
|
||||
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
|
||||
|
||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','garage']; ?>
|
||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','garage','calendar_ios']; ?>
|
||||
<div class="pf-modules-grid">
|
||||
|
||||
<?php if (in_array('calendar', $mods)): ?>
|
||||
@@ -109,6 +109,15 @@ if ($_has_custom_bg): ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array('calendar_ios', $mods)): ?>
|
||||
<a href="/calendar-ios.php" class="pf-module-card">
|
||||
<div class="pf-card-icon">📱</div>
|
||||
<h3 class="pf-card-title"><?= tr('mod_calendar_ios_name') ?></h3>
|
||||
<div class="pf-card-desc"><?= tr('mod_calendar_ios_desc') ?></div>
|
||||
<span class="pf-card-cta"><?= tr('cta_sync') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
$pageTitle = tr('login_title');
|
||||
$activePage = "login";
|
||||
@@ -14,6 +15,9 @@ if (isset($_SESSION['user'])) {
|
||||
$error = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||
$error = "Session invalide (CSRF). Rechargez la page.";
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
@@ -36,6 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif ($user['family_id'] && !$user['family_active']) {
|
||||
$error = "Espace familial désactivé. Contactez l'administrateur.";
|
||||
} else {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user'] = [
|
||||
'id' => (int)$user['id'],
|
||||
'username' => $user['username'],
|
||||
@@ -45,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
];
|
||||
$_SESSION['family_db'] = $user['db_name'];
|
||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts"]', true);
|
||||
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
||||
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
||||
header('Location: ' . $redirectTo);
|
||||
exit;
|
||||
@@ -55,6 +60,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
@@ -75,6 +81,7 @@ require __DIR__ . '/header.php';
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/login.php<?= isset($_GET['redirect']) ? '?redirect=' . urlencode($_GET['redirect']) : '' ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="username"><?= tr('label_username') ?></label>
|
||||
<input type="text" id="username" name="username" class="pf-input"
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
||||
require_login();
|
||||
require_once dirname(__DIR__, 2) . '/includes/db.php';
|
||||
require_once dirname(__DIR__, 2) . '/includes/meta_db.php';
|
||||
require_once __DIR__ . '/caldav_sync.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$userId = (int)($_SESSION['user']['id'] ?? 0);
|
||||
$familyId = (int)($_SESSION['user']['family_id'] ?? 0);
|
||||
|
||||
$meta_pdo->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);
|
||||
@@ -0,0 +1,79 @@
|
||||
.ios-calendar-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ios-calendar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ios-calendar-head h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.ios-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ios-form-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.ios-form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ios-events-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ios-event-card {
|
||||
border: 1px solid var(--pf-border);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
background: var(--pf-bg-page);
|
||||
}
|
||||
|
||||
.ios-event-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ios-event-card-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ios-event-card-meta {
|
||||
color: var(--pf-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ios-event-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ios-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.ios-calendar-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.ios-event-card-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
const iosEventsList = document.getElementById("ios-events-list");
|
||||
const iosSyncStatus = document.getElementById("ios-sync-status");
|
||||
const iosForm = document.getElementById("ios-event-form");
|
||||
|
||||
async function iosApi(action, options = {}) {
|
||||
const method = options.method || "GET";
|
||||
const headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
...(options.headers || {}),
|
||||
};
|
||||
if (method !== "GET") {
|
||||
headers["X-CSRF-Token"] = window.CSRF_TOKEN || "";
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
const response = await fetch(`/modules/calendar-ios/api.php?action=${encodeURIComponent(action)}`, {
|
||||
method,
|
||||
credentials: "same-origin",
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
const json = await response.json();
|
||||
if (!json.ok) throw new Error(json.error || "Erreur inconnue");
|
||||
return json.data;
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function fillForm(evt) {
|
||||
document.getElementById("ios-event-id").value = evt.id;
|
||||
document.getElementById("ios-title").value = evt.title || "";
|
||||
document.getElementById("ios-location").value = evt.location || "";
|
||||
document.getElementById("ios-description").value = evt.description || "";
|
||||
document.getElementById("ios-start").value = (evt.start_at || "").slice(0, 16);
|
||||
document.getElementById("ios-end").value = (evt.end_at || "").slice(0, 16);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
iosForm.reset();
|
||||
document.getElementById("ios-event-id").value = "";
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const events = await iosApi("events");
|
||||
iosEventsList.innerHTML = "";
|
||||
if (!events.length) {
|
||||
iosEventsList.innerHTML = "<div class='pf-muted-note'>Aucun événement.</div>";
|
||||
return;
|
||||
}
|
||||
events.forEach((evt) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "ios-event-card";
|
||||
card.innerHTML = `
|
||||
<div class="ios-event-card-head">
|
||||
<div>
|
||||
<div class="ios-event-card-title">${evt.title || "(Sans titre)"}</div>
|
||||
<div class="ios-event-card-meta">${formatDate(evt.start_at)} → ${formatDate(evt.end_at)}</div>
|
||||
<div class="ios-event-card-meta">${evt.location || ""}</div>
|
||||
</div>
|
||||
<div class="ios-event-card-actions">
|
||||
<button class="pf-btn btn-secondary" data-edit="${evt.id}">Modifier</button>
|
||||
<button class="pf-btn btn-secondary" data-delete="${evt.id}">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
card.querySelector("[data-edit]").addEventListener("click", () => fillForm(evt));
|
||||
card.querySelector("[data-delete]").addEventListener("click", async () => {
|
||||
if (!confirm("Supprimer cet événement ?")) return;
|
||||
await iosApi("events", { method: "DELETE", body: { id: evt.id } });
|
||||
await loadEvents();
|
||||
});
|
||||
iosEventsList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSyncStatus() {
|
||||
const status = await iosApi("sync_status");
|
||||
iosSyncStatus.textContent = status.message;
|
||||
}
|
||||
|
||||
iosForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById("ios-event-id").value;
|
||||
const payload = {
|
||||
id: id ? parseInt(id, 10) : null,
|
||||
title: document.getElementById("ios-title").value.trim(),
|
||||
location: document.getElementById("ios-location").value.trim(),
|
||||
description: document.getElementById("ios-description").value.trim(),
|
||||
start_at: document.getElementById("ios-start").value,
|
||||
end_at: document.getElementById("ios-end").value,
|
||||
};
|
||||
await iosApi("events", { method: id ? "PUT" : "POST", body: payload });
|
||||
resetForm();
|
||||
await loadEvents();
|
||||
});
|
||||
|
||||
document.getElementById("ios-form-reset").addEventListener("click", resetForm);
|
||||
document.getElementById("ios-sync-btn").addEventListener("click", async () => {
|
||||
document.getElementById("ios-sync-btn").disabled = true;
|
||||
try {
|
||||
const data = await iosApi("sync", { method: "POST", body: {} });
|
||||
iosSyncStatus.textContent = data.message;
|
||||
await loadEvents();
|
||||
} finally {
|
||||
document.getElementById("ios-sync-btn").disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
await loadEvents();
|
||||
await loadSyncStatus();
|
||||
} catch (e) {
|
||||
iosSyncStatus.textContent = e.message;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/includes/crypto.php';
|
||||
|
||||
function ios_make_ics(array $event): string
|
||||
{
|
||||
$uid = $event['external_uid'] ?: ('hh-' . $event['id'] . '@househub');
|
||||
$start = gmdate('Ymd\THis\Z', strtotime($event['start_at']));
|
||||
$end = gmdate('Ymd\THis\Z', strtotime($event['end_at']));
|
||||
$summary = addcslashes($event['title'] ?? '', ",;");
|
||||
$description = addcslashes($event['description'] ?? '', ",;");
|
||||
$location = addcslashes($event['location'] ?? '', ",;");
|
||||
$updated = gmdate('Ymd\THis\Z');
|
||||
|
||||
return "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//HouseHub//Calendar iOS//FR\r\nBEGIN:VEVENT\r\nUID:$uid\r\nDTSTAMP:$updated\r\nDTSTART:$start\r\nDTEND:$end\r\nSUMMARY:$summary\r\nDESCRIPTION:$description\r\nLOCATION:$location\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
}
|
||||
|
||||
function ios_caldav_request(string $url, string $username, string $password, string $method = 'GET', ?string $body = null, array $headers = []): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$requestHeaders = array_merge([
|
||||
'Content-Type: text/calendar; charset=utf-8',
|
||||
], $headers);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_USERPWD => $username . ':' . $password,
|
||||
CURLOPT_HTTPHEADER => $requestHeaders,
|
||||
CURLOPT_HEADER => true,
|
||||
]);
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$raw = curl_exec($ch);
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($raw === false) {
|
||||
return ['code' => 0, 'headers' => '', 'body' => ''];
|
||||
}
|
||||
return [
|
||||
'code' => $code,
|
||||
'headers' => substr($raw, 0, $headerSize),
|
||||
'body' => substr($raw, $headerSize),
|
||||
];
|
||||
}
|
||||
|
||||
function ios_fetch_remote_events(array $integration): array
|
||||
{
|
||||
$password = hh_decrypt_secret($integration['secret_encrypted']);
|
||||
$res = ios_caldav_request($integration['calendar_url'], $integration['username'], $password, 'GET', null, ['Accept: text/calendar']);
|
||||
if ($res['code'] < 200 || $res['code'] >= 400) {
|
||||
throw new RuntimeException('Lecture CalDAV impossible (HTTP ' . $res['code'] . ')');
|
||||
}
|
||||
$blocks = preg_split('/BEGIN:VEVENT|END:VEVENT/', $res['body']);
|
||||
$events = [];
|
||||
foreach ($blocks as $chunk) {
|
||||
if (strpos($chunk, 'UID:') === false) {
|
||||
continue;
|
||||
}
|
||||
preg_match('/UID:(.+)\R/', $chunk, $uidM);
|
||||
preg_match('/SUMMARY:(.+)\R/', $chunk, $sumM);
|
||||
preg_match('/DESCRIPTION:(.+)\R/', $chunk, $descM);
|
||||
preg_match('/LOCATION:(.+)\R/', $chunk, $locM);
|
||||
preg_match('/DTSTART(?:;VALUE=DATE)?:(.+)\R/', $chunk, $startM);
|
||||
preg_match('/DTEND(?:;VALUE=DATE)?:(.+)\R/', $chunk, $endM);
|
||||
if (empty($uidM[1]) || empty($startM[1])) {
|
||||
continue;
|
||||
}
|
||||
$start = date('Y-m-d H:i:s', strtotime(trim($startM[1])));
|
||||
$end = !empty($endM[1]) ? date('Y-m-d H:i:s', strtotime(trim($endM[1]))) : $start;
|
||||
$events[] = [
|
||||
'external_uid' => trim($uidM[1]),
|
||||
'title' => trim($sumM[1] ?? ''),
|
||||
'description' => trim($descM[1] ?? ''),
|
||||
'location' => trim($locM[1] ?? ''),
|
||||
'start_at' => $start,
|
||||
'end_at' => $end,
|
||||
];
|
||||
}
|
||||
return $events;
|
||||
}
|
||||
|
||||
function ios_push_event_to_remote(array $integration, array $event): array
|
||||
{
|
||||
$password = hh_decrypt_secret($integration['secret_encrypted']);
|
||||
$uid = $event['external_uid'] ?: ('hh-' . $event['id'] . '@househub');
|
||||
$url = rtrim($integration['calendar_url'], '/') . '/' . rawurlencode($uid) . '.ics';
|
||||
$ics = ios_make_ics($event);
|
||||
return ios_caldav_request($url, $integration['username'], $password, 'PUT', $ics);
|
||||
}
|
||||
|
||||
function ios_delete_remote_event(array $integration, string $externalUid): array
|
||||
{
|
||||
$password = hh_decrypt_secret($integration['secret_encrypted']);
|
||||
$url = rtrim($integration['calendar_url'], '/') . '/' . rawurlencode($externalUid) . '.ics';
|
||||
return ios_caldav_request($url, $integration['username'], $password, 'DELETE', null, ['Accept: */*']);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (isset($_SESSION['user'])) {
|
||||
header('Location: /index.php');
|
||||
@@ -40,6 +41,9 @@ function createFamilyDb(PDO $meta, string $db_host, string $db_user, string $db_
|
||||
// ─── Traitement du formulaire ─────────────────────────────────────────────────
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||
$error = "Session invalide (CSRF). Rechargez la page.";
|
||||
} else {
|
||||
$action = $_POST['action'] ?? 'create';
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
@@ -93,6 +97,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'family_id' => (int)$family['id'],
|
||||
];
|
||||
$_SESSION['family_db'] = $family['db_name'];
|
||||
session_regenerate_id(true);
|
||||
header('Location: /index.php');
|
||||
exit;
|
||||
}
|
||||
@@ -133,6 +138,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'family_id' => $family_id,
|
||||
];
|
||||
$_SESSION['family_db'] = $db_name;
|
||||
session_regenerate_id(true);
|
||||
header('Location: /index.php');
|
||||
exit;
|
||||
}
|
||||
@@ -143,6 +149,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = "Inscription — HouseHub";
|
||||
$activePage = "register";
|
||||
@@ -172,6 +179,7 @@ require __DIR__ . '/header.php';
|
||||
|
||||
<!-- Formulaire commun -->
|
||||
<form method="post" action="/register.php" id="reg-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" id="reg-action" value="create">
|
||||
|
||||
<div class="pf-form-group">
|
||||
|
||||
+34
@@ -235,3 +235,37 @@ VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/ig
|
||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||
|
||||
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
+133
-2
@@ -2,6 +2,7 @@
|
||||
require __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
require_once __DIR__ . '/includes/crypto.php';
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
$user_id = $_SESSION['user']['id'];
|
||||
@@ -10,12 +11,31 @@ $family_id = $_SESSION['user']['family_id'];
|
||||
$error = null;
|
||||
$success = null;
|
||||
|
||||
$meta_pdo->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)
|
||||
)");
|
||||
|
||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||
$error = "Session invalide (CSRF). Rechargez la page.";
|
||||
} else {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'set_modules' && $family_id) {
|
||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo'];
|
||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'calendar_ios'];
|
||||
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
||||
if (empty($enabled)) {
|
||||
$error = "Vous devez garder au moins un module actif.";
|
||||
@@ -112,6 +132,65 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
||||
$success = "Image d'accueil réinitialisée.";
|
||||
}
|
||||
|
||||
if ($action === 'calendar_ios_save') {
|
||||
$username = trim($_POST['icloud_username'] ?? '');
|
||||
$appPassword = trim($_POST['icloud_app_password'] ?? '');
|
||||
$calendarUrl = trim($_POST['icloud_calendar_url'] ?? '');
|
||||
|
||||
if (!$username || !$appPassword || !$calendarUrl) {
|
||||
$error = "Merci de renseigner identifiant iCloud, mot de passe d'app et URL CalDAV.";
|
||||
} else {
|
||||
try {
|
||||
$encrypted = hh_encrypt_secret($appPassword);
|
||||
$meta_pdo->prepare("
|
||||
INSERT INTO user_calendar_integrations (user_id, provider, username, secret_encrypted, calendar_url, status, updated_at)
|
||||
VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW())
|
||||
ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW()
|
||||
")->execute([$user_id, $username, $encrypted, $calendarUrl]);
|
||||
$success = "Connexion calendrier iOS enregistrée.";
|
||||
} catch (\Throwable $e) {
|
||||
$error = "Impossible d'enregistrer la connexion iOS: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'calendar_ios_test') {
|
||||
$row = $meta_pdo->prepare("SELECT username, secret_encrypted, calendar_url FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||
$row->execute([$user_id]);
|
||||
$integration = $row->fetch();
|
||||
if (!$integration) {
|
||||
$error = "Aucune connexion iOS configurée.";
|
||||
} else {
|
||||
try {
|
||||
$pwd = hh_decrypt_secret($integration['secret_encrypted']);
|
||||
$ch = curl_init($integration['calendar_url']);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_USERPWD => $integration['username'] . ':' . $pwd,
|
||||
]);
|
||||
curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code >= 200 && $code < 400) {
|
||||
$success = "Connexion iCloud CalDAV valide.";
|
||||
} else {
|
||||
$error = "Test connexion échoué (HTTP $code).";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = "Test connexion impossible: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'calendar_ios_disconnect') {
|
||||
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
||||
$success = "Connexion iOS supprimée.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Chargement données ───────────────────────────────────────────────────────
|
||||
@@ -131,6 +210,10 @@ if ($family_id) {
|
||||
$members = $mem->fetchAll();
|
||||
}
|
||||
|
||||
$calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||
$calendarIntegration->execute([$user_id]);
|
||||
$calendarIntegration = $calendarIntegration->fetch();
|
||||
|
||||
$pageTitle = "Paramètres — HouseHub";
|
||||
$activePage = "settings";
|
||||
require __DIR__ . '/header.php';
|
||||
@@ -179,7 +262,7 @@ require __DIR__ . '/header.php';
|
||||
<h2 class="pf-card-h2 pf-card-h2--tight">🧩 Modules actifs</h2>
|
||||
<p class="pf-muted-note">Choisissez les modules visibles dans la navigation (partagé avec tous les membres de l'espace).</p>
|
||||
<?php
|
||||
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts'];
|
||||
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios'];
|
||||
$allModules = [
|
||||
'calendar' => ['icon' => '📅', 'label' => tr('menu_calendar')],
|
||||
'budget' => ['icon' => '💰', 'label' => tr('menu_budget')],
|
||||
@@ -188,6 +271,7 @@ require __DIR__ . '/header.php';
|
||||
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
|
||||
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
|
||||
'todo' => ['icon' => '✅', 'label' => tr('menu_todo')],
|
||||
'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')],
|
||||
];
|
||||
?>
|
||||
<form method="post">
|
||||
@@ -206,6 +290,43 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2 pf-card-h2--tight">📱 Intégration Calendrier iOS (CalDAV)</h2>
|
||||
<p class="pf-muted-note">Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.</p>
|
||||
<form method="post" class="pf-stack-md">
|
||||
<input type="hidden" name="action" value="calendar_ios_save">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Identifiant Apple (email iCloud)</label>
|
||||
<input type="text" name="icloud_username" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['username'] ?? '') ?>" placeholder="nom@icloud.com" required>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Mot de passe d'app Apple</label>
|
||||
<input type="password" name="icloud_app_password" class="pf-input" placeholder="xxxx-xxxx-xxxx-xxxx" required>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">URL du calendrier CalDAV</label>
|
||||
<input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pf-flex-gap-8 pf-mt-sm">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="calendar_ios_test">
|
||||
<button type="submit" class="pf-btn btn-secondary">Tester la connexion</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="calendar_ios_disconnect">
|
||||
<button type="submit" class="pf-btn btn-secondary">Déconnecter</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php if (!empty($calendarIntegration['last_sync_at'])): ?>
|
||||
<p class="pf-muted-note">Dernière synchro: <?= htmlspecialchars($calendarIntegration['last_sync_at']) ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<!-- ── Fond page d'accueil ───────────────────────────────────────────── -->
|
||||
<?php if ($family_id):
|
||||
$bg_file = null;
|
||||
@@ -346,6 +467,16 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('form[method="post"]').forEach((form) => {
|
||||
if (!form.querySelector('input[name="csrf_token"]')) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'csrf_token';
|
||||
input.value = window.CSRF_TOKEN || '';
|
||||
form.appendChild(input);
|
||||
}
|
||||
});
|
||||
|
||||
function copyCode() {
|
||||
const code = document.getElementById('invite-code').textContent.trim();
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
|
||||
Reference in New Issue
Block a user