Update WhatsAppService.php
This commit is contained in:
+131
-53
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user