# Regras e Portabilidade — Editor Estrutural + Landing Sections Editor

> Caminhos relativos à raiz `lotep/`. Foco no que não é óbvio: semântica de ordem/ocultação,
> sanitização, confiança no template, resolução de mídia e acoplamentos.

---

## 1. Regras de negócio não óbvias

### 1.1 A home é uma lista, não um conjunto de flags

`homepage_sections` versão 3 é uma **lista ordenada**. Estar na lista = visível; não estar =
oculto. O campo `hide` existe para preservar posição/configuração sem renderizar.

```php
$order[] = $type;

// Seção oculta (flag hide): preserva posição/configuração, não renderiza.
if (!empty($item['hide'])) {
    $hidden[] = $type;
}

if (in_array($type, self::STATIC_TYPES, true) && is_array($item['parameters'] ?? null)) {
    $sections[$type] = $item['parameters'];
}
```

Fonte: `V2/Modules/Support/StructuralLandingEditor/Services/HomepageSectionsService.php:223-233`.

Consequências:

- Remover uma seção da lista **descarta** seus parâmetros no próximo save do editor (o JS remove do
  array). O `hide` é a forma de "desligar sem perder".
- `order` só contém tipos conhecidos; desconhecidos são ignorados silenciosamente.
- `hidden` é derivado de `hide`; não há coluna própria.

### 1.2 Whitelist de renderização

| Conjunto | Conteúdo | Efeito |
|---|---|---|
| `ALL_TYPES` (9) | hero, acesso-rápido, imprensa, papel-regulatório, dashboard, destaques-cidadão, canais-atendimento, agenda, jogo-responsável | layout default |
| `OPTIONAL_TYPES` (1) | `home-jogos-autorizados` | só entra se configurado |
| `COMMUNIQUE_TYPE` | `home-comunicado` | só entra se configurado |
| `HEADER_TYPE` | `home-header` | extraído para `header`, nunca na ordem |
| `DYNAMIC_TYPES` (4) | imprensa, dashboard, agenda, jogos-autorizados | ordem apenas; dados de `build()` |

`knownTypes()` = `ALL_TYPES + OPTIONAL_TYPES` (`:86-89`). O `home-comunicado` é aceito fora de
`knownTypes()` por uma exceção explícita (`:221`).

### 1.3 Defaults vivem em dois lugares

`HomepageSectionsService::sectionDefaults()` espelha os defaults dos partials Blade
(`V2/Modules/Institucional/BasicPage/Views/portal/homepage/partials/hero.blade.php:5-13`;
`acesso-rapido.blade.php:5-18`). A renderização usa `array_merge($defaults, $data)` no partial; o
editor usa o array PHP para pré-preencher.

**Risco:** alterar um partial sem alterar `sectionDefaults()` faz o editor mostrar um valor e o
portal renderizar outro quando o campo não for salvo.

### 1.4 Sanitização não é uniforme

`sanitizeContent()` só chama o sanitize de widgets **registrados**:

```php
foreach ($content as &$section) {
    $type = $section['type'] ?? '';
    $params = $section['parameters'] ?? null;
    if (isset($widgets[$type]) && is_array($params)) {
        $section['parameters'] = $widgets[$type]->sanitize($params);
    }
}

return $content;
```

Fonte: `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:246-259`.

Tipos não registrados passam **intactos** para o banco. Eles não renderizam na home (whitelist do
`parseConfig`), mas permanecem no payload.

Regras por widget:

| Widget | Sanitize | Consequência |
|---|---|---|
| `BaseWidget` (padrão) | `array_walk_recursive` + `strip_tags` | remove **todas** as tags de todas as strings; não é sanitizador de atributos, apenas remove marcação |
| `html-free` | whitelist de 5 chaves; preserva string | HTML armazenado e renderizado em `iframe srcdoc` |
| `home-comunicado` | remove `<script>`, `on*` e `javascript:` por regex | preserva o resto do HTML |
| `bloco-sle` | só `template` | preserva template Blade/PHP |

Fontes: `Widgets/BaseWidget.php:62-72`; `Widgets/HtmlFreeWidget.php:45-58`;
`Widgets/HomeComunicadoWidget.php:22-34`; `Widgets/BlocoSleWidget.php:44-50`.

`strip_tags` **remove a tag e mantém o texto interno**: `<script>alert(1)</script>` vira
`alert(1)`, que aparece como texto. Para os campos de texto isso é ruído, não execução.

### 1.5 Resolução de mídia

`prepareSections()` faz **uma** consulta para todas as seções:

```php
$anexos = \Illuminate\Support\Facades\DB::table('v2_gallery_media')
    ->whereIn('id', array_unique($anexoIds))
    ->get()
    ->keyBy('id');
```

Fonte: `LandingSectionsEditorRendererService.php:339-348`.

Depois:

- `HomeImageWidget` mapeia caminhos com wildcard (`items.*.image_id` → `items.*.image`) e escreve
  `{src, alt}`, com prioridade para o `alt` explícito (`Widgets/HomeImageWidget.php:69-97`).
