Files
echo-du-huit/docs/solutions/2026-06-30-trouver-l-url-ical-publique-nextcloud-o42d-solution.md
T
2026-06-30 00:20:45 +02:00

6.2 KiB

Solution : [T1] Lire les événements via CalDAV (session navigateur)

Problème résolu

Donner à l'extension sa matière première : le flux iCalendar brut du calendrier Nextcloud de l'Atelier du Huit, en réutilisant la session du navigateur (cookies), sans jamais stocker de mot de passe. Frontière T1 tenue : on s'arrête au flux brut vérifié + une preuve de vie (comptage des VEVENT). Le parsing ical.js, le dédoublonnage par UID, les RRULE/fuseaux et le filtrage « à venir » sont hors T1 (tâche T2).

Le vrai enjeu n'était pas « fetcher une URL » mais savoir qu'on tient bien un calendrier : Nextcloud peut renvoyer une page de login en HTTP 200 HTML, donc response.ok ne suffit pas à garantir la validité du flux.

Approche choisie

GET …/?export + fetch direct depuis la page de l'extension (credentials:"include"), validation du flux par sniffing du corps (trimStart().startsWith("BEGIN:VCALENDAR")). Transport isolé dans son propre module extension/nextcloud.js, pur de toute présentation : il renvoie un type d'erreur, pas un message.

Pourquoi pas les alternatives :

  • CalDAV REPORT + time-range (filtre serveur, expansion RRULE) : corps XML à construire/maintenir, multi-statut 207, sémantique d'expansion à valider — surdimensionné pour 473 événements. Le ?export brut suffit pour T1.
  • Couche réseau centralisée dans le background : utile plus tard (lecture CalDAV des statuts, cache), mais sur-ingénierie tant qu'il n'y a qu'un consommateur. On garde le déplacement peu coûteux grâce au module isolé.

Décisions clés

  • Résultat discriminé { ok:true, flux } | { ok:false, erreur } avec erreur ∈ {"non-connecte","reseau","serveur"}. Le mapping type → message FR actionnable vit dans liste.js, pas dans le transport. Séparation nette données / présentation.
  • fetchImpl injecté et obligatoire (convention « params obligatoires par défaut ») : le module est testable sans réseau réel (Bun fabrique des Response), et le wiring navigateur reste un détail de l'appelant.
  • Ordre des branches : 401/403 traités avant !reponse.ok (sinon le générique « serveur » les masquerait), puis sniffing du corps en dernier.
  • Détection « non connecté » par sniffing, pas par redirect:"manual" : le corps après redirection-login (200 HTML) est déjà détecté ; inutile de complexifier. 401/403 captés en amont pour le cas réel.
  • manifest.json intact : host_permissions couvrait déjà https://atelier-huit.frama.space/*. Changement purement additif.
  • Constatation terrain (testée contre le vrai serveur) : sans cookie, Nextcloud répond 401 (Sabre\DAV\Exception\NotAuthenticated, 0 redirect), pas un 200 HTML de login. Le sniffing du corps reste une ceinture-et- bretelles utile (cas redirection possible selon config), mais la branche réellement empruntée « pas connecté » est le 401.

Patterns à réutiliser

Transport injectable, pur, à résultat typé (testable sans réseau) :

export async function recupererFluxCalendrier(fetchImpl) {
  let reponse;
  try { reponse = await fetchImpl(URL_CALENDRIER, { credentials: "include" }); }
  catch { return { ok: false, erreur: "reseau" }; }       // fetch lève = réseau
  if (reponse.status === 401 || reponse.status === 403)
    return { ok: false, erreur: "non-connecte" };          // session refusée
  if (!reponse.ok) return { ok: false, erreur: "serveur" };
  const flux = await reponse.text();
  if (!flux.trimStart().startsWith("BEGIN:VCALENDAR"))     // login 200 HTML
    return { ok: false, erreur: "non-connecte" };
  return { ok: true, flux };
}

Comptage ancré ligne (ne compte pas une occurrence dans une DESCRIPTION, tolère le CRLF de la RFC 5545) :

export function compterEvenements(flux) {
  return (flux.match(/^BEGIN:VEVENT\s*$/gm) ?? []).length;
}

Wrapper fetch côté page (voir piège ci-dessous) :

const res = await recupererFluxCalendrier((u, o) => fetch(u, o));

Rendu d'état reconstruit à chaque tentative : etat.replaceChildren() avant de re-rendre → pas d'empilement de messages/boutons/listeners quand on clique Réessayer plusieurs fois. Région #etat en role="status" aria-live="polite" pour l'accessibilité.

Tests Bun important un module ES du navigateur : import { … } from "./extension/nextcloud.js", fetchImpl remplacé par async () => new Response(corps, { status }) ou un throw. Aucun jsdom, aucun réseau.

Pièges à éviter

  • response.ok ne prouve pas un calendrier : la page de login peut arriver en 200 HTML. Toujours valider le contenu (BEGIN:VCALENDAR), pas que le statut. C'est le piège central de la tâche.
  • fetch nu détaché de window lève « Illegal invocation » dans le navigateur. Passer un wrapper (u, o) => fetch(u, o) (ou fetch.bind(window)), jamais fetch nu en argument.
  • Regex non ancrée : /BEGIN:VEVENT/g compterait une occurrence apparaissant dans une DESCRIPTION. Ancrer ^…$ + flag m.
  • Ordre des branches d'erreur : tester 401/403 avant le !ok générique, sinon « non-connecte » serait noyé dans « serveur ».
  • BOM / espaces en tête du flux : trimStart() avant la comparaison startsWith, sinon un VCALENDAR valide précédé d'un BOM serait rejeté.
  • 0 VEVENT n'est pas une erreur : un calendrier valide mais vide est un succès (« 0 événement lu »), à ne pas confondre avec un échec.
  • CSS mort : la classe .liste-vide (squelette) n'est plus utilisée depuis que liste.js orchestre #etat. Signalée P3 en review, laissée volontairement car T2 (rendu détaillé de la liste) la réemploiera. À nettoyer si T2 ne la reprend pas.
  • Warning MISSING_DATA_COLLECTION_PERMISSIONS (web-ext) : préexistant sur manifest.json (commit squelette), non touché par T1, hors périmètre — pas une régression. À traiter dans une tâche dédiée au manifest.

Tags

tags: [firefox-extension, manifest-v3, webextension, vanilla-js, caldav, ical, nextcloud, fetch, session-cookies, bun-test, dependency-injection, error-handling, low-tech]