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)) { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => tr('error_csrf')]); exit; } $error = tr('error_csrf'); } else { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka', 'food']; $allViews = ['food_meals', 'food_liste', 'budget_prev', 'budget_epargne', 'budget_provisions']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); $enabledViews = array_values(array_filter($allViews, fn($v) => isset($_POST['view_' . $v]))); if (empty($enabled)) { $error = tr('set_err_min_module'); } else { $meta_pdo->prepare("UPDATE families SET enabled_modules = ?, enabled_views = ? WHERE id = ?") ->execute([json_encode($enabled), json_encode($enabledViews), $family_id]); $_SESSION['enabled_modules'] = $enabled; $_SESSION['enabled_views'] = $enabledViews; $success = tr('set_success_modules'); } } if ($action === 'set_lang') { $lang = $_POST['lang'] ?? 'fr'; if (in_array($lang, ['fr', 'ca', 'en'])) { $_SESSION['app_lang'] = $lang; $meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?") ->execute([$lang, $user_id]); $success = tr('set_success_lang'); } } if ($action === 'update_profile') { $display_name = trim($_POST['display_name'] ?? ''); $color = trim($_POST['color'] ?? '#0891b2'); if (!$display_name) { $error = tr('set_err_empty_name'); } else { $meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?") ->execute([$display_name, $user_id]); require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?"); $stmtPeopleColor->execute([$color, $user_id]); } $_SESSION['user']['display_name'] = $display_name; $success = tr('set_success_profile'); } } if ($action === 'change_password') { $current = $_POST['current_password'] ?? ''; $new = $_POST['new_password'] ?? ''; $confirm = $_POST['confirm_password'] ?? ''; $row = $meta_pdo->prepare("SELECT password_hash FROM users WHERE id = ?"); $row->execute([$user_id]); $row = $row->fetch(); if (!password_verify($current, $row['password_hash'])) { $error = tr('set_err_wrong_pwd'); } elseif (strlen($new) < 6) { $error = tr('set_err_pwd_length'); } elseif ($new !== $confirm) { $error = tr('set_err_pwd_match'); } else { $meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?") ->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]); $success = tr('set_success_pwd'); } } if ($action === 'update_family_name' && $family_id) { $name = trim($_POST['family_name'] ?? ''); if ($name) { $meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?") ->execute([$name, $family_id]); $success = tr('set_success_family'); } } if ($action === 'regen_invite' && $family_id) { $new_code = bin2hex(random_bytes(8)); $meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?") ->execute([$new_code, $family_id]); $success = tr('set_success_invite'); } if ($action === 'upload_home_bg' && $family_id) { $upload_dir = '/uploads/'; if (!is_dir($upload_dir)) @mkdir($upload_dir, 0755, true); $file = $_FILES['home_bg'] ?? null; if ($file && $file['error'] === 0) { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['jpg','jpeg','png','webp'])) { $error = tr('set_err_bg_format'); } else { $dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext; foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old); if (move_uploaded_file($file['tmp_name'], $dest)) { $success = tr('set_success_bg'); } else { $error = tr('set_err_upload'); } } } } if ($action === 'reset_home_bg' && $family_id) { foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old); $success = tr('set_success_bg_reset'); } if ($action === 'grocery_history_max' && $family_id) { require_once __DIR__ . '/includes/db.php'; if (!isset($pdo)) { $error = tr('set_err_db_unavailable'); } else { $n = (int) ($_POST['history_max'] ?? 20); $n = max(1, min(50, $n)); try { $pdo->prepare( "INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)" )->execute([(string) $n]); $success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.'; } catch (\Throwable $e) { $error = (tr('groceries_settings_error') ?: 'Erreur') . ' : ' . $e->getMessage(); } } } if ($action === 'calendar_ios_save') { $username = trim($_POST['icloud_username'] ?? ''); $appPassword = hh_normalize_apple_app_password((string) ($_POST['icloud_app_password'] ?? '')); $calendarUrl = trim($_POST['icloud_calendar_url'] ?? ''); if (!$username || !$appPassword || !$calendarUrl) { $error = tr('set_err_ios_missing'); } else { try { $resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl); $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, $resolvedUrl]); $msg = tr('set_success_ios'); if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) { $msg .= tr('set_success_ios_auto'); } $success = $msg; } catch (\Throwable $e) { $error = tr('set_err_ios_save') . $e->getMessage(); } } } if ($action === 'calendar_ios_test') { $row = $meta_pdo->prepare("SELECT id, 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 = tr('set_err_ios_none'); } else { try { $pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted'])); $calendarUrl = trim($integration['calendar_url']); $resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($integration['username'], $pwd, $calendarUrl); $code = hh_caldav_test_calendar_collection($integration['username'], $pwd, $resolvedUrl); if (in_array($code, [200, 207], true)) { if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) { $meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?") ->execute([$resolvedUrl, $integration['id']]); } $success = sprintf(tr('set_success_ios_test'), $code); } else { $error = sprintf(tr('set_err_ios_test'), $code); } } catch (\Throwable $e) { $error = tr('set_err_ios_test_fail') . $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 = tr('set_success_ios_del'); } // --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) --- if ($action === 'add_attached_profile') { $name = trim($_POST['profile_name'] ?? ''); $role = trim($_POST['profile_role'] ?? 'child'); $color = trim($_POST['profile_color'] ?? '#3b82f6'); if ($name) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, ?)"); $stmt->execute([$name, $role, $color]); $success = "Profil de {$name} ajouté avec succès."; } } else { $error = "Le prénom est obligatoire."; } } if ($action === 'update_attached_profile_color') { $profile_id = (int)($_POST['profile_id'] ?? 0); $color = trim($_POST['color'] ?? '#3b82f6'); if ($profile_id > 0) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL"); $stmt->execute([$color, $profile_id]); $success = "Couleur mise à jour."; } } } if ($action === 'delete_attached_profile') { $profile_id = (int)($_POST['profile_id'] ?? 0); if ($profile_id > 0) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL"); $stmt->execute([$profile_id]); $success = tr('set_success_profile_del'); } } } if ($action === 'update_profile_color') { $profile_id = (int)($_POST['profile_id'] ?? 0); $color = trim($_POST['color'] ?? '#0891b2'); if ($profile_id > 0) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL"); $stmt->execute([$color, $profile_id]); $success = tr('set_success_color_profile'); } } } if ($action === 'update_attached_profile_color') { $profile_id = (int)($_POST['profile_id'] ?? 0); $color = trim($_POST['color'] ?? ''); // Vérification stricte format Hexadécimal if ($profile_id > 0 && preg_match('/^#[a-f0-9]{6}$/i', $color)) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { // user_id IS NULL garantit qu'on ne modifie pas un vrai compte Parent par accident $stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL"); $stmt->execute([$color, $profile_id]); if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { header('Content-Type: application/json'); echo json_encode(['success' => true]); exit; } $success = tr('set_success_color_update'); } } } } } // ─── Chargement données ─────────────────────────────────────────────────────── if ($family_id) { require_once __DIR__ . '/includes/db.php'; } $stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmtUser->execute([$user_id]); $user = $stmtUser->fetch(); $user_color = '#0891b2'; if ($family_id && isset($pdo)) { $stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?"); $stmtColorFetch->execute([$user_id]); $fetched_color = $stmtColorFetch->fetchColumn(); if ($fetched_color) { $user_color = $fetched_color; } } $family = null; $members = []; $attached_profiles = []; if ($family_id) { $fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?"); $fam->execute([$family_id]); $family = $fam->fetch(); $mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id"); $mem->execute([$family_id]); $members = $mem->fetchAll(); if (isset($pdo)) { $stmtAttached = $pdo->query("SELECT id, name, role, color FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC"); $attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC); } } $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(); $groceryHistoryMaxSetting = 20; if ($family_id && isset($pdo)) { try { $gv = $pdo->query("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'")->fetchColumn(); if ($gv !== false && $gv !== null && $gv !== '') { $groceryHistoryMaxSetting = max(1, min(50, (int) $gv)); } } catch (\Throwable $e) {} } $pageTitle = tr('set_title_page'); $activePage = "settings"; require __DIR__ . '/header.php'; ?>

'; $en_flag = ''; $senyera = ''; $langs = [ 'fr' => ['flag' => $fr_flag, 'label' => 'Français'], 'en' => ['flag' => $en_flag, 'label' => 'English'], 'ca' => ['flag' => $senyera, 'label' => 'Català'], ]; $currentLang = $_SESSION['app_lang'] ?? 'fr'; foreach ($langs as $code => $lang): ?>

['icon' => '📅', 'label' => tr('menu_calendar')], 'budget' => [ 'icon' => '💰', 'label' => tr('menu_budget'), 'mandatory_note' => tr('budget_mandatory_note'), 'views' => [ 'budget_prev' => tr('budget_tab_prev'), 'budget_epargne' => tr('budget_tab_savings'), 'budget_provisions' => tr('budget_tab_provisions') ] ], 'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')], 'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')], 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], 'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], 'liste' => ['icon' => '🛒', 'label' => tr('menu_liste')], 'food' => [ 'icon' => '🍳', 'label' => tr('mod_food_name'), 'views' => [ 'food_meals' => tr('food_tab_meals'), 'food_liste' => tr('food_tab_list') ] ], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')], 'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')], 'planka' => ['icon' => '📋', 'label' => tr('menu_planka')], ]; ?>
$mod): $active = in_array($key, $enabledMods); ?>
$vLabel): ?>

Fond actuel

/register.php

()

@

🧸

$c): ?>