// background.js — Service worker (Manifest V3) — v4 // // Rôles : // 1. Au clic sur l'icône : ouvrir le viewer // 2. Répondre aux messages du viewer : // - getSession : trouve l'onglet EasyVista ouvert, renvoie {phpsessid, origin} // - fetchPlanning : fetch le XML du planning pour une date (1 requête = tout) // - fetchXhr2 : fetch un texte d'action détaillé (utilisé en lazy-load au survol) // - fetchFiche : fetch une fiche individuelle (HTML) pour statut + commentaire tech // 3. Nettoyer les vieux caches (>7 jours) // (v4.2 : l'auto-refresh 12h/15h a été retiré) // // v4 : suppression de fetchTimeline (pu utilisé). Le calendar_block contient // directement ref/contact/lieu/catégorie dans ses attributs attr1/attr2/attr3, // donc on n'a plus besoin ni de xhr2 en masse, ni de l'API timeline. // Domaines EasyVista reconnus (interne d'abord, externe en fallback) const EV_ORIGINS = [ "https://itsma.etat-de-vaud.ch", "https://itsma.vd.ch" ]; // ============================================================================ // Clic sur l'icône → ouvrir le viewer // ============================================================================ chrome.action.onClicked.addListener(async () => { const viewerUrl = chrome.runtime.getURL("viewer.html"); // Si le viewer est déjà ouvert, on focus cet onglet plutôt que d'en ouvrir un autre const existing = await chrome.tabs.query({ url: viewerUrl + "*" }); if (existing.length > 0) { await chrome.tabs.update(existing[0].id, { active: true }); await chrome.windows.update(existing[0].windowId, { focused: true }); } else { await chrome.tabs.create({ url: viewerUrl }); } }); // ============================================================================ // Trouver l'onglet EasyVista actif et en extraire le PHPSESSID // ============================================================================ async function findEasyVistaSession() { // Chercher tous les onglets sur un domaine EasyVista for (const origin of EV_ORIGINS) { const tabs = await chrome.tabs.query({ url: origin + "/*" }); for (const tab of tabs) { const m = (tab.url || "").match(/[?&]PHPSESSID=([a-zA-Z0-9]+)/); if (m) { return { phpsessid: m[1], origin: origin, tabId: tab.id }; } } } return null; } // ============================================================================ // Fetch helpers (s'exécutent dans le contexte du service worker, // les cookies du domaine sont automatiquement inclus via credentials: include) // ============================================================================ /** * Fetch du XML retourné par planning_xhr.php?div=calendar_block. * Contient les interventions de nos 8 techs pour la date donnée (~40 ko). * * Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données * dans le HTML, elles arrivent via cet endpoint AJAX. */ async function fetchPlanningXml(origin, phpsessid, unixDate) { const techIds = "76272,83725,66635,92235,90070,40944,72485,86874"; const groupId = "191"; const url = `${origin}/planning_xhr.php` + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + `&div=calendar_block` + `&mode=day` + `&group_id=${groupId}` + `&event_name=HelpDesk_PlanningItem` + `&sql_param=${techIds}` + `&unix_date=${unixDate}` + `&start_date_label=Date` + `&end_date_label=Date` + `&click_here_label=Ici` + `&mail_title=mail` + `&day_start_hour=8` + `&day_end_hour=19`; console.log("[bg] fetchPlanningXml →", url.substring(0, 140)); 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 // écran (session expirée vs EV inaccessible). const err = new Error("HTTP " + r.status); err.kind = classifyHttpStatus(r.status); err.status = r.status; throw err; } const xml = await r.text(); console.log("[bg] taille XML =", xml.length); 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) { 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; } // ============================================================================ // v4.2 : récupération de l'utilisateur connecté // ============================================================================ /** * Essaie de récupérer le nom de l'utilisateur EasyVista connecté en fetchant * la page d'accueil avec la session active. EasyVista n'exposant pas * d'endpoint public simple, on cherche des patterns typiques dans le HTML : * - ...Nom, Prénom... * - éléments avec data-user-name, data-user-login * - balises cachées ou variables JS EV.User.name * - champ "Bienvenue Nom Prénom" * Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si * tout a échoué. */ async function fetchCurrentUser(origin, phpsessid) { const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`; const resp = await fetch(url, { method: "GET", credentials: "include", headers: { "Accept": "text/html,*/*" } }); // v4.2 : cette fonction est lancée en tâche de fond au démarrage. Si la // session est expirée ou EV inaccessible, on retourne juste null — le // planning lui-même déclenchera l'écran d'erreur approprié. if (!resp.ok) return null; const html = await resp.text(); if (looksLikeLoginPage(html)) return null; // v4.2.2 : patterns spécifiques à la structure EasyVista réelle du Canton // de Vaud (identifiés à partir du HTML de la page d'accueil). L'user est // affiché dans un dropdown ".ev-employee-dropdown" avec ces éléments : // // Nom, Prénom // 3.3 DGNSI-ServiceDesk // ... // // Le title du parent contient aussi "Nom, Prénom / Service / Société". const patterns = [ // 1) Le plus fiable : span class="h5" dans profile-info (structure EV 2026) /]*>\s*]*title=["']([^"']{2,80})["']/i, // 2) Fallback : span class="h5" avec title= même hors profile-info /]*title=["']([^"']{2,80})["'][^>]*>\s*([^<]{2,80})<\/span>/i, // 3) Fallback : title= de ev-employee-dropdown (format "Nom, Prénom / Service / Société") /class=["'][^"']*ev-employee-dropdown[^"']*["'][^>]*title=["']([^"'\/]+?)(?:\s*\/\s*[^"']+)?["']/i, // 4) Anciens patterns génériques (autres instances EasyVista éventuelles) /data-user-name\s*=\s*["']([^"']+)["']/i, /data-username\s*=\s*["']([^"']+)["']/i, /data-user-fullname\s*=\s*["']([^"']+)["']/i, /EV\.User\.name\s*=\s*["']([^"']+)["']/, /EV\.User\.fullname\s*=\s*["']([^"']+)["']/, /userFullName\s*[:=]\s*["']([^"']+)["']/, // 5) "Bienvenue" / "Welcome" /(?:Bienvenue|Welcome)[,\s]+(?:M\.?\s+|Mme\s+)?([A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+(?:\s*,?\s+[A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+){0,3})/ ]; let name = null; for (const rx of patterns) { const m = html.match(rx); if (m && m[1]) { const candidate = m[1].trim() .replace(/\s+/g, " ") .replace(/^(?:EasyVista|EV|Accueil|Home|Planning|ITSMA)[\s\-|•]+/i, "") .replace(/[\s\-|•]+(?:EasyVista|EV|ITSMA)$/i, "") .trim(); if (candidate && candidate.length >= 3 && candidate.length <= 80 && /[A-Za-zÀ-ÿ]/.test(candidate) && !/\b(login|connexion|sign\s*in|easyvista|ITSMA)\b/i.test(candidate)) { name = candidate; break; } } } // v4.2.2 : on extrait aussi le service/unité si disponible (h6 à côté du h5) let service = null; const serviceMatch = html.match( /]*>[\s\S]{0,500}?]*title=["']([^"']{2,80})["']/i ); if (serviceMatch && serviceMatch[1]) { service = serviceMatch[1].trim(); } // Login / identifiant court (optionnel) let login = null; const loginPatterns = [ /data-user-login\s*=\s*["']([^"']+)["']/i, /data-login\s*=\s*["']([^"']+)["']/i, /EV\.User\.login\s*=\s*["']([^"']+)["']/, /userLogin\s*[:=]\s*["']([^"']+)["']/ ]; for (const rx of loginPatterns) { const m = html.match(rx); if (m && m[1]) { login = m[1].trim(); break; } } if (!name && !login && !service) return null; return { name, login, service }; } // ============================================================================ // v4.2.6 : Création d'absence // ============================================================================ /** * Envoie un POST vers plan_set_holidays_popup.php pour créer une absence. * Format attendu (analysé depuis le HTML EasyVista) : * Query params : PHPSESSID, MAIN_DIRECTORY, ROOT_DIRECTORY, current_date, * empl_ids, begin_hour, end_hour, plagehoraire * Body : start_date, start_time, end_date, end_time, label_guid, dialog_action * * @param {string} origin - "https://itsma.vd.ch" ou similaire * @param {string} phpsessid * @param {Object} opts - { techIds: string[], startDate: "DD/MM/YYYY", * startTime: "HH:MM:SS", endDate, endTime, * typeGuid, currentDate } */ async function submitAbsence(origin, phpsessid, opts) { const emplIds = (opts.techIds || []).join(","); if (!emplIds) throw new Error("Aucun technicien sélectionné"); const internalurltime = Math.floor(Date.now() / 1000); const url = `${origin}/include/components/staff/planning/plan_set_holidays_popup.php` + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + `&internalurltime=${internalurltime}` + `&MAIN_DIRECTORY=${encodeURIComponent("/")}` + `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}` + `¤t_date=${encodeURIComponent(opts.currentDate)}` + `&empl_ids=${encodeURIComponent(emplIds)}` + `&begin_hour=8` + `&end_hour=18` + `&plagehoraire=0`; const body = new URLSearchParams(); body.set("start_date", opts.startDate); body.set("start_time", opts.startTime); body.set("end_date", opts.endDate); body.set("end_time", opts.endTime); body.set("label_guid", opts.typeGuid); body.set("dialog_action", "save_holidays"); console.log("[bg] submitAbsence →", url.substring(0, 140)); console.log("[bg] body:", body.toString()); const r = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString() }); console.log("[bg] status =", r.status); if (!r.ok) { throw new Error("HTTP " + r.status); } const responseText = await r.text(); if (looksLikeLoginPage(responseText)) { throw new Error("session_expired"); } // Succès : on ne sait pas le format exact de la réponse EasyVista, on // considère qu'un HTTP 200 non-login signifie succès. return { status: r.status }; } // ============================================================================ // v4.2.6 : Envoi sur douchette // ============================================================================ /** * Envoie la planification du jour sur la douchette des techs sélectionnés. * * Endpoint identifié (via l'inspection de la page EasyVista) : * POST /include/components/staff/planning/plan_set_tech_planif_popup.php * Query : PHPSESSID, current_date, empl_ids (CSV), begin_hour, end_hour, * plagehoraire * Body : dialog_action=save_planif * * Contrairement à l'absence, un seul POST suffit pour tous les techs (empl_ids * est une CSV), pas besoin de boucler. * * @param {string} origin * @param {string} phpsessid * @param {Object} opts - { techIds, currentDate } * @returns {{ okCount, errors }} */ async function submitDouchette(origin, phpsessid, opts) { const techIds = opts.techIds || []; if (techIds.length === 0) throw new Error("Aucun technicien sélectionné"); const emplIds = techIds.join(","); const internalurltime = Math.floor(Date.now() / 1000); const url = `${origin}/include/components/staff/planning/plan_set_tech_planif_popup.php` + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + `&internalurltime=${internalurltime}` + `&MAIN_DIRECTORY=${encodeURIComponent("/")}` + `&ROOT_DIRECTORY=${encodeURIComponent("/ccv/data/www/itsma/htdocs/")}` + `¤t_date=${encodeURIComponent(opts.currentDate)}` + `&empl_ids=${encodeURIComponent(emplIds)}` + `&begin_hour=8` + `&end_hour=18` + `&plagehoraire=0`; const body = new URLSearchParams(); body.set("dialog_action", "save_planif"); console.log("[bg] submitDouchette →", url.substring(0, 160)); console.log("[bg] body:", body.toString()); console.log("[bg] techs:", emplIds); try { const r = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString() }); console.log("[bg] status =", r.status); if (r.status === 401 || r.status === 403) { return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) }; } if (!r.ok) { return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "HTTP " + r.status })) }; } const responseText = await r.text(); if (looksLikeLoginPage(responseText)) { return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: "session_expired" })) }; } return { okCount: techIds.length, errors: [] }; } catch (err) { const msg = err && err.message ? err.message : String(err); return { okCount: 0, errors: techIds.map(t => ({ techId: t, error: msg })) }; } } // ============================================================================ // v5.0.0 : Suppression d'une absence ou d'une réservation // ============================================================================ /** * Supprime un item du planning (absence ou réservation) côté EasyVista. * * v5.0.1 : l'endpoint exact n'est pas totalement certain selon les versions * EasyVista. On essaye plusieurs `function_name` jusqu'à trouver celui qui * marche. Un "status 200" ne garantit pas que ça a été supprimé (l'API peut * répondre 200 même sur un nom de fonction inconnu), mais ça + le reload * post-suppression donne un bon signal : si le ticket est toujours là après * reload, on réessaye avec le nom suivant. * * Pour l'absence, dans le HTML le bouton "Supprimer" appelle : * onclick="g_arr_player[N].delete_absence();" * qui fait probablement un GET /planning_updator_xhr.php?function_name=... * mais le nom exact varie (peut être "delete_absence", "Planning_delete_absence", * "fc_delete_absence", etc.) * * @param {string} origin * @param {string} phpsessid * @param {string} actionId - ID de l'action à supprimer * @param {string} kind - "absence" ou "reservation" */ async function deletePlanningItem(origin, phpsessid, actionId, kind) { if (!actionId) throw new Error("actionId manquant"); // v5.0.1 : plusieurs function_name à tester dans l'ordre (du plus probable // au moins probable). Le premier qui renvoie 200 ET non-login est considéré OK. const fnNames = kind === "reservation" ? [ "Planning_delete_reservation", "delete_reservation", "fc_delete_reservation", "delete_act_reservation", "delete_planning_reservation", "remove_reservation", // v5.0.2 : réservations sont parfois traitées comme absences côté API "Planning_delete_absence", "delete_absence", "fc_delete_absence" ] : [ // v5.0.2 : élargir la liste, on a essayé 3 sans succès. Les variantes // plausibles vues dans les API EasyVista : "Planning_delete_absence", // le plus "officiel" "delete_absence", // le nom JS dans le onclick "fc_delete_absence", // pattern fc_* "delete_act_absence", // parfois "act_" dans les noms "Planning_delete_holiday", // en anglais "delete_holiday", "fc_delete_holiday", "delete_planning_absence", // variation complète "remove_absence" ]; let lastErr = null; let lastBody = null; for (const fn of fnNames) { const url = `${origin}/planning_updator_xhr.php` + `?PHPSESSID=${encodeURIComponent(phpsessid)}` + `&function_name=${encodeURIComponent(fn)}` + `&action_id=${encodeURIComponent(actionId)}`; console.log(`[bg] deletePlanningItem → tente function_name=${fn}`, url.substring(0, 180)); try { const r = await fetch(url, { method: "GET", credentials: "include" }); const body = await r.text(); console.log(`[bg] status=${r.status} | body (200 premiers chars) : ${(body || "").substring(0, 200)}`); if (r.status === 401 || r.status === 403) { throw new Error("session_expired"); } if (!r.ok) { lastErr = new Error("HTTP " + r.status); continue; // tente le prochain } if (looksLikeLoginPage(body)) { throw new Error("session_expired"); } // v5.0.1 : heuristique pour détecter si la suppression a marché. // EasyVista renvoie typiquement : // - une chaine vide ou "ok" ou "1" si succès // - un message d'erreur / html d'erreur si function_name inconnu // On considère que tout ce qui n'est pas un message d'erreur évident // est un succès. Si plusieurs fn renvoient 200, on prend le premier. const trimmed = (body || "").trim().toLowerCase(); const looksLikeError = trimmed.includes("error") || trimmed.includes("erreur") || trimmed.includes("unknown function") || trimmed.includes("fonction inconnue") || trimmed.includes(" ({ id, name: "? (" + id + ")", alreadyInTeam: true })), groupId }; } // Parser le HTML. Différents patterns possibles. const results = []; const currentIdsSet = new Set(supportIds.split(",").filter(Boolean)); // v5.0.1 : log le début du HTML pour diagnostic si parsing échoue console.log("[bg] popup HTML (début) =", popupHtml.substring(0, 500)); // Pattern 1 : checkboxes + texte voisin const rxCheckbox = /]*type=["']checkbox["'][^>]*value=["'](\d{4,7})["'][^>]*>([\s\S]{0,400}?)(?= r.id === id)) { results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); } } console.log("[bg] parsing pattern 1 (checkbox) :", results.length, "résultats"); // Pattern 2 : fallback if (results.length === 0) { const rxOption = /]*value=["'](\d{4,7})["'][^>]*>([^<]+)<\/option>/gi; let mO; while ((mO = rxOption.exec(popupHtml)) !== null) { const id = mO[1]; const name = (mO[2] || "").trim(); if (!results.some(r => r.id === id)) { results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); } } console.log("[bg] parsing pattern 2 (option) :", results.length, "résultats"); } // Pattern 3 : fallback brut tags HTML contenant ID à proximité d'un nom if (results.length === 0) { // Chercher chaque ID 4-7 chiffres et regarder les 200 caractères qui suivent const rxAnyId = /\b(\d{5,7})\b([\s\S]{0,200})/g; let mA; while ((mA = rxAnyId.exec(popupHtml)) !== null) { const id = mA[1]; // Ignorer les IDs qui ressemblent à des timestamps / hash if (id.length > 6 && parseInt(id, 10) > 1000000000) continue; const context = mA[2]; const nameMatch = context.match(/([A-ZÀ-ÿ][A-Za-zÀ-ÿ\-']+,\s*[A-ZÀ-ÿ][A-Za-zÀ-ÿ\-' \.]{2,30})/); if (nameMatch && !results.some(r => r.id === id)) { results.push({ id, name: nameMatch[1].trim(), alreadyInTeam: currentIdsSet.has(id) }); } } console.log("[bg] parsing pattern 3 (brut) :", results.length, "résultats"); } // Ajouter les IDs actuels manquants (sans nom) for (const id of currentIdsSet) { if (!results.some(r => r.id === id)) { results.push({ id, name: "? (" + id + ")", alreadyInTeam: true }); } } console.log("[bg] " + results.length + " personnes retournées"); return { ids: results, groupId: groupId }; } // ============================================================================ // Messages du viewer // ============================================================================ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { (async () => { try { if (msg.type === "getSession") { const session = await findEasyVistaSession(); sendResponse({ ok: true, session }); return; } if (msg.type === "fetchPlanning") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { // Fetch XML calendar_block du planning (rapide ~40 ko) const xml = await fetchPlanningXml(session.origin, session.phpsessid, msg.unixDate); if (looksLikeLoginPage(xml)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, xml, session }); } catch (err) { // v4.2 : classification de l'erreur pour afficher le bon écran const errorCode = err.kind || ( /network|fetch|typeerror/i.test(err.message) ? "ev_unreachable" : "ev_unreachable" ); sendResponse({ ok: false, error: errorCode, httpStatus: err.status, detail: err.message }); } return; } if (msg.type === "fetchXhr2") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const body = await fetchXhr2(session.origin, session.phpsessid, msg.actionId); sendResponse({ ok: true, body }); } catch (err) { sendResponse({ ok: false, error: err.kind || "fetch_failed", httpStatus: err.status, detail: err.message || String(err) }); } return; } if (msg.type === "fetchFiche") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const html = await fetchFicheHtml(session.origin, session.phpsessid, msg.formLink); if (looksLikeLoginPage(html)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, html, session }); } catch (err) { sendResponse({ ok: false, error: err.kind || "fetch_failed", httpStatus: err.status, detail: err.message || String(err) }); } return; } if (msg.type === "fetchTimelineApi") { const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const body = await fetchTimelineApi( session.origin, session.phpsessid, msg.guid, msg.formId, msg.formChecksum ); if (looksLikeLoginPage(body)) { sendResponse({ ok: false, error: "session_expired" }); return; } sendResponse({ ok: true, body }); } catch (err) { sendResponse({ ok: false, error: err.kind || "fetch_failed", httpStatus: err.status, detail: err.message || String(err) }); } return; } if (msg.type === "fetchCurrentUser") { // v4.2 : essaie d'identifier l'utilisateur EasyVista connecté en // fetchant la page d'accueil et en cherchant dans le HTML un champ // contenant son nom. Si on trouve rien, on renvoie { ok: true, // user: null } pour que l'UI sache qu'on n'a pas pu. const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const user = await fetchCurrentUser(session.origin, session.phpsessid); sendResponse({ ok: true, user }); } catch (err) { sendResponse({ ok: false, error: String(err) }); } return; } if (msg.type === "submitAbsence") { // v4.2.6 : crée une absence dans EasyVista via POST vers // /include/components/staff/planning/plan_set_holidays_popup.php const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const result = await submitAbsence(session.origin, session.phpsessid, msg); sendResponse({ ok: true, result }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); } return; } if (msg.type === "submitDouchette") { // v4.2.6 : envoie la planification sur la douchette de chaque tech. // On teste plusieurs URLs possibles (l'endpoint exact n'est pas dans // le HTML statique que nous avons analysé). const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const result = await submitDouchette(session.origin, session.phpsessid, msg); sendResponse({ ok: true, okCount: result.okCount, errors: result.errors }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); } return; } if (msg.type === "deletePlanningItem") { // v5.0.0 : supprime une absence ou réservation côté EasyVista. // Endpoint : /planning_updator_xhr.php?function_name=...&action_id=... // Exemples de function_name : // - Planning_delete_absence // - Planning_delete_reservation const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const result = await deletePlanningItem( session.origin, session.phpsessid, msg.actionId, msg.kind ); sendResponse({ ok: true, result }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); } return; } if (msg.type === "detectTeam") { // v5.0.0 : détecte la liste des IDs de techniciens depuis le HTML // v5.0.1 : retourne aussi les noms via la popup group_supports const session = await findEasyVistaSession(); if (!session) { sendResponse({ ok: false, error: "no_session" }); return; } try { const result = await detectTeamFromEV(session.origin, session.phpsessid); // result = { ids: [{id,name,alreadyInTeam}, ...], groupId } sendResponse({ ok: true, members: result.ids, groupId: result.groupId }); } catch (err) { sendResponse({ ok: false, error: err.message || String(err) }); } 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 }); return; } sendResponse({ ok: false, error: "unknown_message" }); } catch (err) { console.error("background error:", err); sendResponse({ ok: false, error: err.message || String(err) }); } })(); // Retourner true pour garder sendResponse asynchrone return true; }); // ============================================================================ // v4.2 : les alarmes d'auto-refresh 12h/15h ont été supprimées. Seul le // nettoyage quotidien des caches > 7 jours reste. // On supprime aussi activement les anciennes alarmes créées par les // versions précédentes pour éviter qu'elles restent programmées. // ============================================================================ async function clearLegacyRefreshAlarms() { try { await chrome.alarms.clear("refresh_12h"); await chrome.alarms.clear("refresh_15h"); } catch (e) { console.warn("clearLegacyRefreshAlarms:", e); } } // ============================================================================ // Nettoyage caches > 7 jours // ============================================================================ async function cleanupOldCaches(daysToKeep) { const all = await chrome.storage.local.get(null); const threshold = new Date(); threshold.setDate(threshold.getDate() - daysToKeep); const thresholdStr = threshold.toISOString().substring(0, 10); // YYYY-MM-DD const toRemove = []; for (const key of Object.keys(all)) { // Nos clés de cache sont planning_cache_YYYY-MM-DD const m = key.match(/^planning_cache_(\d{4}-\d{2}-\d{2})$/); if (m && m[1] < thresholdStr) { toRemove.push(key); } } if (toRemove.length > 0) { await chrome.storage.local.remove(toRemove); } return toRemove.length; } // Au démarrage, nettoyer les anciennes alarmes et les anciens caches chrome.runtime.onInstalled.addListener(() => { clearLegacyRefreshAlarms(); cleanupOldCaches(7).catch(err => console.warn("cleanup:", err)); }); chrome.runtime.onStartup.addListener(() => { clearLegacyRefreshAlarms(); cleanupOldCaches(7).catch(err => console.warn("cleanup:", err)); });