Update WhatsAppService.php

This commit is contained in:
Lizandro Guarnizo
2026-02-21 11:39:25 -05:00
parent 0aa450be67
commit e00c96c614
+131 -53
View File
@@ -422,7 +422,7 @@ class WhatsAppService
/**
* Subir archivo multimedia a WhatsApp
* Intenta primero con PHP curl. Si falla por SSL/conexión (errno 35, 56, 77),
* reintenta con distintas opciones SSL y usa proc_open como fallback final.
* reintenta con distintas opciones SSL y usa stream_context/proc_open como fallback.
*/
public function uploadMedia($filePath, $mimeType)
{
@@ -431,62 +431,63 @@ class WhatsAppService
}
$url = $this->apiUrl . $this->phoneNumberId . '/media';
$errors = [];
// ── Intento 1: PHP curl TLS 1.2 + sin ALPN (fix Meta/Facebook errno 56) ───
$attempt1 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
// ── Intento 1: PHP curl TLS 1.2 + sin ALPN + IPv4 ──────────────────────
$a1 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_ENABLE_ALPN => false,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
]);
if ($attempt1['ok']) return $attempt1['result'];
if ($a1['ok']) return $a1['result'];
$errors[] = "curl1: #{$a1['errno']}({$a1['error']}) http={$a1['http']}";
error_log("WhatsAppService::uploadMedia a1 fail: {$errors[0]}");
error_log("WhatsAppService::uploadMedia attempt1 fail: errno={$attempt1['errno']} err={$attempt1['error']} http={$attempt1['http']}");
// ── Intento 2: PHP curl TLS 1.2 sin restricción de IP (por si IPv4 falla) ──
$attempt2 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_ENABLE_ALPN => false,
// ── Intento 2: PHP curl SSL deshabilitado + IPv4 (bypassa verificación) ──
$a2 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
]);
if ($attempt2['ok']) return $attempt2['result'];
if ($a2['ok']) return $a2['result'];
$errors[] = "curl2: #{$a2['errno']}({$a2['error']}) http={$a2['http']}";
error_log("WhatsAppService::uploadMedia a2 fail: {$errors[1]}");
error_log("WhatsAppService::uploadMedia attempt2 fail: errno={$attempt2['errno']} err={$attempt2['error']} http={$attempt2['http']}");
// ── Intento 3: PHP stream_context (evita libcurl/OpenSSL completamente) ──
// Usa el wrapper SSL de PHP (diferente al de libcurl), sin ALPN issues
try {
$result = $this->_streamContextUpload($url, $filePath, $mimeType);
if ($result) return $result;
} catch (Exception $e) {
$errors[] = "stream: " . $e->getMessage();
error_log("WhatsAppService::uploadMedia stream fail: " . $e->getMessage());
}
// ── Intento 3: PHP curl sin restricciones SSL (TLS negociado automático) ───
$attempt3 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
if ($attempt3['ok']) return $attempt3['result'];
error_log("WhatsAppService::uploadMedia attempt3 fail: errno={$attempt3['errno']} err={$attempt3['error']} http={$attempt3['http']}");
// ── Intento 4: proc_open con curl CLI (hereda entorno completo de red) ─────
// proc_open es más confiable que exec() para mantener env de red en PHP-FPM
// ── Intento 4: proc_open + curl CLI con --ipv4 ──────────────────────────
if (function_exists('proc_open')) {
try {
$result = $this->_procOpenUpload($url, $filePath, $mimeType);
if ($result) return $result;
} catch (Exception $e) {
$errors[] = "proc_open: " . $e->getMessage();
error_log("WhatsAppService::uploadMedia proc_open fail: " . $e->getMessage());
}
}
// Ningún método funcionó — lanzar error con contexto
throw new Exception(
"Error al subir archivo a WhatsApp. " .
"Intento1: curl#{$attempt1['errno']}({$attempt1['error']}). " .
"Intento2: curl#{$attempt2['errno']}({$attempt2['error']}). " .
"Intento3: curl#{$attempt3['errno']}({$attempt3['error']}). " .
"Solución: reconstruir contenedor con 'docker compose up --build'."
"Error al subir archivo a WhatsApp tras 4 intentos: " . implode(' | ', $errors) .
". Solución permanente: docker compose up --build (aplica parche OpenSSL en contenedor)."
);
}
/**
* Helper interno: un intento de upload con PHP curl con opciones extras.
* Helper interno: upload con PHP curl + opciones extra.
* IMPORTANTE: $extraOpts siempre sobrescriben los base. IPv4 se fuerza en base.
*/
private function _curlUploadAttempt($url, $filePath, $mimeType, array $extraOpts = [])
{
$ch = curl_init();
// Base siempre incluye IPRESOLVE_V4; extraOpts puede sobreescribirlo si es necesario
$base = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
@@ -500,13 +501,14 @@ class WhatsAppService
CURLOPT_TIMEOUT => 300,
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, // siempre IPv4 salvo que extraOpts lo cambie
];
curl_setopt_array($ch, $base + $extraOpts);
$response = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$errno = curl_errno($ch);
$response = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0 || ($http !== 200 && $http !== 201)) {
@@ -514,32 +516,108 @@ class WhatsAppService
}
$result = json_decode($response, true);
if (!isset($result['id'])) {
return ['ok' => false, 'errno' => 0, 'error' => 'No ID in response: ' . substr($response, 0, 200), 'http' => $http];
return ['ok' => false, 'errno' => 0, 'error' => 'No ID: ' . substr($response, 0, 200), 'http' => $http];
}
return ['ok' => true, 'result' => $result];
}
/**
* Helper interno: upload usando proc_open + curl CLI.
* proc_open hereda el entorno de red completo, más confiable que exec() en PHP-FPM.
* Upload usando PHP stream_context (evita libcurl y su OpenSSL completamente).
* Construye manualmente el body multipart/form-data.
*/
private function _streamContextUpload($url, $filePath, $mimeType)
{
$boundary = '----WA' . bin2hex(random_bytes(16));
$fileContent = file_get_contents($filePath);
if ($fileContent === false) {
throw new Exception("No se pudo leer el archivo: $filePath");
}
$filename = basename($filePath);
// Construir multipart body manualmente
$body = "--{$boundary}\r\n";
$body .= "Content-Disposition: form-data; name=\"messaging_product\"\r\n\r\n";
$body .= "whatsapp\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Disposition: form-data; name=\"type\"\r\n\r\n";
$body .= "{$mimeType}\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Disposition: form-data; name=\"file\"; filename=\"{$filename}\"\r\n";
$body .= "Content-Type: {$mimeType}\r\n\r\n";
$body .= $fileContent . "\r\n";
$body .= "--{$boundary}--\r\n";
$opts = [
'http' => [
'method' => 'POST',
'header' => implode("\r\n", [
'Authorization: Bearer ' . $this->token,
'Content-Type: multipart/form-data; boundary=' . $boundary,
'Content-Length: ' . strlen($body),
]),
'content' => $body,
'timeout' => 300,
'ignore_errors' => true,
'protocol_version' => '1.1',
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
],
];
$ctx = stream_context_create($opts);
$response = @file_get_contents($url, false, $ctx);
// Obtener HTTP code de los response headers
$httpCode = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $h) {
if (preg_match('#^HTTP/\S+ (\d+)#', $h, $m)) {
$httpCode = intval($m[1]);
}
}
}
error_log("WhatsAppService::_streamContextUpload http={$httpCode} response=" . substr((string)$response, 0, 300));
if ($response === false || ($httpCode !== 200 && $httpCode !== 201)) {
$err = error_get_last();
throw new Exception("stream_context HTTP {$httpCode}: " . ($err['message'] ?? 'sin respuesta'));
}
$result = json_decode($response, true);
if (!isset($result['id'])) {
throw new Exception("stream_context sin ID: " . substr($response, 0, 300));
}
return $result;
}
/**
* Upload usando proc_open + curl CLI con --ipv4 explícito.
* proc_open hereda el entorno de red completo del proceso PHP-FPM.
*/
private function _procOpenUpload($url, $filePath, $mimeType)
{
// Buscar curl binary
// Localizar curl binary
$curlBin = '/usr/bin/curl';
if (!file_exists($curlBin)) {
$curlBin = trim(shell_exec('which curl 2>/dev/null') ?: '');
foreach (['/usr/local/bin/curl', '/bin/curl'] as $c) {
if (file_exists($c)) { $curlBin = $c; break; }
}
}
if (empty($curlBin) || !file_exists($curlBin)) {
if (!file_exists($curlBin)) {
throw new Exception("curl CLI no encontrado");
}
$tmpResponse = tempnam(sys_get_temp_dir(), 'wa_up_');
// Construir argumentos sin shell_exec (array, evita problemas de escape)
$args = [
$curlBin,
'--silent', '--show-error',
'--ipv4', // forzar IPv4 (evita errno 7 por IPv6 no ruteado)
'--tlsv1.2',
'--max-time', '300',
'-o', $tmpResponse,
@@ -552,22 +630,22 @@ class WhatsAppService
$url,
];
// Construir comando como string para proc_open
$cmd = implode(' ', array_map('escapeshellarg', $args));
$descriptors = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
// Heredar entorno completo del proceso actual (clave para que funcione DNS en PHP-FPM)
$env = array_merge($_ENV ?: [], ['HOME' => '/tmp', 'PATH' => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin']);
$env = array_merge(
$_ENV ?: [],
['HOME' => '/tmp', 'PATH' => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin']
);
$proc = proc_open($cmd, $descriptors, $pipes, null, $env);
if (!is_resource($proc)) {
@unlink($tmpResponse);
throw new Exception("proc_open falló");
throw new Exception("proc_open no pudo iniciar el proceso");
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proc);
@@ -576,17 +654,17 @@ class WhatsAppService
$body = @file_get_contents($tmpResponse);
@unlink($tmpResponse);
error_log("WhatsAppService::_procOpenUpload exit={$exitCode} http={$httpCode} stderr=" . substr($stderr, 0, 200));
error_log("WhatsAppService::_procOpenUpload exit={$exitCode} http={$httpCode} stderr=" . substr($stderr, 0, 300));
if ($exitCode !== 0) {
throw new Exception("curl CLI exit {$exitCode}: " . trim($stderr));
}
if ($httpCode !== 200 && $httpCode !== 201) {
throw new Exception("HTTP {$httpCode}: " . substr($body, 0, 300));
throw new Exception("CLI HTTP {$httpCode}: " . substr((string)$body, 0, 300));
}
$result = json_decode($body, true);
if (!isset($result['id'])) {
throw new Exception("No ID en respuesta CLI: " . substr($body, 0, 300));
throw new Exception("CLI sin ID: " . substr((string)$body, 0, 300));
}
return $result;
}