# helpers/ — Utilidades centralizadas

Estos archivos se crearon en FASE 1 de la auditoría global (2026-06-02) para resolver patrones críticos repetidos en toda la plataforma.

## helpers/http.php

Cliente HTTP centralizado con:
- ✅ SSL verify ON por defecto (`CURLOPT_SSL_VERIFYPEER => true`)
- ✅ Hostname validation (`CURLOPT_SSL_VERIFYHOST => 2`)
- ✅ Sin redirect hijack (`CURLOPT_FOLLOWLOCATION => false` por defecto)
- ✅ Retry exponencial con jitter para 5xx, 429, 529, timeouts
- ✅ Error code mapping: `network`, `auth_failed`, `rate_limit`, `server_overload`, `not_found`, `http_4xx/5xx`
- ✅ Respuesta uniforme `{ok, status, data, error, error_code, raw}`

### Uso básico

```php
require_once __DIR__ . '/../helpers/http.php';

// GET con query + headers
$r = dm_http_get('https://graph.facebook.com/v19.0/me', 
    ['access_token' => $token, 'fields' => 'id,name']);

if ($r['ok']) {
    $name = $r['data']['name'] ?? '';
} else if ($r['error_code'] === 'auth_failed') {
    // Token Meta inválido o expirado
    return ['error' => 'Token de Meta expirado. Reconecta.'];
} else if ($r['error_code'] === 'rate_limit') {
    return ['error' => 'Meta rate limit (429). Reintenta en 1 min.'];
} else {
    return ['error' => $r['error']]; // Mensaje específico
}

// POST JSON
$r = dm_http_post('https://api.anthropic.com/v1/messages', [
    'model' => 'claude-sonnet-4-6',
    'max_tokens' => 1000,
    'messages' => [['role'=>'user', 'content'=>'Hola']]
], [
    'x-api-key: ' . CLAUDE_API_KEY,
    'anthropic-version: 2023-06-01',
]);

// POST form-encoded
$r = dm_http_post_form('https://oauth2.googleapis.com/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => $rt,
    'client_id' => $cid,
    'client_secret' => $secret,
]);
```

### Opciones avanzadas

```php
$r = dm_http_request('POST', $url, [
    'body'    => $payload,
    'headers' => $headers,
    'timeout' => 60,           // default 25
    'retries' => 3,            // default 2
    'follow'  => true,         // permitir redirects (default false)
    'cacert'  => '/path/cacert.pem',  // CA bundle custom
    'debug'   => true,         // incluir 'raw' en respuesta (solo dev)
]);
```

## helpers/json.php

Decodificación JSON segura. Resuelve el patrón global de
`json_decode($raw, true)` accedido como array sin validar.

### Uso

```php
require_once __DIR__ . '/../helpers/json.php';

// Decodificar con logging si falla
$data = dm_json_decode($raw, ['context' => 'meta-action.duplicate']);
if ($data === null) {
    return ['error' => 'Respuesta inválida de Meta'];
}

// Con fallback obligatorio
$data = dm_json_decode_or($raw, [], ['context' => 'google-data.campaigns']);

// Acceso seguro a path anidado
$text = dm_json_path($data, 'content.0.text', '');
$msg  = dm_json_path($data, 'error.message', 'Sin detalle');
$code = dm_json_path($data, 'error.code', 0);

// Extraer primer JSON dentro de respuesta Claude (con markdown alrededor)
$ai_response = dm_json_extract_first($txt, ['context' => 'meta-data.ai_recs']);
if ($ai_response) {
    $recs = $ai_response['recommendations'] ?? [];
}

// Encode seguro (siempre devuelve string)
echo dm_json_encode(['ok' => true, 'data' => $data]);
```

Los errores se loguean automáticamente en `/logs/json_errors.log`
con contexto para debugging post-mortem.

---

## Plan de refactor — FASE 2

Reemplazar el patrón inseguro en estos archivos (ver `AUDITORIA_*.md` para línea exacta):

### Bloque Meta
- `api/meta-data.php:337-345` — `meta_api_call()` → reemplazar con `dm_http_get()`
- `api/meta-action.php:25-26` (y todos los curl manuales) → `dm_http_post()`
- `api/meta-data.php:303-308` — Claude call con SSL off → `dm_http_post()`
- Líneas `310-313, 355` — `json_decode` raw → `dm_json_decode` + `dm_json_path('content.0.text')`

### Bloque Google
- `api/google-auth.php:25` — token request → `dm_http_post_form()`
- `api/google-ads.php` — 23 instancias `CURLOPT_SSL_VERIFYPEER=>false` → `dm_http_*()`
- `api/google-ads-create.php` — todos los curl → `dm_http_*()`

### Bloque SEO
- `api/gsc-data.php:117-122` — GSC POST → `dm_http_post()`
- `api/pagespeed.php` — PageSpeed call → `dm_http_get()`
- `api/seo-article.php:45-67` — OAuth refresh → `dm_http_post_form()` (con retry built-in)
- `api/wp-publish.php:67-85` — WP REST → `dm_http_post()` (con retry built-in)

### Bloque otros
- `api/shopify-data.php` — todas las calls → `dm_http_get()`
- `api/uptime-check.php:73`, `api/ssl-check.php:46-47` → `dm_http_get()`
- `api/competitive_analysis.php:25`, `api/competitive-cron.php:114` → `dm_http_post()`
- `api/reports.php:34` (y todas las llamadas) → `dm_http_*()`
- `api/videos-analyze.php`, `api/images-analyze.php` → Vision via `dm_http_post()`

### Patrón antes/después

**ANTES (inseguro):**
```php
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 25,
    CURLOPT_SSL_VERIFYPEER => false,  // ❌ MITM
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => $headers,
    CURLOPT_POSTFIELDS     => json_encode($body),
]);
$raw  = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);  // ❌ sin validar
if ($code !== 200) {
    return ['error' => 'HTTP ' . $code, 'raw' => $data];  // ❌ expone interno
}
$text = $data['content'][0]['text'];  // ❌ null access posible
```

**DESPUÉS (seguro):**
```php
require_once __DIR__ . '/../helpers/http.php';
require_once __DIR__ . '/../helpers/json.php';

$r = dm_http_post($url, $body, $headers, ['timeout' => 25, 'retries' => 2]);
if (!$r['ok']) {
    // Mensaje específico según el código
    return ['error' => match($r['error_code']) {
        'auth_failed'     => 'Credenciales inválidas o expiradas',
        'rate_limit'      => 'Rate limit alcanzado, reintenta en 1 min',
        'server_overload' => 'API saturada, reintenta en 30 s',
        default           => $r['error'],
    }];
}
$text = dm_json_path($r['data'], 'content.0.text', '');
if ($text === '') {
    return ['error' => 'Respuesta vacía de Claude'];
}
```

## Impacto esperado FASE 2 (helpers + refactor)
- ✅ Cierra ~30 vulnerabilidades MITM
- ✅ Cierra ~40 bugs de null access en JSON
- ✅ Estandariza mensajes de error en toda la plataforma
- ✅ Retry automático en errores transitorios (mejor UX)
- ✅ Eliminado el riesgo de exposición de errores internos al frontend
