# Correção: upload de imagens na aba "Imagens" das Configurações

- **Branch:** grupo de correções `infra-update` (primeiro registro)
- **Stack:** Laravel 5.5.50 / PHP 7.2.34 (cPanel/AlmaLinux/Apache+php-fpm)
- **Arquivos alterados:**
  - `app/Http/Controllers/Prefecture/ConfigurationController.php`
  - `app/Http/Requests/ConfigurationRequest.php`
  - `docker/apache/php.ini` (apenas ambiente de dev)

---

## 1. Problema

Na tela `cms/configuracoes/editar/configuracoes`, aba **Imagens**, ao enviar
`logo`, `favicon` ou `logo_rodape` (PNG ou SVG), o sistema exibia o flash:

> Não foi possível processar a imagem "Logo". Verifique o formato do arquivo.

O arquivo **não era gravado** e a imagem antiga era mantida. O erro ocorria mesmo
para um PNG simples e **sem nenhum log** do lado do servidor, dificultando o
diagnóstico.

## 2. Causa real

A mensagem vem do `catch (\Throwable $e)` do bloco de upload de arquivos em
`ConfigurationController::updateByCategory()`. A exceção real (revelada por um
flash de diagnóstico temporário) era:

> `Path is outside of the defined root, path: [../assets/images/prefeitura-logo.png]`

Fluxo exato:

1. No primeiro envio, `$config->value` ainda contém o **default legado do seeder**
   (`ConfigurationsTableSeeder.php` → `'../assets/images/prefeitura-logo.png'`).
2. O bloco de remoção do arquivo antigo chamava
   `Storage::disk('public')->exists($fileOld)` / `delete($fileOld)` sobre esse valor.
3. Como o valor contém `..` e está **fora da raiz do disco `public`**
   (`storage/app/public`), o Flysystem lança `LogicException`
   *"Path is outside of the defined root"* **antes** de a imagem ser finalizada.
4. O processamento da imagem em si (`Image::make` → `resize` → `encode` → `put`)
   funcionava normalmente; a quebra era inteiramente na remoção do arquivo antigo.

Por que "sem logs": o `catch` antigo (versão em cache do opcache) **ainda não
tinha o `logger()->error()`** que foi adicionado num refinamento posterior.
O `php.ini` de dev tinha `opcache.validate_timestamps = 0`, então o php-fpm
servia um código obsoleto em memória e o logger nunca rodava.

> Nota: o código original (`HEAD`) tinha um guard `str_contains($fileOld,'configuracoes')`
> que evitava esse caso; a reescrita do bloco havia removido o guard e passado a
> chamar `exists()` direto, reintroduzindo o crash.

## 3. Correção real

### 3.1 Remoção segura do arquivo antigo (`updateByCategory`)
A remoção do arquivo anterior agora (a) ignora caminhos com `..` (defaults
legados, fora da raiz) e (b) está protegida por `try/catch`, apagando apenas o
que de fato existe no disco `public`:

```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;
```

### 3.2 Mesmo bug em `update()` por id (edição individual)
O método `update($id)` tinha o crash idêntico **e** gravava no disco errado
(`storeAs('configuracoes/', $name)` sem `'public'` → `storage/app/`, onde a
imagem não era exibida). Corrigido para disco `public` + remoção segura:

```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');
```

### 3.3 Validação de upload (`ConfigurationRequest`)
Campos do tipo `file` receberam regra declarativa `image|max:2048`, evitando que
a regra `max:255` (string) fosse aplicada a uploads e limitando o tamanho.

### 3.4 Ambiente de dev (opcache)
`docker/apache/php.ini`: `opcache.validate_timestamps = 0` → `1`, para que
edições passem a valer sem limpar opcache manualmente. Em produção mantém-se `0`
(com limpeza de opcache no deploy).

## 4. Retrocompatibilidade

- Campo **já com imagem enviada** (`configuracoes/<id>.<ext>`): o arquivo antigo é
  apagado e o novo gravado. (Bônus: corrige o bug anterior em que o arquivo antigo
  virava órfão por estar no disco errado.)
- **Defaults legados** (`../assets/...` ou `assets/images/...`): ignorados com
  segurança, sem crash e sem tocar fora da raiz.
- Nenhuma referência quebra: os 6 modelos do portal
  (`ConfigHeaderServiceProvider` / `FooterServiceProvider`) relêm `$config->value`
  a cada request e injetam nas views `nav.blade.php`, `head.blade.php`,
  `layout.blade.php`.

Testado por reprodução no container: PNG + SVG + defaults legados processam sem
crash e gravam em `storage/app/public/configuracoes/`.

## 5. Pré-requisito de exibição (storage:link)

As views renderizam a imagem via `url('storage/...')`, que depende do symlink
`public/storage` → `storage/app/public`. Sem ele, a imagem é gravada mas **não
aparece** no CMS nem no portal:

```bash
php artisan storage:link
```

## 6. Notas de deploy

- Commit dos arquivos alterados (`ConfigurationController.php`,
  `ConfigurationRequest.php`, `docker/apache/php.ini`).
- No cPanel, após subir: limpar opcache (reiniciar php-fpm/Apache ou
  `php artisan optimize:clear`) para não servir código obsoleto.
- Garantir que `php artisan storage:link` foi executado no alvo.
