Changeset 3460823
- Timestamp:
- 02/13/2026 02:05:37 PM (7 weeks ago)
- Location:
- holnix/trunk
- Files:
-
- 2 added
- 6 edited
-
admin/class-holnix-admin.php (modified) (23 diffs)
-
admin/css/holnix.css (modified) (1 diff)
-
admin/import.php (modified) (6 diffs)
-
admin/js/holnix-selection.js (modified) (15 diffs)
-
holnix.php (modified) (6 diffs)
-
readme.txt (modified) (3 diffs)
-
tests (added)
-
tests/holnix-cli-runner.php (added)
Legend:
- Unmodified
- Added
- Removed
-
holnix/trunk/admin/class-holnix-admin.php
r3421769 r3460823 62 62 63 63 // --- CONFIGURACIÓN DE API --- 64 $this->api_url = 'https://api.holnix.com/api';64 $this->api_url = $this->get_api_base_url(); 65 65 $this->api_headers = [ 66 66 'headers' => [ … … 68 68 'Accept' => 'application/json', 69 69 'Content-Type' => 'application/json', 70 'X-Holnix-Source' => 'wordpress', 70 71 ], 72 ]; 73 74 $this->holnix_log('Inicialización de cliente API', [ 75 'api_url' => $this->api_url, 76 'token_present' => !empty($this->bearer), 77 'token_length' => strlen((string) $this->bearer), 78 'source' => 'wordpress', 79 ]); 80 } 81 82 /** 83 * Construye los argumentos para requests a Holnix. 84 * 85 * @param array $extra_args Argumentos extra para wp_remote_*. 86 * @return array 87 */ 88 public function get_api_request_args($extra_args = []) 89 { 90 $args = [ 91 'timeout' => 45, 92 'redirection' => 3, 93 'httpversion' => '1.1', 94 'headers' => [], 95 ]; 96 $args = array_replace_recursive($args, $this->api_headers); 97 98 if (!empty($extra_args)) { 99 $args = array_replace_recursive($args, $extra_args); 100 } 101 102 return $args; 103 } 104 105 /** 106 * Obtiene la URL base de la API. 107 * 108 * @return string 109 */ 110 private function get_api_base_url() 111 { 112 $default_url = 'https://api.holnix.com'; 113 $configured_url = defined('HOLNIX_API_BASE_URL') ? HOLNIX_API_BASE_URL : $default_url; 114 115 if (!is_string($configured_url)) { 116 return $default_url; 117 } 118 119 $configured_url = trim($configured_url); 120 if ($configured_url === '') { 121 return $default_url; 122 } 123 124 return untrailingslashit($configured_url); 125 } 126 127 /** 128 * Indica si el logging de diagnóstico está habilitado. 129 * 130 * @return bool 131 */ 132 private function is_holnix_logging_enabled() 133 { 134 if (defined('HOLNIX_DEBUG_LOG')) { 135 return (bool) HOLNIX_DEBUG_LOG; 136 } 137 138 if (function_exists('wp_get_environment_type') && wp_get_environment_type() === 'production') { 139 return false; 140 } 141 142 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 143 return true; 144 } 145 146 if (defined('WP_DEBUG') && WP_DEBUG) { 147 return true; 148 } 149 150 // Por defecto, evitar ruido de logs en instalaciones productivas. 151 return false; 152 } 153 154 /** 155 * Obtiene un extracto seguro para logging. 156 * 157 * @param mixed $value Valor a resumir. 158 * @param int $max_length Longitud máxima. 159 * @return string 160 */ 161 private function get_log_excerpt($value, $max_length = 1200) 162 { 163 if (!is_string($value)) { 164 if (is_scalar($value)) { 165 $value = (string) $value; 166 } else { 167 $value = wp_json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 168 } 169 } 170 171 if (!is_string($value)) { 172 return ''; 173 } 174 175 if (strlen($value) > $max_length) { 176 return substr($value, 0, $max_length) . '...'; 177 } 178 179 return $value; 180 } 181 182 /** 183 * Registra mensajes de diagnóstico del plugin en debug.log. 184 * 185 * @param string $message Mensaje principal. 186 * @param array $context Contexto adicional serializable. 187 * @return void 188 */ 189 private function holnix_log($message, $context = []) 190 { 191 if (!$this->is_holnix_logging_enabled()) { 192 return; 193 } 194 195 $line = '[Holnix] ' . $message; 196 if (!empty($context)) { 197 $json = wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 198 if (is_string($json) && $json !== '') { 199 $line .= ' | ' . $json; 200 } 201 } 202 203 error_log($line); 204 } 205 206 /** 207 * Resume una respuesta HTTP de WordPress para logging. 208 * 209 * @param array|WP_Error $response Respuesta de wp_remote_*. 210 * @return array 211 */ 212 private function get_response_summary($response) 213 { 214 if (is_wp_error($response)) { 215 $error_codes = $response->get_error_codes(); 216 $error_data = []; 217 foreach ($error_codes as $error_code) { 218 $error_data[$error_code] = $response->get_error_data($error_code); 219 } 220 221 return [ 222 'wp_error_codes' => $error_codes, 223 'wp_error' => $response->get_error_message(), 224 'wp_error_data' => $error_data, 225 ]; 226 } 227 228 $body = wp_remote_retrieve_body($response); 229 $body = $this->get_log_excerpt($body); 230 231 return [ 232 'status_code' => wp_remote_retrieve_response_code($response), 233 'body' => $body, 234 ]; 235 } 236 237 /** 238 * Obtiene un posible fallback de URL alternando el prefijo /api. 239 * 240 * @param string $api_url URL original. 241 * @return string 242 */ 243 private function get_fallback_api_url($api_url) 244 { 245 if (!is_string($api_url) || $api_url === '') { 246 return ''; 247 } 248 249 if (strpos($api_url, '/api/') !== false) { 250 return preg_replace('#/api/#', '/', $api_url, 1); 251 } 252 253 $parts = wp_parse_url($api_url); 254 if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { 255 return ''; 256 } 257 258 $path = isset($parts['path']) ? $parts['path'] : '/'; 259 if (strpos($path, '/api/') === 0 || $path === '/api') { 260 return ''; 261 } 262 263 $fallback = $parts['scheme'] . '://' . $parts['host']; 264 if (!empty($parts['port'])) { 265 $fallback .= ':' . $parts['port']; 266 } 267 268 $fallback_path = '/api' . (strpos($path, '/') === 0 ? $path : '/' . $path); 269 $fallback .= $fallback_path; 270 271 if (!empty($parts['query'])) { 272 $fallback .= '?' . $parts['query']; 273 } 274 275 return $fallback; 276 } 277 278 /** 279 * Obtiene el path normalizado de una URL de API. 280 * 281 * @param string $api_url URL completa del endpoint. 282 * @return string 283 */ 284 private function get_normalized_api_path($api_url) 285 { 286 if (!is_string($api_url) || $api_url === '') { 287 return ''; 288 } 289 290 $parts = wp_parse_url($api_url); 291 $path = is_array($parts) && isset($parts['path']) ? (string) $parts['path'] : ''; 292 if ($path === '') { 293 return ''; 294 } 295 296 $path = '/' . ltrim($path, '/'); 297 $path = strtolower($path); 298 $path = preg_replace('#^/api(?:/|$)#', '/', $path); 299 if (!is_string($path) || $path === '') { 300 return ''; 301 } 302 303 return '/' . trim($path, '/'); 304 } 305 306 /** 307 * Resuelve el event_type para logging del backend. 308 * 309 * @param string $method Método HTTP. 310 * @param string $api_url URL de request. 311 * @param array $context Contexto opcional (permite override con event_type). 312 * @return string 313 */ 314 private function get_request_event_type($method, $api_url, $context = []) 315 { 316 if (!empty($context['event_type']) && is_string($context['event_type'])) { 317 $custom_event = sanitize_key($context['event_type']); 318 if ($custom_event !== '') { 319 return $custom_event; 320 } 321 } 322 323 $normalized_path = $this->get_normalized_api_path($api_url); 324 if ($normalized_path === '') { 325 return ''; 326 } 327 328 $normalized_method = strtoupper((string) $method); 329 330 if (preg_match('#^/news(?:-v2)?(?:/|$)#', $normalized_path)) { 331 return 'news_list'; 332 } 333 334 if (preg_match('#^/imprint(?:-v2)?(?:/|$)#', $normalized_path)) { 335 return 'imprint_list'; 336 } 337 338 if ($normalized_method === 'POST' && preg_match('#^/search-isbns(?:/|$)#', $normalized_path)) { 339 return 'bulk_query'; 340 } 341 342 if ($normalized_method === 'GET' && preg_match('#^/isbn/[^/]+$#', $normalized_path)) { 343 return 'detail_view'; 344 } 345 346 if ($normalized_method === 'GET' && preg_match('#^/isbns(?:/|$)#', $normalized_path)) { 347 return 'detail_view'; 348 } 349 350 if ($normalized_method === 'GET' && preg_match('#^/isbns-v2/([^/]+)$#', $normalized_path, $matches)) { 351 return 'detail_view'; 352 } 353 354 return ''; 355 } 356 357 /** 358 * Detecta errores de conexión candidatos a reintento. 359 * 360 * @param array|WP_Error $response Respuesta HTTP. 361 * @return bool 362 */ 363 private function is_retryable_connection_error($response) 364 { 365 if (!is_wp_error($response)) { 366 return false; 367 } 368 369 $message = strtolower($response->get_error_message()); 370 $needles = [ 371 'connection reset by peer', 372 'cURL error 35', 373 'ssl_connect', 374 'timed out', 375 'couldn\'t connect', 376 'could not resolve host', 377 ]; 378 379 foreach ($needles as $needle) { 380 if (strpos($message, strtolower($needle)) !== false) { 381 return true; 382 } 383 } 384 385 return false; 386 } 387 388 /** 389 * Resume información útil de runtime para diagnóstico. 390 * 391 * @return array 392 */ 393 private function get_runtime_summary() 394 { 395 global $wp_version; 396 397 $summary = [ 398 'php_version' => PHP_VERSION, 399 'wp_version' => isset($wp_version) ? $wp_version : '', 400 'openssl' => defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : '', 401 ]; 402 403 if (function_exists('curl_version')) { 404 $curl_data = curl_version(); 405 $summary['curl_version'] = isset($curl_data['version']) ? $curl_data['version'] : ''; 406 $summary['curl_ssl_version'] = isset($curl_data['ssl_version']) ? $curl_data['ssl_version'] : ''; 407 } 408 409 return $summary; 410 } 411 412 /** 413 * Genera diagnóstico básico de DNS/TCP para un host remoto. 414 * 415 * @param string $api_url URL a diagnosticar. 416 * @return array 417 */ 418 private function get_network_summary($api_url) 419 { 420 $parts = wp_parse_url($api_url); 421 $host = is_array($parts) && isset($parts['host']) ? $parts['host'] : ''; 422 $scheme = is_array($parts) && isset($parts['scheme']) ? $parts['scheme'] : 'https'; 423 $port = is_array($parts) && !empty($parts['port']) ? (int) $parts['port'] : (($scheme === 'http') ? 80 : 443); 424 425 if ($host === '') { 426 return ['host' => '', 'port' => $port]; 427 } 428 429 $resolved_ip = gethostbyname($host); 430 $dns_resolved = $resolved_ip && $resolved_ip !== $host; 431 432 $tcp_status = 'not_checked'; 433 $tcp_error = ''; 434 $errno = 0; 435 $errstr = ''; 436 $socket = @fsockopen($host, $port, $errno, $errstr, 3); 437 if ($socket) { 438 $tcp_status = 'ok'; 439 fclose($socket); 440 } else { 441 $tcp_status = 'error'; 442 $tcp_error = $errno . ' ' . $errstr; 443 } 444 445 return [ 446 'host' => $host, 447 'port' => $port, 448 'resolved_ip' => $dns_resolved ? $resolved_ip : '', 449 'dns_resolved' => $dns_resolved, 450 'tcp_status' => $tcp_status, 451 'tcp_error' => $tcp_error, 452 ]; 453 } 454 455 /** 456 * Ejecuta un request contra la API con diagnóstico y fallback opcional /api. 457 * 458 * @param string $method Método HTTP. 459 * @param string $api_url URL objetivo. 460 * @param array $extra_args Argumentos extra del request. 461 * @param array $context Contexto para logging. 462 * @return array 463 */ 464 public function holnix_api_request($method, $api_url, $extra_args = [], $context = []) 465 { 466 $request_args = $this->get_api_request_args($extra_args); 467 $request_method = strtoupper((string) $method); 468 $request_args['method'] = $request_method; 469 470 $event_type = ''; 471 if ( 472 isset($request_args['headers']) && 473 is_array($request_args['headers']) && 474 !empty($request_args['headers']['X-Holnix-Event']) 475 ) { 476 $event_type = sanitize_key((string) $request_args['headers']['X-Holnix-Event']); 477 } else { 478 $event_type = $this->get_request_event_type($request_method, $api_url, $context); 479 } 480 481 if ( 482 !isset($request_args['headers']) || 483 !is_array($request_args['headers']) 484 ) { 485 $request_args['headers'] = []; 486 } 487 488 if ($event_type !== '') { 489 $request_args['headers']['X-Holnix-Event'] = $event_type; 490 } 491 492 $this->holnix_log('Metadata de request API', array_merge($context, [ 493 'method' => $request_method, 494 'api_url' => $api_url, 495 'event_type' => $event_type, 496 'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '', 497 ])); 498 499 $attempt_urls = [$api_url]; 500 $fallback_url = $this->get_fallback_api_url($api_url); 501 if (!empty($fallback_url) && $fallback_url !== $api_url) { 502 $attempt_urls[] = $fallback_url; 503 } 504 505 $last_response = null; 506 $final_url = $api_url; 507 $attempt = 0; 508 509 foreach ($attempt_urls as $attempt_url) { 510 $attempt++; 511 $final_url = $attempt_url; 512 $response = wp_remote_request($attempt_url, $request_args); 513 $last_response = $response; 514 515 if (!is_wp_error($response)) { 516 $status_code = wp_remote_retrieve_response_code($response); 517 if ($status_code === 404 && $attempt === 1 && count($attempt_urls) > 1) { 518 $this->holnix_log('HTTP 404 en request API, probando fallback de base URL', array_merge($context, [ 519 'method' => $request_method, 520 'attempt' => $attempt, 521 'api_url' => $attempt_url, 522 'fallback_url' => $fallback_url, 523 'event_type' => $event_type, 524 'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '', 525 ])); 526 continue; 527 } 528 529 return [ 530 'response' => $response, 531 'final_url' => $final_url, 532 'attempt' => $attempt, 533 'event_type' => $event_type, 534 ]; 535 } 536 537 $this->holnix_log('WP_Error en request API', array_merge($context, [ 538 'method' => $request_method, 539 'attempt' => $attempt, 540 'api_url' => $attempt_url, 541 'event_type' => $event_type, 542 'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '', 543 'response' => $this->get_response_summary($response), 544 'runtime' => $this->get_runtime_summary(), 545 'network' => $this->get_network_summary($attempt_url), 546 ])); 547 548 if (!$this->is_retryable_connection_error($response)) { 549 break; 550 } 551 } 552 553 return [ 554 'response' => $last_response, 555 'final_url' => $final_url, 556 'attempt' => $attempt, 557 'event_type' => $event_type, 71 558 ]; 72 559 } … … 90 577 91 578 $isbns = isset($_POST['isbns']) ? array_map('sanitize_text_field', $_POST['isbns']) : []; 579 $this->holnix_log('Inicio holnix_start_full_import', ['isbn_count' => count($isbns)]); 92 580 93 581 if (empty($isbns)) { 582 $this->holnix_log('holnix_start_full_import sin ISBNs'); 94 583 wp_send_json_error(['message' => 'No se proporcionaron ISBNs.']); 95 584 return; … … 99 588 $isbn_chunks = array_chunk($isbns, 200); 100 589 101 foreach ($isbn_chunks as $chunk ) {590 foreach ($isbn_chunks as $chunk_index => $chunk) { 102 591 $search_url = $this->api_url . '/search-isbns'; 103 $args = [ 104 'headers' => $this->api_headers['headers'], 105 'body' => json_encode(['isbns' => $chunk]), 592 $request = $this->holnix_api_request('POST', $search_url, [ 593 'body' => wp_json_encode(['isbns' => $chunk]), 106 594 'timeout' => 45, 107 ]; 108 $response = wp_remote_post($search_url, $args); 595 ], [ 596 'context' => 'holnix_start_full_import', 597 'chunk_index' => $chunk_index, 598 'chunk_size' => count($chunk), 599 ]); 600 $response = $request['response']; 601 $final_url = $request['final_url']; 602 603 $this->holnix_log('Enviando request search-isbns', [ 604 'url' => $search_url, 605 'final_url' => $final_url, 606 'chunk_index' => $chunk_index, 607 'chunk_size' => count($chunk), 608 'attempt' => $request['attempt'], 609 ]); 109 610 110 611 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { 612 $this->holnix_log('Error en search-isbns', [ 613 'url' => $search_url, 614 'final_url' => $final_url, 615 'chunk_index' => $chunk_index, 616 'chunk_size' => count($chunk), 617 'response' => $this->get_response_summary($response), 618 ]); 111 619 $error_message = 'Error conectando con Holnix API para buscar ISBNs.'; 112 620 if (is_wp_error($response)) { … … 123 631 if (is_array($data)) { 124 632 $all_books_data = array_merge($all_books_data, $data); 633 } else { 634 $this->holnix_log('Respuesta JSON inválida en search-isbns', [ 635 'url' => $search_url, 636 'chunk_index' => $chunk_index, 637 'chunk_size' => count($chunk), 638 'response' => $this->get_response_summary($response), 639 ]); 125 640 } 126 641 } 127 642 128 643 if (!empty($all_books_data)) { 644 $this->holnix_log('Datos recibidos para importación', ['books_count' => count($all_books_data)]); 129 645 $importer = new Holnix_Import(); 130 646 $result = $importer->schedule_import($all_books_data); 131 647 if (is_wp_error($result)) { 648 $this->holnix_log('Error al programar importación', [ 649 'error_code' => $result->get_error_code(), 650 'error_message' => $result->get_error_message(), 651 ]); 132 652 wp_send_json_error(['message' => $result->get_error_message()]); 133 653 return; … … 135 655 wp_send_json_success(['message' => 'Importación de productos iniciada correctamente.']); 136 656 } else { 657 $this->holnix_log('search-isbns no retornó libros', ['isbn_count' => count($isbns)]); 137 658 wp_send_json_error(['message' => 'No se encontró información para los ISBNs seleccionados.']); 138 659 } … … 164 685 $api_url = add_query_arg($query_params, $api_url); 165 686 166 $response = wp_remote_get($api_url, $this->api_headers); 687 $this->holnix_log('Enviando request holnix_fetch_imprint_batch', [ 688 'url' => $api_url, 689 'imprint' => $imprint, 690 'language' => $language, 691 'page' => $page, 692 'per_page' => $per_page, 693 ]); 694 695 $request = $this->holnix_api_request('GET', $api_url, [], [ 696 'context' => 'holnix_fetch_imprint_batch', 697 'imprint' => $imprint, 698 'language' => $language, 699 'page' => $page, 700 'per_page' => $per_page, 701 ]); 702 $response = $request['response']; 703 $final_url = $request['final_url']; 167 704 168 705 if (is_wp_error($response)) { 706 $this->holnix_log('Error en holnix_fetch_imprint_batch (WP_Error)', [ 707 'url' => $api_url, 708 'final_url' => $final_url, 709 'imprint' => $imprint, 710 'language' => $language, 711 'page' => $page, 712 'response' => $this->get_response_summary($response), 713 ]); 169 714 wp_send_json_error(['message' => 'Error de conexión con la API: ' . $response->get_error_message()]); 170 715 return; … … 176 721 177 722 if ($response_code !== 200) { 723 $this->holnix_log('Error HTTP en holnix_fetch_imprint_batch', [ 724 'url' => $api_url, 725 'final_url' => $final_url, 726 'imprint' => $imprint, 727 'language' => $language, 728 'page' => $page, 729 'response' => $this->get_response_summary($response), 730 ]); 178 731 wp_send_json_error(['message' => 'La API devolvió un error: ' . $response_code, 'details' => $data]); 179 732 return; … … 181 734 182 735 if (empty($data)) { 736 $this->holnix_log('Respuesta vacía en holnix_fetch_imprint_batch', [ 737 'url' => $api_url, 738 'final_url' => $final_url, 739 'page' => $page, 740 ]); 183 741 wp_send_json_success(['html' => '', 'pagination' => ['currentPage' => $page, 'lastPage' => $page, 'total' => 0], 'count' => 0]); 184 742 return; … … 191 749 'total' => isset($data['total']) ? $data['total'] : count($items), 192 750 ]; 751 752 $this->holnix_log('Respuesta OK en holnix_fetch_imprint_batch', [ 753 'url' => $api_url, 754 'final_url' => $final_url, 755 'page' => $pagination_data['currentPage'], 756 'last_page' => $pagination_data['lastPage'], 757 'total' => $pagination_data['total'], 758 'items_count' => is_array($items) ? count($items) : 0, 759 ]); 193 760 194 761 ob_start(); … … 316 883 </div> 317 884 <?php } elseif ($this->page === 'holnix_imprint') { 318 $response = wp_remote_get($this->api_url . '/imprints', $this->api_headers); 319 if (is_wp_error($response) || $response['response']['code'] == 401) { 320 echo '<div class="error"><p>Error de autentificación o conexión con la API.</p></div>'; 885 $imprints_url = $this->api_url . '/imprints'; 886 $request = $this->holnix_api_request('GET', $imprints_url, [], [ 887 'context' => 'form_params_imprints', 888 'page' => $this->page, 889 ]); 890 $response = $request['response']; 891 $final_url = $request['final_url']; 892 $response_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response); 893 if (is_wp_error($response) || $response_code === 401) { 894 $this->holnix_log('Error cargando imprints para formulario', [ 895 'url' => $imprints_url, 896 'final_url' => $final_url, 897 'response' => $this->get_response_summary($response), 898 ]); 899 $message = is_wp_error($response) 900 ? 'Error de conexión con la API: ' . $response->get_error_message() 901 : 'Error de autenticación con la API (401).'; 902 echo '<div class="error"><p>' . esc_html($message) . '</p></div>'; 321 903 return; 322 904 } … … 472 1054 { 473 1055 474 echo wp_kses_post($this->header());1056 $this->header(); 475 1057 476 1058 $this->display_import_progress_bar(); … … 482 1064 <div class="custom-dashboard"> 483 1065 484 <?php echo wp_kses_post($this->customNav()); ?>485 486 <?php echo wp_kses_post($this->customSections()); ?>1066 <?php $this->customNav(); ?> 1067 1068 <?php $this->customSections(); ?> 487 1069 488 1070 </div> … … 490 1072 <?php 491 1073 492 $response = wp_remote_get($this->api_url . '/skus', $this->api_headers); 493 494 if (is_wp_error($response) || $response['response']['code'] == 401) { 495 496 echo '<div class="error"><p>Error de autentificación o conexión con la API.</p></div>'; 497 1074 $skus_url = $this->api_url . '/skus'; 1075 $request = $this->holnix_api_request('GET', $skus_url, [], [ 1076 'context' => 'holnix_catalog_page', 1077 'page' => $this->page, 1078 ]); 1079 $response = $request['response']; 1080 $final_url = $request['final_url']; 1081 $response_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response); 1082 1083 if (is_wp_error($response) || $response_code === 401) { 1084 $this->holnix_log('Error cargando skus en dashboard', [ 1085 'url' => $skus_url, 1086 'final_url' => $final_url, 1087 'response' => $this->get_response_summary($response), 1088 ]); 1089 $message = is_wp_error($response) 1090 ? 'Error de conexión con la API: ' . $response->get_error_message() 1091 : 'Error de autenticación con la API (401).'; 1092 echo '<div class="error"><p>' . esc_html($message) . '</p></div>'; 498 1093 return; 499 500 1094 } 501 1095 … … 618 1212 { 619 1213 620 echo wp_kses_post($this->header());1214 $this->header(); 621 1215 622 1216 ?> … … 626 1220 <div class="custom-dashboard"> 627 1221 628 <?php echo wp_kses_post($this->customNav()); ?>1222 <?php $this->customNav(); ?> 629 1223 630 1224 </div> … … 1112 1706 1113 1707 1114 echo wp_kses_post($this->header());1708 $this->header(); 1115 1709 1116 1710 $this->display_import_progress_bar(); … … 1136 1730 <div class="custom-dashboard"> 1137 1731 1138 <?php echo wp_kses_post($this->customNav()); ?>1139 1140 <?php echo wp_kses_post($this->customSections()); ?>1732 <?php $this->customNav(); ?> 1733 1734 <?php $this->customSections(); ?> 1141 1735 1142 1736 </div> … … 1214 1808 1215 1809 1216 $api_response = wp_remote_get($api_url, $this->api_headers); 1810 $this->holnix_log('Enviando request holnix_display_table', [ 1811 'page' => $this->page, 1812 'endpoint' => $endpoint, 1813 'api_url' => $api_url, 1814 'params' => $input_values, 1815 ]); 1816 1817 $request = $this->holnix_api_request('GET', $api_url, [], [ 1818 'context' => 'holnix_display_table', 1819 'page' => $this->page, 1820 'endpoint' => $endpoint, 1821 'params' => $input_values, 1822 ]); 1823 $api_response = $request['response']; 1824 $final_url = $request['final_url']; 1217 1825 1218 1826 if (is_wp_error($api_response) || in_array(wp_remote_retrieve_response_code($api_response), [401, 404])) { 1219 1220 echo '<div class="error"><p>No se encontraron libros con los parámetros seleccionados o hubo un error de conexión.</p></div>'; 1827 $error_message = 'No se encontraron libros con los parámetros seleccionados o hubo un error de conexión.'; 1828 if (is_wp_error($api_response)) { 1829 $error_message = 'Error de conexión con la API: ' . $api_response->get_error_message(); 1830 } elseif (wp_remote_retrieve_response_code($api_response) === 401) { 1831 $error_message = 'Error de autenticación con la API (401). Verifica el token configurado.'; 1832 } elseif (wp_remote_retrieve_response_code($api_response) === 404) { 1833 $error_message = 'El endpoint de Holnix no fue encontrado (404).'; 1834 } 1835 1836 $this->holnix_log('Error en holnix_display_table', [ 1837 'page' => $this->page, 1838 'endpoint' => $endpoint, 1839 'api_url' => $api_url, 1840 'final_url' => $final_url, 1841 'params' => $input_values, 1842 'user_message' => $error_message, 1843 'response' => $this->get_response_summary($api_response), 1844 ]); 1845 1846 echo '<div class="error"><p>' . esc_html($error_message) . '</p></div>'; 1221 1847 1222 1848 return; … … 1224 1850 } 1225 1851 1226 $api_response = json_decode(wp_remote_retrieve_body($api_response), true); 1852 $api_response_body = wp_remote_retrieve_body($api_response); 1853 $api_response = json_decode($api_response_body, true); 1227 1854 1228 1855 if (!$api_response || !is_array($api_response)) { 1856 $this->holnix_log('Respuesta no válida en holnix_display_table', [ 1857 'page' => $this->page, 1858 'endpoint' => $endpoint, 1859 'api_url' => $api_url, 1860 'final_url' => $final_url, 1861 'params' => $input_values, 1862 'body_excerpt' => $this->get_log_excerpt($api_response_body), 1863 ]); 1229 1864 1230 1865 echo '<div class="error"><p>No se encontraron libros con los parámetros seleccionados.</p></div>'; … … 1257 1892 1258 1893 } 1894 1895 $this->holnix_log('Respuesta OK en holnix_display_table', [ 1896 'page' => $this->page, 1897 'endpoint' => $endpoint, 1898 'api_url' => $api_url, 1899 'final_url' => $final_url, 1900 'items_count' => count($api_response), 1901 ]); 1259 1902 1260 1903 echo "</tbody></table><div class='d-flex justify-content-between align-items-center'><span id='results-count-footer'>" . count($api_response) . " resultados</span><p class='submit'><button id='holnix-import-button-footer' class='button-import " . esc_attr($disabled) . "' " . esc_attr($disabled) . ">Importar</button>" . ($disabled ? "<br><br>Botón deshabilitado mientras se procesa la cola actual." : "") . "</p><div></div></div></div>"; … … 1276 1919 check_admin_referer('holnix_update_product'); 1277 1920 $product = wc_get_product($product_id); 1278 $response = wp_remote_get($this->api_url . "/isbn/" . $product->sku, $this->api_headers); 1921 $update_url = $this->api_url . "/isbn/" . $product->sku; 1922 $request = $this->holnix_api_request('GET', $update_url, [], [ 1923 'context' => 'holnix_update_page', 1924 'product_id' => $product_id, 1925 'product_sku' => isset($product->sku) ? $product->sku : '', 1926 ]); 1927 $response = $request['response']; 1928 $final_url = $request['final_url']; 1279 1929 1280 1930 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { 1931 $this->holnix_log('Error en holnix_update_page', [ 1932 'product_id' => $product_id, 1933 'product_sku' => isset($product->sku) ? $product->sku : '', 1934 'url' => $update_url, 1935 'final_url' => $final_url, 1936 'response' => $this->get_response_summary($response), 1937 ]); 1281 1938 set_transient('holnix_update_message', ['type' => 'error', 'text' => 'No se pudo actualizar. El ISBN no existe en Holnix o hubo un error de conexión.'], 30); 1282 1939 } else { … … 1308 1965 { 1309 1966 add_menu_page('Holnix', 'Holnix', 'manage_options', 'holnix_catalog', [$this, 'holnix_catalog_page'], plugins_url('assets/images/holnix_menu_item.png', __FILE__)); 1310 add_submenu_page( null, 'Holnix Settings', 'Settings', 'manage_options', 'holnix_settings', [$this, 'holnix_settings_page']);1311 add_submenu_page( null, 'News', 'News', 'manage_options', 'holnix_news', fn() => $this->holnix_display_table('/news-v2', ['date' => false, 'maxDate' => false]));1312 add_submenu_page( null, 'ISBN', 'ISBN', 'manage_options', 'holnix_isbn', fn() => $this->holnix_display_table('/isbns-v2/{isbns}', ['isbns' => true]));1313 add_submenu_page( null, 'Imprint', 'Imprint', 'manage_options', 'holnix_imprint', fn() => $this->holnix_display_table('/imprint-v2/{imprint}', ['imprint' => true, 'language' => false]));1314 add_submenu_page( null, 'Update', 'Update', 'manage_options', 'holnix_update', [$this, 'holnix_update_page']);1967 add_submenu_page('holnix_catalog', 'Holnix Settings', 'Settings', 'manage_options', 'holnix_settings', [$this, 'holnix_settings_page']); 1968 add_submenu_page('holnix_catalog', 'News', 'News', 'manage_options', 'holnix_news', fn() => $this->holnix_display_table('/news-v2', ['date' => false, 'maxDate' => false])); 1969 add_submenu_page('holnix_catalog', 'ISBN', 'ISBN', 'manage_options', 'holnix_isbn', fn() => $this->holnix_display_table('/isbns-v2/{isbns}', ['isbns' => true])); 1970 add_submenu_page('holnix_catalog', 'Imprint', 'Imprint', 'manage_options', 'holnix_imprint', fn() => $this->holnix_display_table('/imprint-v2/{imprint}', ['imprint' => true, 'language' => false])); 1971 add_submenu_page('holnix_catalog', 'Update', 'Update', 'manage_options', 'holnix_update', [$this, 'holnix_update_page']); 1315 1972 } 1316 1973 -
holnix/trunk/admin/css/holnix.css
r3421769 r3460823 143 143 } 144 144 145 .body-holnix input[type=checkbox]:checked::before{145 /* .body-holnix input[type=checkbox]:checked::before{ 146 146 content:"\f00c"; 147 147 font-family: "Font Awesome 6 Free"; 148 font-weight: 900; 148 149 color: black; 149 150 line-height:1; 150 151 width:100%; 151 152 margin:0; 152 } 153 } */ 153 154 154 155 .body-holnix input[type=checkbox]:checked:disabled{ -
holnix/trunk/admin/import.php
r3421769 r3460823 10 10 class Holnix_Import 11 11 { 12 /** 13 * Indica si el logging de diagnóstico está habilitado. 14 * 15 * @return bool 16 */ 17 private function is_holnix_logging_enabled() 18 { 19 if (defined('HOLNIX_DEBUG_LOG')) { 20 return (bool) HOLNIX_DEBUG_LOG; 21 } 22 23 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 24 return true; 25 } 26 27 if (defined('WP_DEBUG') && WP_DEBUG) { 28 return true; 29 } 30 31 return true; 32 } 33 34 /** 35 * Obtiene un extracto seguro para logging. 36 * 37 * @param mixed $value Valor a resumir. 38 * @param int $max_length Longitud máxima. 39 * @return string 40 */ 41 private function get_log_excerpt($value, $max_length = 1200) 42 { 43 if (!is_string($value)) { 44 if (is_scalar($value)) { 45 $value = (string) $value; 46 } else { 47 $value = wp_json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 48 } 49 } 50 51 if (!is_string($value)) { 52 return ''; 53 } 54 55 if (strlen($value) > $max_length) { 56 return substr($value, 0, $max_length) . '...'; 57 } 58 59 return $value; 60 } 61 62 /** 63 * Registra mensajes de diagnóstico del importador en debug.log. 64 * 65 * @param string $message Mensaje principal. 66 * @param array $context Contexto adicional serializable. 67 * @return void 68 */ 69 private function holnix_log($message, $context = []) 70 { 71 if (!$this->is_holnix_logging_enabled()) { 72 return; 73 } 74 75 $line = '[Holnix][Import] ' . $message; 76 if (!empty($context)) { 77 $json = wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 78 if (is_string($json) && $json !== '') { 79 $line .= ' | ' . $json; 80 } 81 } 82 83 error_log($line); 84 } 12 85 13 86 /** … … 51 124 52 125 if (!empty($actions_in_progress)) { 126 $this->holnix_log('Importación bloqueada por cola en progreso', [ 127 'pending_actions_detected' => count($actions_in_progress), 128 ]); 53 129 return new WP_Error('import_in_progress', 'Por favor, espera a que el proceso de la cola actual finalice para poder realizar una nueva importación.'); 54 130 } elseif ($items) { … … 56 132 // Siempre se procesa en segundo plano para mejorar la experiencia de usuario. 57 133 $chunks = array_chunk($items, $chunk_count); 134 $this->holnix_log('Programando importación', [ 135 'items_count' => count($items), 136 'chunk_count' => count($chunks), 137 'chunk_size' => $chunk_count, 138 ]); 58 139 update_option('holnix_import_total_chunks', count($chunks)); 59 140 update_option('holnix_import_start_time', time()); … … 70 151 as_schedule_single_action(time(), 'update_related_products', [$chunk_key]); 71 152 } 153 154 $this->holnix_log('Importación programada correctamente', [ 155 'scheduled_chunks' => count($chunk_keys), 156 ]); 72 157 } else { 158 $this->holnix_log('schedule_import sin productos'); 73 159 return new WP_Error('no_products', 'Sin productos selecionados para importar.'); 74 160 } … … 86 172 if ($chunk_key) { 87 173 $chunk_data = get_option($chunk_key); 174 if (false === $chunk_data) { 175 $this->holnix_log('Chunk no encontrado en options', ['chunk_key' => $chunk_key]); 176 return; 177 } 88 178 $items = unserialize($chunk_data); 89 179 } 180 181 if (!is_array($items) || empty($items)) { 182 $this->holnix_log('process_chunk sin items válidos', ['chunk_key' => $chunk_key]); 183 return; 184 } 185 186 $this->holnix_log('Inicio process_chunk', [ 187 'chunk_key' => $chunk_key, 188 'items_count' => count($items), 189 ]); 90 190 91 191 $currency = str_replace("lbs", "lb", get_woocommerce_currency()); … … 97 197 $translations = get_transient('holnix_api_translations'); // Intentar obtener de caché 98 198 if (false === $translations) { 99 $response = wp_remote_get($admin_base->api_url . '/translations', $admin_base->api_headers); 199 $this->holnix_log('Solicitando traducciones', [ 200 'url' => $admin_base->api_url . '/translations', 201 'chunk_key' => $chunk_key, 202 ]); 203 $request = $admin_base->holnix_api_request('GET', $admin_base->api_url . '/translations', [], [ 204 'context' => 'import_translations', 205 'chunk_key' => $chunk_key, 206 ]); 207 $response = $request['response']; 208 $final_url = $request['final_url']; 100 209 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { 210 $this->holnix_log('Error obteniendo traducciones', [ 211 'url' => $admin_base->api_url . '/translations', 212 'final_url' => $final_url, 213 'chunk_key' => $chunk_key, 214 'wp_error' => is_wp_error($response) ? $response->get_error_message() : '', 215 'status_code' => is_wp_error($response) ? null : wp_remote_retrieve_response_code($response), 216 'body' => is_wp_error($response) ? '' : $this->get_log_excerpt(wp_remote_retrieve_body($response)), 217 ]); 101 218 return; 102 219 } -
holnix/trunk/admin/js/holnix-selection.js
r3421827 r3460823 3 3 const storeName = 'selectedItems'; 4 4 let db; 5 let useMemoryFallback = false; 6 const memoryStore = new Map(); 7 8 function toErrorMessage(error) { 9 if (!error) { 10 return 'Error desconocido'; 11 } 12 13 if (typeof error === 'string') { 14 return error; 15 } 16 17 if (error.message) { 18 return error.message; 19 } 20 21 if (error.name) { 22 return error.name; 23 } 24 25 return String(error); 26 } 5 27 6 28 async function init() { … … 11 33 } 12 34 13 const request = indexedDB.open(dbName, 1); 35 if (useMemoryFallback) { 36 resolve(null); 37 return; 38 } 39 40 if (typeof window === 'undefined' || !window.indexedDB) { 41 useMemoryFallback = true; 42 console.warn('IndexedDB no disponible. Se usará almacenamiento temporal en memoria.'); 43 resolve(null); 44 return; 45 } 46 47 const request = window.indexedDB.open(dbName, 1); 14 48 15 49 request.onupgradeneeded = (event) => { … … 26 60 27 61 request.onerror = (event) => { 28 console.error('Error al abrir IndexedDB:', event.target.errorCode); 29 reject(event.target.errorCode); 62 const error = event && event.target ? (event.target.error || event.target.errorCode) : null; 63 useMemoryFallback = true; 64 console.error('Error al abrir IndexedDB, fallback a memoria:', toErrorMessage(error)); 65 resolve(null); 30 66 }; 31 67 }); … … 34 70 async function addItem(item) { 35 71 const db = await init(); 72 if (useMemoryFallback || !db) { 73 if (!item || !item.isbn) { 74 return; 75 } 76 memoryStore.set(item.isbn, item); 77 return; 78 } 79 36 80 return new Promise((resolve, reject) => { 37 81 const transaction = db.transaction([storeName], 'readwrite'); … … 45 89 async function removeItem(isbn) { 46 90 const db = await init(); 91 if (useMemoryFallback || !db) { 92 memoryStore.delete(isbn); 93 return; 94 } 95 47 96 return new Promise((resolve, reject) => { 48 97 const transaction = db.transaction([storeName], 'readwrite'); … … 56 105 async function getAllItems() { 57 106 const db = await init(); 107 if (useMemoryFallback || !db) { 108 return Array.from(memoryStore.values()); 109 } 110 58 111 return new Promise((resolve, reject) => { 59 112 const transaction = db.transaction([storeName], 'readonly'); … … 67 120 async function clearAllItems() { 68 121 const db = await init(); 122 if (useMemoryFallback || !db) { 123 memoryStore.clear(); 124 return; 125 } 126 69 127 return new Promise((resolve, reject) => { 70 128 const transaction = db.transaction([storeName], 'readwrite'); … … 171 229 initializeTableInteractions(); 172 230 } 231 }).catch((error) => { 232 console.error('Fallo al inicializar almacenamiento local:', error); 233 updateSelectedCount(); 234 if (!document.getElementById('holnix-imprint-form')) { 235 initializeTableInteractions(); 236 } 173 237 }); 174 238 … … 208 272 const allSelectedItems = await HolnixDB.getAllItems(); 209 273 const isbns = allSelectedItems.map(item => item.data); 274 275 if (!isbns.length) { 276 if (spinner) spinner.style.display = 'none'; 277 alert('Selecciona al menos un libro para importar.'); 278 return; 279 } 210 280 211 281 HolnixDB.clearAllItems(); … … 220 290 url: holnix_ajax_object.ajax_url, 221 291 type: 'POST', 292 dataType: 'json', 293 timeout: 120000, 222 294 data: { 223 295 action: 'holnix_start_full_import', … … 227 299 success: function (response) { 228 300 if (spinner) spinner.style.display = 'none'; 301 if (!response || typeof response.success === 'undefined') { 302 alert('Respuesta inválida del servidor. Revisa la consola para más detalle.'); 303 console.error('Respuesta inesperada en holnix_start_full_import:', response); 304 return; 305 } 306 229 307 if (response.success) { 230 308 const importedMessage = document.getElementById('imported-message'); … … 233 311 } 234 312 } else { 235 alert('Error iniciando la importación: ' + response.data.message); 313 const message = response.data && response.data.message ? response.data.message : 'Error desconocido.'; 314 alert('Error iniciando la importación: ' + message); 236 315 } 237 316 }, … … 340 419 url: ajaxurl, 341 420 type: 'POST', 421 dataType: 'json', 422 timeout: 120000, 342 423 data: { 343 424 action: 'holnix_fetch_imprint_batch', … … 348 429 }, 349 430 success: function (response) { 431 if (!response || typeof response.success === 'undefined') { 432 progressStatus.textContent = 'Respuesta inválida del servidor.'; 433 spinner.style.display = 'none'; 434 console.error("Respuesta inesperada en holnix_fetch_imprint_batch:", response); 435 return; 436 } 437 350 438 if (response.success) { 351 439 const { html, pagination, count } = response.data; … … 396 484 } 397 485 } else { 398 progressStatus.textContent = 'Error: ' + response.data.message; 486 const message = response.data && response.data.message ? response.data.message : 'Error desconocido.'; 487 progressStatus.textContent = 'Error: ' + message; 399 488 spinner.style.display = 'none'; 400 489 } -
holnix/trunk/holnix.php
r3421827 r3460823 4 4 * Plugin URI: https://www.holnix.com/ 5 5 * Description: Importa catálogos editoriales (metadata ONIX) en WooCommerce con un clic. 6 * Version: 1.1. 16 * Version: 1.1.3 7 7 * Author: Iberpixel 8 8 * Author URI: https://www.iberpixel.com/ … … 12 12 */ 13 13 14 const HOLNIX_VERSION = '1.1. 0';14 const HOLNIX_VERSION = '1.1.3'; 15 15 16 16 // Exit if accessed directly … … 21 21 // Check if WooCommerce is active 22 22 if (!in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) { 23 add_action('admin_notices', function () {23 add_action('admin_notices', function () { 24 24 echo '<div class="error"><p>Holnix requires WooCommerce to be installed and active.</p></div>'; 25 25 }); … … 31 31 32 32 // Initialize the plugin 33 add_action('plugins_loaded', function () {33 add_action('plugins_loaded', function () { 34 34 new Holnix_Admin(); 35 35 }); 36 36 37 add_action('admin_notices', function () {37 add_action('admin_notices', function () { 38 38 $message_data = get_transient('holnix_update_message'); 39 39 if ($message_data) { … … 91 91 // Inline JS 92 92 $holnix_js = " 93 document.addEventListener('DOMContentLoaded', function () { 94 const holnixModal = document.getElementById('holnix-modal'); 95 const holnixBgModal= document.getElementById('holnix-bg-modal'); 96 document.getElementById('holnix-open-modal').addEventListener('click', function () { 93 document.addEventListener('DOMContentLoaded', function () { 94 const holnixModal = document.getElementById('holnix-modal'); 95 const holnixBgModal = document.getElementById('holnix-bg-modal'); 96 const openModalBtn = document.getElementById('holnix-open-modal'); 97 const closeModalBtn = document.getElementById('holnix-close-modal'); 98 99 // Solo ejecutar si el botón de abrir existe (es decir, estamos en la ficha de producto) 100 if (openModalBtn) { 101 openModalBtn.addEventListener('click', function () { 102 if (holnixModal && holnixBgModal) { 97 103 holnixModal.style.display = 'block'; 98 104 holnixBgModal.style.display = 'block'; 99 }); 100 document.getElementById('holnix-close-modal').addEventListener('click', function () { 105 } 106 }); 107 } 108 109 if (closeModalBtn) { 110 closeModalBtn.addEventListener('click', function () { 111 if (holnixModal && holnixBgModal) { 101 112 holnixModal.style.display = 'none'; 102 113 holnixBgModal.style.display = 'none'; 103 }); 104 holnixBgModal.addEventListener('click', function () { 114 } 115 }); 116 } 117 118 if (holnixBgModal) { 119 holnixBgModal.addEventListener('click', function () { 120 if (holnixModal) { 105 121 holnixModal.style.display = 'none'; 106 122 holnixBgModal.style.display = 'none'; 107 } );123 } 108 124 }); 109 "; 125 } 126 }); 127 "; 110 128 111 129 // Enqueue WooCommerce admin CSS as a handle for inline style 112 add_action('admin_enqueue_scripts', function () use ($holnix_css, $holnix_js) {130 add_action('admin_enqueue_scripts', function () use ($holnix_css, $holnix_js) { 113 131 wp_register_style('holnix-admin-style', false, array(), HOLNIX_VERSION); 114 132 wp_enqueue_style('holnix-admin-style'); … … 121 139 122 140 123 add_action('post_submitbox_start', function (){141 add_action('post_submitbox_start', function () { 124 142 global $post; 125 143 126 144 if ($post && 'product' === $post->post_type) { 127 145 $product_id = $post->ID; 128 $base_url = admin_url('admin.php?page=holnix_update&product_id=' ) . $product_id;146 $base_url = admin_url('admin.php?page=holnix_update&product_id=') . $product_id; 129 147 $custom_url = wp_nonce_url($base_url, 'holnix_update_product'); 130 148 -
holnix/trunk/readme.txt
r3421827 r3460823 4 4 Requires at least: 5.8 5 5 Tested up to: 6.8 6 Stable tag: 1.1. 16 Stable tag: 1.1.3 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 26 26 Aviso legal: https://www.holnix.com/aviso-legal/ 27 27 28 == Changelog == =======28 == Changelog == 29 29 = 1.0.2 = 30 30 * Ajustes de optimización y seguridad … … 34 34 * [Mejora] En la vista de libros y carga de libros. 35 35 * [Corrección] Solucionado un error que afectaba a editoriales con grandes volúmenes de libros. 36 = 1.1.3 = 37 * [Mejora] Importación y consulta de libros más estable y confiable. 38 * [Corrección] Se solucionaron incidencias en la búsqueda e importación por ISBN para resultados más precisos. 39 * [Mejora] Experiencia de uso optimizada en el panel de administración al seleccionar e importar libros. 40 * [Mejora] Ajustes generales de estabilidad para un funcionamiento más sólido en producción.
Note: See TracChangeset
for help on using the changeset viewer.