- O laço de variantes injeta `{campo}_variants` para cada anexo
  (`LandingSectionsEditorRendererService.php:359-367`).
- Widgets sem `imageSpecs` usam `legacyPrepare()`, que resolve `background_image_id`, `image_id`,
  `steps[].image_id`, `items[].image_id` e `cards[].image_id` para `*_url`, `*_variants` e `*_alt`
  (`:384-443`).

A consulta **não filtra `deleted_at`**; uma mídia arquivada continua resolvendo URL.

### 1.6 Área vazia não é área removida

`saveEditor()`:

```php
if (empty($content) && !$record) {
    continue;
}

if (empty($content) && $record) {
    // Se área ficou vazia e já existia, desativa em vez de apagar
    $record->update([
        'content' => '',
        'active' => false,
    ]);
    continue;
}
```

Fonte: `V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:413-423`.

Assim, o layout volta ao fallback governamental, mas a linha e o histórico de auditoria permanecem.

### 1.7 Configuração é chave-valor sem schema

`saveConfig()` faz `firstOrNew(['key' => $key])`, sobrescreve `payload` inteiro e audita
(`LayoutBaseCmsController.php:336-348`). Não há merge parcial: cada aba envia o payload completo.
Os `json_decode` de links/redes/LAI não checam tipo nem profundidade — JSON inválido vira `[]`
(`:190-201`, `:235-246`, `:249-260`).

### 1.8 Preview é cache global por token

`savePreview()` grava `Cache::put('landing_preview_' . $token, ['sections' => $sanitizedSections, 'vars' => $previewVars], 3600)`. O token vem do
**cliente**; o servidor não vincula ao usuário, à sessão nem ao formulário
(`LandingSectionsEditorController.php:133-171`). `renderPreview()` apenas lê a chave
(`:179-211`).

---

## 2. Acoplamentos

| Direção | Dependência | Evidência |
|---|---|---|
| módulo → serviço | `HomepageSectionsService` chama `prepareSections` no `BasicPage`; `LayoutBaseCmsController` usa `sanitizeContent` | `LayoutBaseCmsController.php:310-311` |
| módulo → módulo (SupportConfigs) | `UserConfigsService` | `UserConfigsService.php:9-10` |
| módulo → serviço Gallery/Media/Storage | assets, logos, banners, favicon | `AssetManagerService.php:9-14`; `LayoutBaseCmsController.php:11-13` |
| serviço → módulo (SupportConfigs) | `PortalConfigsService` no preview | `LandingSectionsEditorController.php:16` |
| serviço → módulo (BasicPage) | preview busca `HomepageDataService` | `Views/preview.blade.php:44-50` |
| módulo (BasicPage) → módulo (SLE) | `HomepageDataService` importa `HomepageSectionsService` | `HomepageDataService.php:12` |
| serviço → módulo (SLE) | render `bloco-sle` resolve `BlockRenderer` | `Views/render/bloco-sle.blade.php:38` |
| serviço → Core | `ActivityLoggerService` | `LandingSectionsEditorController.php:18` |

O serviço é, na prática, **uma UI compartilhada** com dependências de dois módulos. O README
reconhece a intenção (`V2/Services/LandingSectionsEditor/README.md:17-34`), mas isso contraria a
fronteira física do V7 (ADR-013).

---

## 3. Armadilhas

