# Melhorias de Cache de Borda e CSRF (L1 / L1.0) — rodadas 2026-09-08 → 2026-09-09

Registro das melhorias aplicadas nas últimas rodadas sobre performance de borda dos
portais Laravel 5.5 / PHP 7.2 / EasyApache 4 (`ea-php72`). Tarefa #23410.

> Detalhes de implementação do L1.1/L1.2 em
> `wiki/infra-update/correcao-cache-control-borda-l1.md`; configuração de servidor
> em `wiki/infra-update/WHM.md`. Este documento é o **sumário das rodadas**.

## Contexto / objetivo

O harness indicava `cache_hit = 0` e `cache_policy` baixo: nenhuma página pública
era cacheada na borda. As rodadas trataram:

- **L1.1** — headers `Cache-Control` públicos em GET de conteúdo do portal.
- **L1.2** — `ETag` + resposta `304` em revalidação.
- A descoberta de que o **CSRF por-request** inviabiliza tanto o `304` quanto o
  cache público seguro, e o início do **L1.0** (desacoplar CSRF do HTML cacheável).

## O que foi feito

### 1. L1.1 — Middleware `CacheControl` (headers de borda)

- `app/Http/Middleware/CacheControl.php` (middleware **global**, registrado em
  `Kernel.php` em `$middlewarePriority` + grupo `web` **primeiro**, para rodar
  **por último** no outbound, após `StartSession`).
