const PID_API_BASE = "/api"; function pidGetCookie(name) { const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; } async function pidEnsureCsrfCookie() { if (pidGetCookie("csrftoken")) return; await fetch(`${PID_API_BASE}/auth/csrf/`, { credentials: "include" }); } /** * Escape de texto para montar HTML por template string (`innerHTML`). Vale * para conteúdo **e** para valor de atributo entre aspas duplas ou simples: * trata `& < > " '`. Todo dado vindo do servidor, do usuário ou de arquivo * anexado passa por aqui antes de entrar num `innerHTML` (senão um texto com * HTML vira código executado no navegador de quem abre a tela: XSS). Nunca * usar em HTML que já é intencional (texto rico sanitizado no servidor com * nh3, como a Ajuda e as observações de Acessos Gerais). */ const PID_ESCAPE_HTML_MAPA = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; function pidEscapeHtml(texto) { if (texto === null || texto === undefined) return ""; return String(texto).replace(/[&<>"']/g, (ch) => PID_ESCAPE_HTML_MAPA[ch]); } function pidErrorMessageFrom(data) { if (!data) return "Erro ao processar a solicitação."; if (typeof data === "string") return data; if (typeof data.detail === "string") return data.detail; const values = Object.values(data).flat().filter((v) => typeof v === "string"); return values.length ? values.join(" ") : "Erro ao processar a solicitação."; } /** * Fetch com sessão (cookie) do Django. Adiciona X-CSRFToken automaticamente em * métodos que não são GET/HEAD/OPTIONS, buscando o cookie csrftoken antes se * ainda não existir. Em 401, redireciona para o login por padrão — passe * `redirectOn401: false` em telas onde uma resposta 401 é esperada (ex.: o * próprio formulário de login, ou a checagem de sessão já ativa em index.html). */ async function pidApiRequest(path, options = {}) { const method = (options.method || "GET").toUpperCase(); const isSafeMethod = method === "GET" || method === "HEAD" || method === "OPTIONS"; const redirectOn401 = options.redirectOn401 !== false; if (!isSafeMethod) await pidEnsureCsrfCookie(); const headers = { Accept: "application/json", ...(options.headers || {}) }; let body = options.body; const isFormData = typeof FormData !== "undefined" && body instanceof FormData; if (body !== undefined && !isFormData) { headers["Content-Type"] = "application/json"; body = JSON.stringify(body); } // FormData: deixa o browser definir o Content-Type (com o boundary do multipart) sozinho. if (!isSafeMethod) headers["X-CSRFToken"] = pidGetCookie("csrftoken") || ""; let response; try { response = await fetch(`${PID_API_BASE}${path}`, { method, credentials: "include", headers, body, }); } catch (e) { throw new Error("Não foi possível conectar ao servidor."); } if (response.status === 401 && redirectOn401) { window.location.href = "index.html"; return null; } if (response.status === 204) return null; const text = await response.text(); let data = null; if (text) { try { data = JSON.parse(text); } catch (e) { data = null; } } if (!response.ok) { throw new Error(pidErrorMessageFrom(data)); } return data; }