# 08 — Melhorias específicas de código

> Consolidação das mudanças **de código** (PHP/Laravel/Blade/JS/config) feitas no
> `v6-hardening` para viabilizar performance, cache, segurança e robustez de
> deploy. Complementa [`04-cache-performance.md`](04-cache-performance.md) e
> [`05-seguranca.md`](05-seguranca.md), que explicam as camadas de infraestrutura.
>
> **Stack:** PHP 7.2.34 / Laravel 5.5.50. Todas as soluções respeitam os limites
> dessa stack (sem arrow functions, typed properties, `match`, etc.).
>
> **Cópias literais:** os 83 arquivos de código/config citados aqui estão em
> [`arquivos/codigo/`](arquivos/codigo/), preservando o caminho relativo ao
> repositório. O [`arquivos/manifest.sha256`](arquivos/manifest.sha256) cobre
> todas as cópias.

---

## 1. Mapa rápido por arquivo

| Arquivo | Tipo de melhoria | Ganho |
|---|---|---|
| `app/Http/Kernel.php` | Ordem de middlewares | Garante cache/ETag/CSP corretos no outbound |
| `app/Http/Middleware/CacheControl.php` | Novo middleware | Cache de borda + ETag/304 + strip de CSRF |
| `app/Http/Middleware/PageCache.php` | Novo middleware | Full-Page Cache de origem |
| `app/Http/Middleware/ServerTiming.php` | Novo middleware | Mede TTFB de origem (`Server-Timing`) |
| `app/Http/Middleware/HtmlMinify.php` | Novo middleware | Reduz HTML sem tocar em blocos sensíveis |
| `app/Http/Middleware/PreloadCritical.php` | Novo middleware | Preload de CSS crítico |
| `app/Http/Middleware/PictureTransform.php` | Novo middleware | `<img>` → `<picture>` WebP responsivo |
| `app/Http/Middleware/CspNonce.php` | Novo middleware | CSP por nonce (hoje desligado) |
| `app/Http/Middleware/VerifyCsrfToken.php` | Exceção | Libera `POST /csp-report` |
| `app/Http/Controllers/Portal/CsrfTokenController.php` | Novo controller | Token CSRF client-side |
| `app/Http/Controllers/Portal/CspReportController.php` | Novo controller | Coletor de violações CSP |
| `app/Http/Controllers/Portal/RobotsController.php` | Novo controller | `robots.txt` dinâmico por host |
| `app/Http/Controllers/Portal/SitemapController.php` | Reescrita | `sitemap.xml` válido, sem gravar disco |
| `app/Exceptions/Handler.php` | Correção | 404 real (sem soft-404) |
| `app/Http/Traits/GetInformationsPrefectureTrait.php` | Memoização + eager loading | Menos queries por request |
| `app/Http/Traits/NewsTrait.php` | Cache versionado | Listagens de notícias mais baratas |
| `app/Http/Traits/FileTrait.php` | Novas funções | Variantes WebP no upload |
| `app/Http/Model/Prefecture/Content.php` | Eager loading | `ultimasNoticias` sem N+1 |
| `app/Http/Model/Prefecture/Configuration.php` | Invalidação | Cache de config limpo ao editar |
| `app/Providers/*ServiceProvider.php` | `Cache::remember` | Dados de layout cacheados |
| `app/Providers/AppServiceProvider.php` | Observers/registro condicional | Invalidação + boot seguro |
| `app/Services/Cdn/CdnPurgeService.php` | Novo serviço | Purge Multi-CDN resiliente |
| `app/Console/Commands/ClearPageCache.php` | Novo comando | `cache:page:clear` |
| `app/Console/Commands/RedigestWebpImages.php` | Novo comando | Digestão contínua de WebP |
| `app/Console/Kernel.php` | Scheduler | `images:redigest-webp` a cada 10 min |
| `routes/basepro.php` | Remoção de Closures | Habilita `route:cache` |
| `routes/web.php` | Novas rotas | CSRF, CSP, robots, sitemap |
| `config/app.php`, `mail.php`, `recaptcha.php` | Leitura tolerante | Boot sem `config.json` |
| `config/gemini.php` | Novo config | `env()` centralizado para `config:cache` |
| `config/session.php` | Segurança | `secure`, `same_site` |
| `config/cache.php` | FPC | TTL, tracking params, invalidação |
| `config/cdn.php` | Novo config | Purge no-op seguro |
| `config/csp.php` | Novo config | Liga/desliga CSP |
| `composer.json` | Autoload/scripts | Helpers em produção + `composer prod` |
| `app/Http/Controllers/Prefecture/ConfigurationController.php` | Correção de upload | Evita `Path is outside...` e grava no disco certo |
| `app/Http/Requests/ConfigurationRequest.php` | Validação | `image|max:2048` em uploads |