- Para `GET` de controllers `Portal\` (sem redirect, status < 400, não excluído
  por nome): remove `Pragma`/`Expires`/`Set-Cookie` e seta
  `Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=86400,
  stale-if-error=604800` + `Vary: Accept-Encoding`.
- Abordagem minimalista (sem refactor de grupo de rota `web.public`) para não
  arriscar CSRF/admin.

### 2. L1.2 — `ETag` + `304`

- No mesmo middleware, após setar `Cache-Control`: `ETag = md5(conteúdo)`; responde
  `304` quando o `If-None-Match` bate (ignora weak `W/`, aceita `*`). Fecha o
  Critério **A2** (`revalidation > 0`).
- Cobertura de teste: `tests/Unit/CacheControlTest.php` — **20 testes / 60 asserções**
  (unit + smoke em `artisan serve` no container).

### 3. Descoberta crítica: CSRF por-request inviabiliza cache público (e 304)

- Todo layout do portal embute `<meta name="csrf-token" content="{{ csrf_token() }}">`
  + hidden `_token` (ex.: `resources/views/portal/layouts/modelo1/layout/head.blade.php:15`,
  iguais em `modelo2`–`modelo6`). Como o `CacheControl` remove o cookie de sessão,
  cada `GET` anônimo abre **nova sessão** → o HTML **varia por request**.
- Impacto:
  1. `ETag = md5(corpo)` varia → `304` **nunca dispara** em nenhuma página do portal.
  2. Se a borda cacheasse esse HTML `public`, serviria o token de uma sessão a outra
     → **CSRF 419 "Page Expired"** no submit de qualquer formulário.
- Pré-requisito registrado: seed `public_infra_update-c556` (**L1.0**).

### 4. Mitigação de segurança (no próprio `CacheControl`)

- Inspeção do corpo da resposta: se há `csrf-token` / `_token`, responde
  `Cache-Control: private, no-cache` e **mantém o cookie de sessão**; só páginas
  **sem CSRF** recebem `public` + `ETag`.
- Resultado hoje: como **todas** as páginas do portal têm o meta csrf, nenhuma é
  cacheada publicamente ainda — L1.1 entrega zero cache até o L1.0. Isso **evita o
  419** mesmo quando a borda passar a cachear HTML.

### 5. L1.0 — desacoplamento do CSRF do HTML cacheável (Fase 1 + Fase 2)

**Fase 1 (endpoint + JS):**
- `app/Http/Controllers/Portal/CsrfTokenController.php` + rota `GET /csrf-token`
  (nome `csrf.token`, grupo `web`): devolve `{token: csrf_token()}`,
  `Cache-Control: no-cache, private`, **mantém o cookie de sessão** e grava também
  o cookie `portal_csrf` (httpOnly=false, legível por JS) para as próximas
  navegações. Excluída do cache no `CacheControl` (`excludeNames` = `['csrf']`).

**Fase 2 (strip + flip public) — 2026-09-09:**
- `CacheControl`: para `GET` de controllers `Portal\`, **remove o VALOR** do token
  CSRF do corpo da resposta (`meta[name=csrf-token]` / `input[name=_token]` /
  `var token = '…'` / `_token: '…'` / `'X-CSRF-TOKEN': '…'`) via regex ancorado
  (charset `[A-Za-z0-9+]{40}`, não toca hashes de asset/URL). O nome do atributo
  permanece, o valor é esvaziado → corpo **estável** (ETag constante) e sem token
  vazado entre sessões.
- Em seguida aplica `Cache-Control: public` (+ `s-maxage=300`) + remove `Set-Cookie`
  + `ETag`/304 (L1.2). `excludeNames` reduzido a `['csrf']` (as páginas de formulário
  `contato`/`ouvidoria`/… também ficam cacheáveis, pois o token é client-side).
- `resources/views/portal/layouts/csrf-inject.blade.php`: **refatoração JS centralizada**
  — `window.PortalCsrf` obtém o token (cookie `portal_csrf` ou `/csrf-token`), injeta
  `X-CSRF-TOKEN` em `jQuery.ajaxSend` e `window.fetch`, e preenche `_token` no
  `submit` de `form[POST]`. Incluído em `modelo1`–`modelo6/layout/head.blade.php`.
- **CMS INTOCADO:** `CacheControl` só atua em ações `Portal\` (Admin/Prefecture/Api
  ficam de fora, mantêm sessão/cookie); `csrf-inject` só é incluído nos layouts do
  portal. Os ~60 blades do portal NÃO foram editados — o token é removido no
  middleware, o que é mais seguro (sem risco de arquivo esquecido vazar token) e
  equivalente a editar os templates.

### 6. Seeds / planejamento (`sd`)

- `public_infra_update-f9fc` (L1.2): **fechada** (implementado + testado).
- `public_infra_update-daa9` (L1.1): descrição alinhada à abordagem minimalista +
  gate CSRF; **bloqueada por `c556`**.
- `public_infra_update-4289` (L2.3 FPC): **planejada**; bloqueada por `daa9` + `c556`.
- `public_infra_update-c556` (L1.0): **criada**, adotada em `pl-cc49`, `in_progress`.
- L0 (`5cb8`/`68bb`/`e6bd`/`6121`) e L2.1 (`d9f8`/`8e67`/`3488`): **fechadas**
  (concluídas no servidor).
- `pl-cc49` (approach): atualizado para refletir L1.2 feito, L1.3 adiada (A4),
  L0/L2.1 concluídos e L1.0 como pré-requisito.

### 7. Documentação de servidor (WHM / EA4)

- `wiki/infra-update/WHM.md`: stack **EA4 `ea-php72`** (não alt-php); APCu ativo via
  **build manual pecl**; OPcache ativo (`validate_timestamps=0`).
- `correcao-cache-control-borda-l1.md` §6 (gate L1.0) e §7 (EA4 vs PHP Selector)
  reescritos para a realidade EA4.

## Status

- L1.1 + L1.2 + L1.0 (Fase 1 e Fase 2) **implementados e testados em container**
  (21 testes em `tests/Unit/CacheControlTest.php`, incluindo: token removido do
  corpo, ETag idêntico para tokens distintos, `/csrf-token` fora do cache).
  **Deploy no canário pendente.** `.seeds/` pendente de `sd sync`.
- `cache_hit` só sobe de fato quando a borda **Multi-CDN** cachear o HTML
  (`s-maxage` + sem cookie) — track **A4**.

## Próximos passos

1. **Deploy canário** de L1.1/L1.2 + L1.0; medir `ETAPA=01-cache-cdn`
   (`cache_policy` sobe; `cache_hit` depende de A4).
2. **Validar em canário** forms (newsletter/busca/ouvidoria/contato) sem 419 **com
   CDN cacheando o HTML**; `ETag` estável + `304` (A2). Confirmar que o cookie
   `portal_csrf` + `/csrf-token` entregam o token certo por sessão.
3. **L2.3** (FPC) após L1.0.
4. `sd sync` para commitar `c556` (e eventuais `daa9`/`4289` desbloqueados).

## Guardrails / riscos

- Não remover `Set-Cookie` de páginas com formulário antes do L1.0 ⇒ CSRF 419.
- Nunca usar `<IfModule mod_headers.c>` (causa HTTP 500 neste servidor).
- EA4-only: `opcache.*` / `apcu.*` / `realpath_cache_*` são `PHP_INI_SYSTEM` → só no
  php.ini de sistema (`/opt/cpanel/ea-php72/root/etc/php.ini`) + `php.d`; ignorados
  no `.user.ini` da conta.
- APCu via pecl manual é sobrepujado por rebuild do EasyApache 4 (ver `WHM.md` P0-C).