| # | Armadilha | Onde | Impacto |
|---|---|---|---|
| 1 | Blade arbitrário compilado e incluído | `BlockRenderer.php:156-203` | RCE se o banco ou o e-mail de suporte for comprometido |
| 2 | `extract($data, EXTR_SKIP)` + `include` no escopo do método | `BlockRenderer.php:186-201` | o template acessa `$this` (BlockRenderer) e qualquer helper/`app()` |
| 3 | `strip_tags` como sanitização | `BaseWidget.php:62-72` | não protege atributos/URLs; destrói rich text legítimo |
| 4 | Regex de sanitização em HTML | `HomeComunicadoWidget.php:24-31` | bypass por `<svg/onload>`, entidades, `<iframe>`, `<style>` |
| 5 | `html-free` sem sanitização | `HtmlFreeWidget.php:45-58` | HTML arbitrário no portal (em `srcdoc`) |
| 6 | `bloco-sle` sem sanitização | `BlocoSleWidget.php:44-50` | template executável persistido |
| 7 | Gate por e-mail fixo | `SleCmsController.php:35-40` | um único login (env) concentra todo o poder |
| 8 | Serviço sem RBAC | `V2/Services/LandingSectionsEditor/Routes/cms.php:9` | qualquer usuário do CMS usa form/preview/mídia |
| 9 | Controller chamando controller | `SleCmsController.php:84`, `:148-152`, `:202`, `:276` | efeitos colaterais fora do ciclo de middleware; difícil testar |
| 10 | Auditoria duplicada e divergente na home | `SleCmsController.php:282-286` + `LayoutBaseCmsController.php:343-347` | `new` da tela é a string crua; a do config é o payload sanitizado |
| 11 | `json_decode` silencioso | `LayoutBaseCmsController.php:190-201` | configuração apagada sem erro |
| 12 | `LayoutBaseComposer` com `static` | `ViewComposers/LayoutBaseComposer.php:16-26` | em runtime de longa duração (Octane) o cache sobrevive entre requests e não vê alteração até reiniciar |
| 13 | `BlockRenderer` cacheia `$vars` e `$contentsCache` por instância singleton | `ModuleProvider.php:16-18`; `BlockRenderer.php:31-45`, `:76-84` | config antiga no mesmo request/worker |
| 14 | 8 queries por render da tela de editor | `SleCmsController.php:379-384` | N+1 fixo (8 áreas) |
| 15 | `prepareSections` chamado duas vezes | `landing-sections-viewer.blade.php:10-13` | trabalho e query repetidos |
| 16 | `getForArea()` sem cache | `StructuralPortalContent.php:66-72` | cada chamada consulta o banco |
| 17 | `v2_gallery_media` consultada direto | `LandingSectionsEditorRendererService.php:342` | ignora `deleted_at`, policies e o serviço Gallery |
| 18 | Duplicação de constantes de ordem | `HomepageSectionsService::ALL_TYPES` e `homepage.blade.php:64-68` | divergent versions silently |
| 19 | Schema de `video` aponta para playlist | `LandingSectionsEditorRendererService.php:74` | validação errada para embed simples |
| 20 | `WidgetRegistry` estático nunca limpa | `WidgetRegistry.php:9` | testes e requests longos acumulam estado |
| 21 | Fallback de tipo por nome de classe | `ValidationService.php:43-51` | `homeHero` vira `HomeHeroSchema`; tipos com hífen dependem do registry |
| 22 | `View::exists()` por tipo controlado pelo cliente | `LandingSectionsEditorController.php:89-92` | enumeração de views e 404 diferenciado |
| 23 | Token de preview controlável pelo cliente | `LandingSectionsEditorController.php:137-146` | colisão/poisoning entre sessões |
| 24 | Cache de preview sem invalidação | `:166-168` | 1 h de conteúdo antigo após salvar |
| 25 | Upload de JS/CSS sem validação de conteúdo | `AssetManagerService.php:26-55` | JS arbitrário servido same-origin |
| 26 | Favicon aceita `application/octet-stream` | `LayoutBaseCmsController.php:151-156` | arquivo não-ICO com extensão `.ico` |
| 27 | `firstOrNew` + `SoftDeletes` + `key` UNIQUE | `LayoutBaseCmsController.php:338`; migration `:26` | após um delete lógico, o próximo save tenta INSERT e viola a UNIQUE |
| 28 | `abort(403)` em action não-JSON | `SleCmsController.php:37` | chamadas AJAX recebem HTML de erro |

---

## 4. Onde as versões divergem

O módulo e o serviço existem **apenas na camada V2**. A V1 tem um teste isolado de permissão de
landing page (`tests/Feature/V1/LandingPageCmsPermissionTest.php`), mas não há um editor estrutural
equivalente no escopo analisado. O mapa de versões está em `08-GAPS-E-DIVIDAS.md` §4.

Dentro da própria V2, o "contrato" da home mudou de uma lista simples (sem versão) para o payload
v3 com `version`/`sections`; o `HomepageSectionsService` rejeita versões diferentes de 3 e volta ao
default (`HomepageSectionsService.php:194-198`).

---

## 5. Portabilidade — o que precisa sobreviver

| Regra | Sobrevive? | Como |
|---|---|---|
| Ordem + visibilidade por lista | sim | JSONB de slots/blocos no layout |
| `hide` sem perder configuração | sim | campo `hidden` no bloco |
| Header como configuração fora da ordem | sim | slot de cabeçalho separado |
| Seções dinâmicas como marcadores | sim | bloco `type=dynamic` com `source` |
| Defaults refletidos no editor | sim | defaults vêm do schema do bloco, não do Blade |
| Sanitização por tipo | não | substituída por validação de schema + render seguro |
| HTML/CSS/JS como dado | **não** | ADR-016/017 |
| Preview por token em cache | não | preview autenticado e efêmero, ligado à sessão |
| Gate por e-mail | não | RBAC `{module}.{ver|editar|aprovar|arquivar}` (ADR-033) |

---

## 6. Checklist de leitura para quem for portar

1. Ler `HomepageSectionsService::parseConfig()` e os testes de contrato.
2. Ler `WidgetContract` e `prepareSections()` — o registry é o ativo mais reutilizável.
3. Ler `LayoutBaseCmsController::handleHomepageUpdate()` para entender o que é persistido.
4. **Não** portar `BlockRenderer`, os sanitizes de HTML nem o editor de código livre.
5. Tratar `home-comunicado` e `html-free` como casos de conteúdo estruturado (TipTap/AST).
6. Preservar a semântica "sem config = default" e "remover = ocultar".