---

## 2. Middlewares

### 2.1 `Kernel.php` — ordem cargo-crítica

O grupo `web` foi reorganizado para que o **outbound** (resposta) rode na ordem
correta:

```php
'web' => [
    \App\Http\Middleware\ServerTiming::class,
    \App\Http\Middleware\PageCache::class,
    \App\Http\Middleware\CacheControl::class,
    \App\Http\Middleware\HtmlMinify::class,
    \App\Http\Middleware\PreloadCritical::class,
    \App\Http\Middleware\PictureTransform::class,
    \App\Http\Middleware\CspNonce::class,
    \App\Http\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \App\Http\Middleware\VerifyCsrfToken::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
```

E a prioridade:

```php
protected $middlewarePriority = [
    \App\Http\Middleware\PageCache::class,
    \App\Http\Middleware\CacheControl::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Auth\Middleware\Authenticate::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];
```

**Racional:** o outbound executa em ordem inversa. `PageCache` é o primeiro da
entrada e o último da saída (pode short-circuitar HIT antes de `StartSession`).
`CacheControl` vem depois do `StartSession` no outbound para remover cookie e
sobrescrever `no-cache`. **Atenção:** `CspNonce` está *depois* de `CacheControl`
no array, portanto no outbound ele roda **antes** de `CacheControl`; com CSP
ligado, o nonce entra no corpo **antes** do cálculo do ETag e o ETag varia por
request. Com CSP desligado (estado atual) isso não acontece. Ver
[`10-achados-e-patches.md`](10-achados-e-patches.md) F-01.

### 2.2 `CacheControl.php`

Responsabilidades:

1. Agir só em `GET`, controller `Portal\`, status < 400, não-redirect.
2. Remover o **valor** do token CSRF do corpo (`stripCsrfTokens`).
3. Remover `Pragma`, `Expires`, `Set-Cookie` e esvaziar o array de cookies.
4. Definir `Cache-Control` público com `s-maxage` e `stale-*`.
5. Definir `Vary: Accept-Encoding` e remover `User-Agent`.
6. Emitir ETag **fraco** e responder `304` normalizando `If-None-Match`.

Trecho do strip de CSRF:

```php
public static function stripCsrfTokens($content)
{
    if ($content === '') return $content;

    $patterns = [
        '#(<meta name="csrf-token" content=")[A-Za-z0-9+]{40}(")#i',
        '#(name="_token"[^>]*?value=")[A-Za-z0-9+]{40}(")#i',
        '#(var\s+token\s*=\s*\')[A-Za-z0-9+]{40}(\')#i',
        '#(_token:\s*\')[A-Za-z0-9+]{40}(\')#i',
        "#(['\"]X-CSRF-TOKEN['\"][\s]*[:,][\s]*['\"])[A-Za-z0-9+]{40}(['\"])#i",
    ];

    return preg_replace($patterns, '$1$2', $content);
}
```

Trecho do ETag/304:

```php
$hash = md5($content);
$etag = 'W/"' . $hash . '"';
$response->headers->set('ETag', $etag);

