diff --git a/background.js b/background.js index 41c7e13..14acfc9 100644 --- a/background.js +++ b/background.js @@ -85,7 +85,7 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) { `&day_start_hour=8` + `&day_end_hour=19`; console.log("[bg] fetchPlanningXml →", url.substring(0, 140)); - const r = await fetch(url, { credentials: "include" }); + const r = await evFetch(url, origin); console.log("[bg] status =", r.status); if (!r.ok) { // v4.2 : classifier l'erreur HTTP pour que le viewer affiche le bon @@ -100,6 +100,32 @@ async function fetchPlanningXml(origin, phpsessid, unixDate) { return xml; } +/** + * v5.0.9 : wrapper autour de fetch() qui ajoute systématiquement les + * headers de sécurité attendus par EasyVista (Referer, Sec-Fetch-Site, + * X-Requested-With). Sans ces headers, EV renvoie soit un + * (protection CSRF ou session expirée) + * 3. URL de logout : index.php?...&logout=1 + * 4. Redirection vers le portail SSO : portail.etat-de-vaud.ch/sso/ + * 5. Réponse JSON avec "isLogged": false + */ function looksLikeLoginPage(text) { - // La page de login EasyVista contient cette chaîne - return /customer_login|my\.policy/i.test((text || "").substring(0, 3000)); + const t = (text || "").substring(0, 3000); + if (!t) return false; + // Pattern 1 : page de login EV classique + if (/customer_login|my\.policy/i.test(t)) return true; + // Pattern 2 : script de redirection (< 500 chars = probablement juste ça) + if (t.length < 500 && /]*>\s*window\.location\.href\s*=/i.test(t)) return true; + // Pattern 3 : URL de logout + if (/[?&]logout=1/i.test(t)) return true; + // Pattern 4 : redirection vers portail SSO + if (/portail\.etat-de-vaud\.ch\/sso\//i.test(t)) return true; + // Pattern 5 : JSON isLogged:false + if (/"isLogged"\s*:\s*false/i.test(t)) return true; + return false; +} + +// ============================================================================ +// v5.0.9 : surveillance du timeout de session EasyVista +// ============================================================================ + +/** + * GET /timeout_ajax.php?__AJAX_TIMEOUT_FCT__=session_time + * + * Retourne le nombre de millisecondes restantes avant expiration de la + * session EasyVista (0 à 1 800 000 = 30 min max). + * + * Attention : cette requête EST authentifiée et prolonge probablement la + * session (comme toute requête PHP authentifiée). À utiliser avec parcimonie. + */ +async function fetchSessionTimeRemaining(origin, phpsessid) { + const url = `${origin}/timeout_ajax.php` + + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + + `&__AJAX_TIMEOUT_FCT__=session_time`; + console.log("[bg] fetchSessionTimeRemaining →", url.substring(0, 120)); + const r = await evFetch(url, origin); + if (!r.ok) { + throw new Error("HTTP " + r.status); + } + const body = (await r.text()).trim(); + // Vérifier que c'est bien un nombre (sinon = session morte probable) + if (!/^\d+$/.test(body)) { + console.warn("[bg] réponse session_time anormale :", body.substring(0, 200)); + // Si c'est une page de login/redirect → session expirée + if (looksLikeLoginPage(body)) { + throw new Error("session_expired"); + } + throw new Error("invalid_response"); + } + const ms = parseInt(body, 10); + console.log(`[bg] session_time = ${ms} ms = ${Math.round(ms/60000)} min`); + return ms; +} + +/** + * GET /timeout_ajax.php?__AJAX_TIMEOUT_FCT__=keep_connection + * + * Prolonge la session à 30 min. Retourne 1800000. + */ +async function extendSessionKeepAlive(origin, phpsessid) { + const url = `${origin}/timeout_ajax.php` + + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + + `&__AJAX_TIMEOUT_FCT__=keep_connection`; + console.log("[bg] extendSessionKeepAlive →", url.substring(0, 120)); + const r = await evFetch(url, origin); + if (!r.ok) { + throw new Error("HTTP " + r.status); + } + const body = (await r.text()).trim(); + if (!/^\d+$/.test(body)) { + if (looksLikeLoginPage(body)) throw new Error("session_expired"); + throw new Error("invalid_response"); + } + const ms = parseInt(body, 10); + console.log(`[bg] keep_connection → session prolongée à ${ms} ms`); + return ms; } // ============================================================================ @@ -872,6 +969,54 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { return; } + if (msg.type === "getSessionRemaining") { + // v5.0.9 : récupère le temps restant avant expiration de la session EV + const session = await findEasyVistaSession(); + if (!session) { + sendResponse({ ok: false, error: "no_session" }); + return; + } + try { + const remainingMs = await fetchSessionTimeRemaining(session.origin, session.phpsessid); + sendResponse({ ok: true, remainingMs, phpsessid: session.phpsessid }); + } catch (err) { + sendResponse({ ok: false, error: err.message || String(err) }); + } + return; + } + + if (msg.type === "extendSession") { + // v5.0.9 : prolonge la session EV à 30 min via keep_connection + const session = await findEasyVistaSession(); + if (!session) { + sendResponse({ ok: false, error: "no_session" }); + return; + } + try { + const remainingMs = await extendSessionKeepAlive(session.origin, session.phpsessid); + sendResponse({ ok: true, remainingMs }); + } catch (err) { + sendResponse({ ok: false, error: err.message || String(err) }); + } + return; + } + + if (msg.type === "openEasyVistaLogin") { + // v5.0.9 : ouvre EasyVista dans un nouvel onglet pour provoquer + // le SSO Windows automatique (reconnexion transparente). + const origin = msg.origin || "https://itsma.etat-de-vaud.ch"; + try { + const tab = await chrome.tabs.create({ + url: `${origin}/index.php?eventName=HelpDesk_PlanningItem`, + active: true + }); + sendResponse({ ok: true, tabId: tab.id }); + } catch (err) { + sendResponse({ ok: false, error: err.message || String(err) }); + } + return; + } + if (msg.type === "cleanupOldCaches") { const removed = await cleanupOldCaches(msg.daysToKeep || 7); sendResponse({ ok: true, removed }); diff --git a/manifest.json b/manifest.json index fad6dbe..999343b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "5.0.8", + "version": "5.0.9", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"], "host_permissions": [ diff --git a/viewer.css b/viewer.css index 20e80f3..d7db0bb 100644 --- a/viewer.css +++ b/viewer.css @@ -2217,3 +2217,122 @@ header.topbar::before { font-size: 13px; cursor: pointer; } + +/* ========================================================================== + v5.0.9 : Compteur de session EasyVista (topbar) + ========================================================================== */ + +.app-session { + position: absolute; + top: 50%; + left: calc(50% + 60px); /* à droite de l'horloge (~60px de décalage) */ + transform: translateY(-50%); + display: flex; + align-items: center; + gap: 8px; + padding: 4px 10px; + border-radius: 14px; + font-size: 13px; + font-weight: 500; + font-variant-numeric: tabular-nums; + z-index: 9; + background: rgba(0, 0, 0, 0.05); + transition: background 0.3s, color 0.3s; +} +.app-session.hidden { + display: none; +} +.app-session .session-icon { + font-size: 14px; +} +.app-session .session-time { + font-weight: 600; +} +.app-session .session-extend-btn { + margin-left: 4px; + padding: 3px 8px; + font-size: 11px; + border-radius: 10px; + border: 1px solid currentColor; + background: transparent; + color: inherit; + cursor: pointer; + font-weight: 500; + transition: background 0.2s; +} +.app-session .session-extend-btn:hover { + background: rgba(255, 255, 255, 0.2); +} +.app-session .session-extend-btn:disabled { + opacity: 0.6; + cursor: default; +} + +/* État warning (2-5 min) : jaune */ +.app-session.session-warn { + background: #f5c518; + color: #2a2100; +} +.app-session.session-warn .session-extend-btn { + border-color: #2a2100; +} + +/* État critical (< 2 min) : rouge + pulse */ +.app-session.session-critical { + background: #e74c3c; + color: #fff; + animation: session-pulse 1s infinite; +} +.app-session.session-critical .session-extend-btn { + border-color: #fff; + background: rgba(255, 255, 255, 0.15); + font-weight: 600; +} +.app-session.session-critical .session-extend-btn:hover { + background: rgba(255, 255, 255, 0.3); +} +@keyframes session-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.5); } + 50% { box-shadow: 0 0 0 6px rgba(231, 76, 60, 0); } +} + +/* Bouton "Me reconnecter" dans la bannière session expirée */ +.session-expired-reconnect-btn { + margin-left: 12px; + padding: 6px 14px; + border-radius: 4px; + background: #fff; + color: #c0392b; + border: none; + font-weight: 600; + cursor: pointer; + font-size: 13px; + transition: background 0.2s; +} +.session-expired-reconnect-btn:hover { + background: #f8d7da; +} + +/* Bannière "Reconnexion en cours" */ +.banner-reconnecting { + background: #3498db; + color: #fff; + padding: 10px 20px; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + font-weight: 500; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.banner-reconnecting.hidden { + display: none; +} +.banner-reconnecting .banner-spinner { + font-size: 16px; + animation: spin-slow 2s linear infinite; +} +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/viewer.html b/viewer.html index f060897..968f53e 100644 --- a/viewer.html +++ b/viewer.html @@ -25,6 +25,8 @@
+ +
+ `; + const extendBtn = el.querySelector(".session-extend-btn"); + if (extendBtn) { + extendBtn.onclick = async () => { + extendBtn.disabled = true; + extendBtn.textContent = "…"; + try { + const resp = await sendMessage({ type: "extendSession" }); + if (resp && resp.ok && typeof resp.remainingMs === "number") { + state.sessionExpireAt = Date.now() + resp.remainingMs; + state.sessionPingDone = false; + state._criticalModalShown = false; + showToast("Session prolongée", "30 minutes de plus"); + updateSessionIndicator(); + } else { + throw new Error((resp && resp.error) || "erreur inconnue"); + } + } catch (err) { + extendBtn.disabled = false; + extendBtn.textContent = "🔄 Prolonger"; + if (err.message === "session_expired" || err.message === "no_session") { + handleSessionExpired(); + } + } + }; + } +} + +/** + * Appelée quand le compteur atteint 0 ou quand une requête EV échoue en + * session expirée. Affiche la bannière "Session expirée" avec bouton "Me + * reconnecter". + */ +function handleSessionExpired() { + if (state.sessionExpired) return; + state.sessionExpired = true; + state.sessionExpireAt = null; + state._criticalModalShown = false; + console.warn("[session] session EV expirée"); + showSessionExpiredBanner(); + const el = document.getElementById("app-session"); + if (el) el.classList.add("hidden"); +} + +/** + * Modal auto quand < 2 min : alerte visuelle forte. + */ +function showSessionCriticalModal() { + showAlertModal({ + title: "⚠️ Session EasyVista expire bientôt", + message: "Votre session EasyVista expire dans moins de 2 minutes. Cliquez sur « Prolonger » pour éviter d'être déconnecté.", + buttons: [ + { label: "Ignorer", variant: "secondary", action: () => {} }, + { + label: "🔄 Prolonger maintenant", + variant: "primary", + action: async () => { + try { + const resp = await sendMessage({ type: "extendSession" }); + if (resp && resp.ok && typeof resp.remainingMs === "number") { + state.sessionExpireAt = Date.now() + resp.remainingMs; + state.sessionPingDone = false; + state._criticalModalShown = false; + showToast("Session prolongée", "30 minutes de plus"); + updateSessionIndicator(); + } + } catch (e) { + handleSessionExpired(); + } + } + } + ] + }); +} + +/** + * Appelé au clic "Me reconnecter" dans la bannière. Ouvre EasyVista dans un + * nouvel onglet (déclenche Windows SSO Kerberos automatique). Le polling + * dans initSessionTimer détectera la nouvelle session et rechargera le viewer. + */ +async function triggerReconnect() { + state.reconnecting = true; + hideSessionExpiredBanner(); + showReconnectingBanner(); + try { + const origin = (state.session && state.session.origin) || "https://itsma.etat-de-vaud.ch"; + await sendMessage({ type: "openEasyVistaLogin", origin }); + } catch (err) { + console.warn("[session] openEasyVistaLogin failed:", err); + state.reconnecting = false; + hideReconnectingBanner(); + showSessionExpiredBanner(); + } +} + + // v5.0.0 : stockage des paramètres admin dans chrome.storage.local. // Clé unique : "admin_config". Contient la config éditable (équipe, // absences récurrentes, statuts etc.). Au 1er lancement : initialisée @@ -6530,15 +6771,59 @@ function hideEvUnreachable() { // que les mises à jour sont arrêtées. function showSessionExpiredBanner() { const b = document.getElementById("session-expired-banner"); - if (b) b.classList.remove("hidden"); - // Masquer la bannière EV si présente (on ne montre qu'une bannière à la fois) + if (b) { + b.classList.remove("hidden"); + // v5.0.9 : s'assurer que la bannière contient le bouton "Me reconnecter" + // et qu'il appelle triggerReconnect (SSO Windows transparent). + if (!b.querySelector(".session-expired-reconnect-btn")) { + // Chercher le premier .banner-content ou injecter du contenu si vide + let content = b.querySelector(".banner-content") || b; + // Si déjà du contenu natif, on ajoute juste le bouton à la fin + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "session-expired-reconnect-btn"; + btn.textContent = "🔄 Me reconnecter"; + btn.addEventListener("click", () => triggerReconnect()); + content.appendChild(btn); + } + } hideEvUnreachableBanner(); + hideReconnectingBanner(); } function hideSessionExpiredBanner() { const b = document.getElementById("session-expired-banner"); if (b) b.classList.add("hidden"); } +// v5.0.9 : bannière affichée pendant la reconnexion (remplace la bannière +// expirée après clic sur "Me reconnecter") +function showReconnectingBanner() { + let b = document.getElementById("session-reconnecting-banner"); + if (!b) { + // Créer la bannière si elle n'existe pas (dans le topbar) + b = document.createElement("div"); + b.id = "session-reconnecting-banner"; + b.className = "banner-reconnecting"; + b.innerHTML = ` + + + `; + // L'insérer juste après la topbar + const topbar = document.querySelector(".topbar") || document.querySelector("header") || document.body; + if (topbar.nextSibling) { + topbar.parentNode.insertBefore(b, topbar.nextSibling); + } else { + document.body.insertBefore(b, document.body.firstChild); + } + } + b.classList.remove("hidden"); + hideSessionExpiredBanner(); +} +function hideReconnectingBanner() { + const b = document.getElementById("session-reconnecting-banner"); + if (b) b.classList.add("hidden"); +} + // v4.2.5 : bannière non bloquante "EasyVista inaccessible" function showEvUnreachableBanner() { const b = document.getElementById("ev-unreachable-banner");