$ifNoneMatch = (string) $request->headers->get('If-None-Match', '');
foreach (array_map('trim', explode(',', $ifNoneMatch)) as $candidate) {
    $candidate = ltrim($candidate, 'W/');
    $candidate = trim($candidate, '"');
    $candidate = preg_replace('/-(gzip|br)$/i', '', $candidate);
    if ($candidate === $hash || $candidate === '*') {
        $response->setStatusCode(304);
        $response->setContent(null);
        break;
    }
}
```

**Bugs corrigidos por esse código:**

- `Set-Cookie` não saía com `headers->remove()` porque os cookies ficam no
  **array** da resposta → `removeCookie()` em loop.
- ETag forte quebrava 304 quando o Apache comprimia (`-gzip`/`-br`) → ETag fraco
  + normalização.
- `Vary: User-Agent` fragmentava a borda → remoção explícita.
- Token CSRF por request impedia ETag estável e cache seguro → strip central.

### 2.3 `PageCache.php`

- Chave: `page_cache:v1:` + `md5(normalizedUrl|locale)`.
- Normaliza tracking params (`utm_*`, `gclid`, `fbclid`, etc.) e ordena a query.
- Guards: GET, `Portal\`, não autenticado, 200, `text/html`, sem `Set-Cookie`,
  rota não excluída (`csrf`).
- Em HIT: devolve `Response` com `X-Page-Cache: HIT`, reaplica `CspNonce` e
  `CacheControl`.
- Em MISS: grava corpo já estabilizado (sem token CSRF) no store.
- `flush()`: limpa registry, bumppa `VERSION_KEY`, limpa `layout.*`, chama
  `CdnPurgeService::purgeAll()` de forma resiliente.
- `LAYOUT_KEYS` e `LAYOUT_CONTENT_CATEGORIES` centralizam a invalidação.

Trecho do HIT:

```php
if (Cache::has($key)) {
    $body = Cache::get($key);
    $cached = new Response($body, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
    $cached->headers->set('X-Page-Cache', 'HIT');

    CspNonce::applyToCachedResponse($cached, $request);

    return (new CacheControl())->handle($request, function () use ($cached) {
        return $cached;
    });
}
```

Trecho do `flush()` (resumo):

```php
public static function flush()
{
    $registry = Cache::get(self::REGISTRY_KEY, []);
    $count = 0;
    foreach ($registry as $key) {
        if (Cache::forget($key)) $count++;
    }
    Cache::forever(self::REGISTRY_KEY, []);
    Cache::forever(self::VERSION_KEY, self::version() + 1);
    self::flushLayout();

    try {
        app(\App\Services\Cdn\CdnPurgeService::class)->purgeAll();
    } catch (\Throwable $e) {
        \Illuminate\Support\Facades\Log::warning('cdn.purge.flush_failed', [
            'message' => $e->getMessage(),
        ]);
    }

    return $count;
}
```

### 2.4 `ServerTiming.php`

Mede o tempo do pipeline e expõe no header:

```php
$start = microtime(true);
$response = $next($request);
$duration = (microtime(true) - $start) * 1000;

if (isset($response->headers)) { // headers é propriedade, não método
    $response->headers->set('Server-Timing', 'app;dur=' . round($duration, 2), false);
}
```

> Bug corrigido: a versão anterior usava `method_exists($response, 'headers')`,
> que falhava, e o header nunca era setado.

### 2.5 `HtmlMinify.php`

- Preserva `<pre>`, `<code>`, `<textarea>`, `<script>`, `<style>`, `<svg>` por
  placeholder; só colapsa whitespace entre tags.
- Guards: GET, `!app.debug`, `text/html`.
- Não remove comentários HTML (decisão de zero risco).
- Comentários `//` de JS foram convertidos para `/* */` **na fonte** (85 blades),
  não por regex no middleware.

### 2.6 `PreloadCritical.php`

Injeta `<link rel="preload" as="style">` para:

```php
protected $styleAssets = [
    'assets/css/app.css',
    'assets/vendor/fonts/google/fonts.css',
];
```

Idempotente: não re-injeta se o token já existe. Só portal público, GET,
`!app.debug`, `text/html`.

### 2.7 `PictureTransform.php`

- Reescreve `<img>` local do storage em `<picture>` com `<source type="image/webp">`.
- Variantes `[360, 720, 1200, 1920]`; se não houver sidecar, mantém o original.
- Pula SVG, WebP e GIF animado.
- Fallback `<img>` com `loading="lazy"` + `decoding="async"`; `fetchpriority="high"`
  no primeiro source quando a imagem era eager/LCP.
- Adiciona `width`/`height` quando ausentes (anti-CLS).
- Injeta preload do LCP no `<head>` **por variante**, com o mesmo `media` da
  `<source>` correspondente.
- Protege `<picture>` já existentes com placeholder para não re-embrulhar.

### 2.8 `CspNonce.php`

- Guardado por `config('csp.enabled')`.
- Gera nonce por request e injeta em `<script>` inline (sem `src`) e `<style>`.
- Emite `Content-Security-Policy-Report-Only` (quando ligado).
- `applyToCachedResponse()` tenta permitir que o HIT do FPC receba nonce fresco,
  mas o injetor **ignora tags que já têm `nonce=`**. Como o corpo gravado pelo
  `PageCache` no MISS já contém o nonce (CSP ligado), o HIT não o substitui —
  ver `10-achados-e-patches.md` F-01.
- Exclui `admin`, `prefecture`, `cms`.
- Não usa `unsafe-inline`; usa `nonce` + host-allowlist + `unsafe-hashes`.

### 2.9 `VerifyCsrfToken.php`

```php
protected $except = [
    'csp-report',
];
```

O endpoint de relatórios CSP não tem token (é chamado pelo browser), então fica
fora da verificação CSRF e protegido por `throttle:120,1`.

---

## 3. Controllers e rotas

### 3.1 `CsrfTokenController` + rota

```php
Route::get('/csrf-token', 'Portal\CsrfTokenController@show')->name('csrf.token');
```

Controller:

```php
public function show(Request $request): JsonResponse
{
    $token = csrf_token();

    return response()->json(['token' => $token])
        ->header('Cache-Control', 'no-cache, private')
        ->cookie('portal_csrf', $token, 120, '/', null, false, false);
}
```

- Mantém o cookie de sessão; responde `no-cache, private`.
- Cookie `portal_csrf` legível por JS para evitar corrida no primeiro load.
- Rota excluída do FPC e do `CacheControl` por nome (`csrf`).

### 3.2 `CspReportController`

- Lê o corpo bruto, tolera JSON inválido (`raw`).
- Grava uma linha JSON em `storage/logs/csp-violations.log` com
  `time`, `ip`, `user_agent`, `referer`, `report`.
- Responde `204`.
- Usa `LOCK_EX` para concorrência.

### 3.3 `RobotsController`

```php
$body = "User-agent: *\n"
    . "Disallow:\n"
    . "\n"
    . 'Sitemap: ' . url('/sitemap.xml') . "\n";

return response($body, 200)
    ->header('Content-Type', 'text/plain; charset=UTF-8');
```

Antes: `/robots.txt` caía em 404 (302 → `/error/404`). O `.htaccess` reescreve
para o front controller; a rota responde `text/plain` com o host atual.

### 3.4 `SitemapController`

Reescrito para:

- Não gravar arquivo em disco durante GET.
- Retornar `Response` com `application/xml; charset=UTF-8`.
- Usar `url('/')` (esquema correto, sem `http://` hardcoded).
- Usar `DOMDocument` (não `SimpleXMLElement`) para preservar
  `xmlns:xsi`/`xsi:schemaLocation`.
- Corrigir o bug `strpos($link, '//') == false` (agora `=== 0`).
- Filtrar URLs de hosts externos.
- `createTextNode` para escapar `&`, `<`, `>`.
- `lastmod`/`priority` por URL.

Trecho:

```php
if (preg_match('#^https?://#i', $link) || strpos($link, '//') === 0) {
    $urls[] = $link;
} else {
    $urls[] = $base . '/' . ltrim($link, '/');
}
```

### 3.5 `Handler.php` — 404 real

```php
if ($exception instanceof NotFoundHttpException) {
    if ($exception->getMessage() == 'view_inexistente') {
        return response()->view('portal.manutencao.index');
    }

    return response()->view('admin.errors.404', [], 404);
}
```

Antes: redirecionava para `/error/404` com 302 e a página final respondia 200
(soft-404). Agora o crawler recebe **404 direto**.

### 3.6 Rotas `robots`, `sitemap`, `manifest`, `csp-report`

Em `routes/web.php`, essas rotas ficam **fora** do grupo
`check.site.manutention`:

```php
Route::get('/manifest.webmanifest', 'Portal\ManifestController@show')->name('manifest');
Route::get('/robots.txt', 'Portal\RobotsController@show')->name('robots');
Route::get('/sitemap.xml', 'Portal\SitemapController@sitemap')->name('sitemap');

Route::post('/csp-report', 'Portal\CspReportController@store')
    ->name('csp.report')
    ->middleware('throttle:120,1');
```

Crawlers e PWA continuam funcionando durante manutenção.

### 3.7 `routes/basepro.php` — remoção de Closures

Para habilitar `route:cache`, foram removidas 6 Closures:

- `logout`: removido o 3º arg `function(){ abort(404); }` — que, além de bloquear
  o cache, **impedia o logout**.
- `activate`: removido 3º arg vazio.
- `cms /`: `Route::redirect('/', '/cms/dashboard')`.
- `acidade.getByState`: removido 3º arg vazio.
- `usuario.addRoles` (x2): removidos 3º args vazios.

### 3.8 `ActivationController` / `InformationController`

- `ActivationController::activate`: `env('APP_URL')` → `config('app.url')`.
- `InformationController`: `env('GEMINI_*')` → `config('gemini.*')`.

Isso permite `config:cache` sem `env()` retornar `null`.

---

## 4. Models, traits e providers

### 4.1 `GetInformationsPrefectureTrait` — memoização

```php
/** @var string|null layout ativo memoizado por request */
private static $templateActivedCache = null;
private static $templateActivedResolved = false;

public function getTemplateActived()
{
    if (self::$templateActivedResolved) {
        return self::$templateActivedCache;
    }

    self::$templateActivedResolved = true;

    $templateActived = Configuration::where('key', 'layout')->first()->value;

    if (!View::exists('portal.layouts.' . $templateActived . '.home.index')) {
        DB::table('configurations')->where('key', 'pagina_em_manutencao')->update(['value' => 1]);
        abort(404, 'view_inexistente');
    }

    return self::$templateActivedCache = $templateActived;
}
```

- Antes: ~6 chamadas por request, cada uma com `SELECT` + possível `UPDATE`.
- Agora: 1 leitura por request; o `UPDATE`/`abort` de manutenção é preservado.

### 4.2 Eager loading

- `getContentByCategoryWith`: `Content::with(['linksUnique', 'categoryContents'])`.
- `getContentsByParentCategory`: `->with('categoryContents')`.
- `$sessions` no `PortalController`: eager loading de `categoryContents`.
- `Content::ultimasNoticias`: `with('archives:...', 'categoryContents:id,name,slug')`.
- `NewsTrait::getMainContent`: `with([...])` com colunas específicas.

### 4.3 `NewsTrait::getContentNews` — cache versionado

```php
$page = (int) request()->input('page', 1);
$page = $page > 0 ? $page : 1;

$key = 'news.list.v' . PageCache::version() . '.'
    . md5(serialize([$slugCategory, $limit, $paginate, $page, app()->getLocale()]));

return Cache::remember($key, (int) config('cache.page_cache.ttl', 300), function () use ($slugCategory, $limit, $paginate) {
    $date = date('Y-m-d H:i:s');

    return $this->getMainContent()
        ->byCategoryContentNews($slugCategory)
        ->where("publication_date", "<=", $date)
        ->byOrder('publication_date')
        ->byStatus()
        ->limit($limit)
        ->paginate($paginate);
});
```

- A chave inclui a versão do FPC; `PageCache::flush()` a invalida.
- O MISS mais caro (~3 s em produção) deixa de recomputar a listagem a cada
  expiração do FPC.

### 4.4 `Configuration` model — invalidação

```php
protected static function boot()
{
    parent::boot();

    static::saved(function () {
        Cache::forget('prefecture.config');
    });

    static::deleted(function () {
        Cache::forget('prefecture.config');
    });
}
```

### 4.5 Providers de layout

Cada dataset quase-estático foi envolvido em `Cache::remember` com
`$ttl = config('cache.page_cache.ttl', 300)`:

- `MenuServiceProvider`: `layout.menu_header`, `layout.menu_rapido`,
  `layout.menu_atalhos`, `layout.contact_link`.
- `FooterServiceProvider`: `layout.footer_config`, `layout.footer_menus`,
  `layout.footer_redes`, `layout.system_version`.
- `ConfigHeaderServiceProvider`: `layout.header_bulletins`,
  `layout.header_config`, `layout.header_fornecedor`, `layout.header_acessos`,
  `layout.header_highlight`.
- `BannerServiceProvider`: `layout.banners`, `layout.super_banners`.
- `NewsMorePopularServiceProvider`: `layout.news_more_popular`.

Exemplo:

```php
$configFooter = Cache::remember('layout.footer_config', $ttl, function () {
    return Configuration::whereHas('categoryConfiguration', function ($query) {
        $query->where('slug', 'rodape');
    })->orderBy('key')->get(['key', 'value']);
});
```

### 4.6 `AppServiceProvider` — observers e boot seguro

**Debugbar condicional** (evita fatal com `--no-dev`):

```php
public function register()
{
    if (class_exists(\Barryvdh\Debugbar\ServiceProvider::class)) {
        $this->app->register(\Barryvdh\Debugbar\ServiceProvider::class);
    }
}
```

**Invalidação de FPC/layout:**

```php
foreach (config('cache.page_cache.invalidate_models', []) as $model) {
    if (!class_exists($model)) continue;
    $model::created(function () { PageCache::flush(); });
    $model::deleted(function () { PageCache::flush(); });
}

$layoutModels = [
    \App\Prefecture\MenuItem::class,
    \App\Prefecture\Configuration::class,
    \App\Http\Model\Prefecture\Highlight::class,
    \App\Http\Model\Prefecture\Indicator::class,
    \App\Http\Model\Prefecture\Supplier::class,
    \App\Http\Model\Prefecture\UpdateLogs::class,
];

foreach ($layoutModels as $model) {
    if (!class_exists($model)) continue;
    $model::saved(function () { PageCache::flush(); });
    $model::deleted(function () { PageCache::flush(); });
}

\App\Prefecture\Content::saved(function ($content) {
    $isLayout = $content->categoryContents()
        ->whereIn('slug', PageCache::LAYOUT_CONTENT_CATEGORIES)
        ->exists();
    if ($isLayout) PageCache::flush();
});
```

**Por que `created`/`deleted` e não `updated`?** Evita purgar o cache a cada
`increment('clicks')`/`increment('visualizacoes')`. Edições de conteúdo refletem
em até `ttl`; edições de layout purgam na hora.

---

## 5. Serviços e comandos

### 5.1 `CdnPurgeService`

- `purgeAll()` / `purge(array $urls)`.
- Drivers: `cloudflare`, `fastly`, `webhook`.
- `config('cdn.enabled')` default `false` e `providers` vazio → no-op sem rede.
- Nunca lança para o caller: falha é logada (`cdn.purge.failed`) e absorvida.
- Timeouts curtos (`cdn.timeout`, `cdn.connect_timeout`).
- Credenciais vêm de `config/cdn.php` (nunca literais).

### 5.2 `ClearPageCache`

```php
protected $signature = 'cache:page:clear';
protected $description = 'Limpa o Full-Page Cache (FPC) de origem...';

public function handle()
{
    $count = PageCache::flush();
    $this->info("FPC de origem limpo. {$count} chave(s) removida(s).");
    return 0;
}
```

### 5.3 `RedigestWebpImages`

- `images:redigest-webp {--limit=300}`.
- Processa `Storage::allFiles()` em chunks, com offset persistido em
  `storage/app/public/.webp_redigest_offset`.
- Ao concluir, volta o offset para `0` (processamento contínuo).
- Pula não-imagens e WebP; chama `generateWebpVariants()`.
- Agendado `everyTenMinutes()` em `app/Console/Kernel.php`.

### 5.4 `FileTrait`

- `compress()` chama `generateWebpVariants($src_path)` após gerar as versões
  legadas (`_min`, `_med`, `_larg`).
- `generateWebpVariants()`:
  - idempotente via sidecar `variants/{base}.variants.json`;
  - larguras `[360, 720, 1200, 1920]`;
  - imagens < 360px geram variante única na largura original;
  - SVG e GIF animado são ignorados;
  - `Imagick` + quality 82 + `stripImage()` (remove EXIF);
  - sidecar enriquecido com `w`/`h` para anti-CLS e retrocompatibilidade.
- `stripCore()` usa `method_exists($core, 'stripImage')` para não quebrar em
  drivers sem suporte.

---

## 6. Configuração e bootstrap

### 6.1 Leitura tolerante de `config.json`

`config/app.php`, `config/mail.php` e `config/recaptcha.php` agora fazem:

```php
$json = @file_get_contents(base_path('config.json'));
$data = $json ? json_decode($json) : null;
$app_config = ($data && isset($data->config[2])) ? $data->config[2] : null;
```

E usam fallback:

```php
'env'   => $app_config ? $app_config->app_env : env('APP_ENV', 'production'),
'debug' => $app_config ? $app_config->app_debug : env('APP_DEBUG', false),
```

**Bug corrigido:** antes, `file_get_contents` retornava `false`,
`json_decode(false)` → `null`, e `$data->config[2]` lançava fatal em produção
virgem (sem `config.json`).

### 6.2 Normalização de `APP_ENV`

```php
$appEnv = $app_config ? $app_config->app_env : env('APP_ENV', 'production');
$appEnv = $appEnv === 'prod' ? 'production' : $appEnv;
```

Evita `config('app.env') === 'prod'` quebrando comparações com `production`.

### 6.3 `config/gemini.php`

```php
return [
    'api_url' => env('GEMINI_API_URL'),
    'api_model' => env('GEMINI_API_MODEL', 'gemini-2.5-flash'),
    'api_key' => env('GEMINI_API_KEY'),
];
```

Centraliza `env()` fora de controllers, permitindo `config:cache`.

### 6.4 `config/session.php`

```php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => env('SESSION_SAME_SITE', 'lax'),
```

### 6.5 `config/cache.php`

- `page_cache.enabled`, `ttl` (default 300), `ignored_query_params`,
  `invalidate_models`.
- Lista de tracking params evita fragmentação do FPC/borda.

### 6.6 `config/cdn.php` e `config/csp.php`

- `cdn.enabled=false`, `providers=[]` → purge no-op.
- `csp.enabled=false` → CSP inativa, mas código pronto.

### 6.7 `composer.json`

**Helpers globais em produção:**

```json
"autoload": {
    "psr-4": { "App\\": "app/" },
    "files": [ "app/Helpers/functions.php" ]
}
```

Antes estava em `autoload-dev.files`; com `--no-dev`, `translateMonth()` e outras
funções globais não existiam → fatal. Correção move para `autoload.files`.

**`apcu-autoloader`:**

```json
"config": {
    "apcu-autoloader": true
}
```

Degrada graciosamente se APCu não existir (warning no `dump-autoload`).

**Script `prod`:** ver [`06-deploy-operacao.md`](06-deploy-operacao.md) §3.

---

## 7. Correções de bugs de aplicação

### 7.1 Upload de imagens em Configurações

**Problema:** `Path is outside of the defined root, path:
[../assets/images/prefeitura-logo.png]`. O valor legado do seeder contém `..` e
está fora da raiz do disco `public`; `Storage::disk('public')->exists($fileOld)`
lançava `LogicException` antes de gravar a nova imagem.

**Correção em `updateByCategory`:**

```php
$fileOld = $config->value;
if ($fileOld && strpos($fileOld, '..') === false) {
    try {
        if (Storage::disk('public')->exists($fileOld)) {
            Storage::disk('public')->delete($fileOld);
        }
    } catch (\Throwable $e) {
        logger()->warning('Imagem antiga não pôde ser removida: ' . $e->getMessage(), [
            'key' => $key,
            'fileOld' => $fileOld,
        ]);
    }
}
$config->value = $relative;
```

**Correção em `update($id)`:**

```php
if (!empty($fileOld) && strpos($fileOld, '..') === false) {
    try {
        if (Storage::disk('public')->exists($fileOld)) {
            Storage::disk('public')->delete($fileOld);
        }
    } catch (\Throwable $e) { /* log */ }
}
$configuration['value'] = 'configuracoes/' . $name;
$request->value->storeAs('configuracoes/', $name, 'public');
```

**Validação (`ConfigurationRequest`):**

```php
if ($config->type == 'file') {
    $rules += [
        $keys[$i] => ($config->notnull == true ? 'required|' : 'nullable|')
            . 'image|max:2048'
    ];
    continue;
}
```

**Outros ganhos:**

- SVG é armazenado sem passar pelo Intervention (driver GD não decodifica SVG).
- Imagens raster são redimensionadas e encodadas a 90.
- Log de erro inclui `key`, `mime`, `title`, `trace`.
- `php artisan storage:link` é pré-requisito para exibição.

### 7.2 Debugbar em produção

- Removido de `config/app.php` `providers`.
- Registro condicional em `AppServiceProvider::register()`.
- `composer install --no-dev` não quebra mais o boot.

### 7.3 Helpers globais

- `app/Helpers/functions.php` movido para `autoload.files`.
- `composer dump-autoload` resolve.

### 7.4 Logout

- Removida Closure `abort(404)` do 3º argumento da rota `logout`.
- O logout volta a funcionar e a rota entra no `route:cache`.

### 7.5 reCAPTCHA

- Placeholder literal `reCAPTCHA_site_key` substituído por
  `{{ config('recaptcha.public_key') }}` em `contact.blade.php` e
  `ouvidoria/contact.blade.php`.
- O reCAPTCHA volta a funcionar quando `RECAPTCHA_PUBLIC_KEY` está setada.

### 7.6 Sitemap

- Removido `echo`/gravação em disco no GET.
- Corrigido `strpos($link, '//') == false` para `=== 0`.
- `DOMDocument` em vez de `SimpleXML` para namespaces válidos.
- Filtro de host e escaping de `&`/`<`/`>`.
- Ver [`05-seguranca.md`](05-seguranca.md) e [`04-cache-performance.md`](04-cache-performance.md) §12.6.

---

## 8. Frontend/Blade/JS

- **`csrf-inject.blade.php`:** centraliza a obtenção/injeção do token CSRF em
  `jQuery.ajaxSend`, `window.fetch` e `form[POST]`.
- **`preconnect.blade.php`:** `preconnect`/`dns-prefetch` para gstatic, google,
  vlibras, YouTube, AddToAny, Facebook e queimadas; Speculation Rules.
- **Layouts `modelo1`–`modelo6`:**
  - remoção de `@import`/`<link>` do Google Fonts (Raleway vendorizada);
  - `defer` em libs sem init inline síncrono; jQuery mantido síncrono;
  - remoção de polyfills mortos (`html5shiv`, `respond`, `modernizr`);
  - dedup de Bootstrap;
  - `loading="lazy"` em iframes YouTube;
  - CSS não-crítico não-bloqueante.
- **`rum.js`:** LCP/CLS/INP via `PerformanceObserver` + `sendBeacon`.
- **`cv-auto`:** `content-visibility:auto` em rodapés.
- **`lighthouse/budget.json`:** budget de LCP/CLS/TBT.

---

## 9. Testes adicionados

| Teste | Cobre |
|---|---|
| `tests/Unit/CacheControlTest.php` | cache público, ETag/304, ETag fraco, Vary sem UA, strip de CSRF, HEAD/POST/redirect/erro |
| `tests/Unit/PageCacheTest.php` | HIT/MISS, escopo Portal, POST, csrf, flush, normalização de URL, versão |
| `tests/Unit/LayoutCacheTest.php` | cache dos providers, invalidação por model events, sem thrash em conteúdo comum |
| `tests/Unit/CspNonceTest.php` | header/nonce, injeção, nonce por request, HIT do FPC, CSP desligado |
| `tests/Unit/PerformanceIndexTest.php` | existência dos índices + EXPLAIN sem Seq Scan |
| `tests/Unit/EagerLoadMemoizeTest.php` | memoização + eager loading |
| `tests/Unit/CdnPurgeServiceTest.php` | no-op, provider desabilitado/mal configurado, sem exceção |

> A suíte usa `CountingCacheRepository` para provar que os closures de cache são
> executados uma única vez. `PerformanceIndexTest` usa
> `SET enable_seqscan = off` para ser robusto mesmo com tabelas vazias.

---

## 10. Invariantes de código (não regredir)

1. **PHP 7.2:** nada de `fn()`, typed properties, `match`, `?->`, `??=`, enums,
   union types, constructor promotion. Usar `function(){}`, `?Type`, docblocks.
2. **Laravel 5.5:** sem `view:cache`, `@csrf`, `@error`; usar `csrf_field()`,
   `@if($errors->has(...))`; hash bcrypt.
3. **`env()` só em `config/*`.** Em runtime com `config:cache`, `env()` é `null`.
4. **Nunca Closure como ação/after de rota** — quebra `route:cache` all-or-nothing.
5. **Não reordenar o grupo `web`** sem revalidar FPC/HIT/MISS, ETag/304 e CSP.
6. **`CacheControl`:** ETag fraco + normalização; `Vary` sem `User-Agent`;
   remover cookies via `removeCookie()`.
7. **`PageCache`:** só `Portal\`, GET, 200, `text/html`, sem cookie, não
   autenticado; invalidar via `PageCache::flush()`.
8. **CSRF:** não remover `Set-Cookie` de formulários antes do client-side; manter
   `/csrf-token` fora do cache.
9. **`PictureTransform`:** não editar Blades de módulo; preservar `alt`, adicionar
   `width`/`height`, não usar `role="img"` redundante.
10. **`HtmlMinify`:** preservar blocos sensíveis; conversão `//`→`/* */` é na
    fonte, não no middleware.
11. **Uploads:** ignorar caminhos com `..` no disco `public`; gravar sempre no
    disco `public`; validar `image|max:2048`.
12. **`config.json`:** leitura tolerante com fallback `env()`.
13. **Debugbar:** registro condicional por `class_exists`.
14. **Helpers:** `autoload.files`, não `autoload-dev.files`.

---

## 11. Como validar as mudanças de código

```bash
# Lint dos arquivos alterados
php -l app/Http/Middleware/CacheControl.php
php -l app/Http/Middleware/PageCache.php
php -l app/Http/Middleware/HtmlMinify.php
php -l app/Http/Middleware/PictureTransform.php
php -l app/Http/Middleware/CspNonce.php
php -l app/Http/Traits/FileTrait.php
php -l app/Services/Cdn/CdnPurgeService.php

# Suíte
php vendor/bin/phpunit

# Rotas (após route:cache)
php artisan route:list | grep -E 'csrf|csp|robots|sitemap|logout|activate'

# Config cache
php artisan config:cache
php artisan route:cache

# Comandos
php artisan cache:page:clear
php artisan images:redigest-webp --limit=10

# Headers/cache (GET real)
curl -s -D - -o /dev/null https://SEU_HOST/ \
  | grep -iE 'cache-control|etag|vary|set-cookie|x-page-cache|server-timing'
```

> Antes de validar em produção, reinicie/reload do PHP-FPM: com
> `opcache.validate_timestamps=0`, o código antigo pode continuar em memória.


---

## 12. Achados que afetam este código

A revisão crítica está em [`10-achados-e-patches.md`](10-achados-e-patches.md).
Os principais para esta camada são **F-01** (CSP × ETag × FPC), **F-02**
(`immutable` em uploads), **F-11** (`PageCache::shouldCache` frágil) e **F-13**
(`CacheControl`/`CspNonce` em respostas não-HTML).
