Changeset 3800151
- Timestamp:
- 11/13/2026 09:59:52 AM (36 hours ago)
- Location:
- 360-digital-marketing-seo-analyzer/trunk
- Files:
-
- 6 edited
-
api/rest-routes.php (modified) (2 diffs)
-
api/scan-controller.php (modified) (7 diffs)
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/dashboard.js (modified) (9 diffs)
-
core/scan-client.php (modified) (6 diffs)
-
views/dashboard-view.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
360-digital-marketing-seo-analyzer/trunk/api/rest-routes.php
r3722466 r3800151 11 11 12 12 $permission_callback = function () { 13 $nonce = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'] ?? ($_REQUEST['_wpnonce'] ?? ''))); 13 14 return current_user_can('manage_options') && wp_verify_nonce( 14 sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'] ?? '')),15 $nonce, 15 16 'wp_rest' 16 17 ); … … 190 191 ]); 191 192 193 register_rest_route('seo-cro/v1', '/scans/(?P<id>[^/]+)/export/pdf/status', [ 194 'methods' => 'GET', 195 'callback' => ['AI_SEO_CRO_Scan_Controller', 'get_pdf_status'], 196 'permission_callback' => $permission_callback, 197 ]); 198 199 register_rest_route('seo-cro/v1', '/scans/(?P<id>[^/]+)/export/pdf/download', [ 200 'methods' => 'GET', 201 'callback' => ['AI_SEO_CRO_Scan_Controller', 'download_pdf'], 202 'permission_callback' => $permission_callback, 203 ]); 204 192 205 register_rest_route('seo-cro/v1', '/scans/(?P<id>[^/]+)/save-export', [ 193 206 'methods' => 'POST', -
360-digital-marketing-seo-analyzer/trunk/api/scan-controller.php
r3722466 r3800151 20 20 'csv' => 'csv', 21 21 'xlsx' => 'xlsx', 22 'pdf' => ' pdf',22 'pdf' => 'zip', 23 23 ]; 24 24 … … 37 37 echo $body; 38 38 exit; 39 } 40 41 private static function stream_remote_file($remote_url, $content_type, $filename, $headers = []) { 42 @ini_set('memory_limit', '512M'); 43 @set_time_limit(0); 44 45 while (ob_get_level()) { 46 ob_end_clean(); 47 } 48 49 if (!headers_sent()) { 50 nocache_headers(); 51 header('Content-Type: ' . ($content_type ?: 'application/octet-stream')); 52 header('Content-Disposition: attachment; filename="' . $filename . '"'); 53 header('X-Accel-Buffering: no'); 54 } 55 56 // Try streaming via cURL if available 57 if (function_exists('curl_init')) { 58 $ch = curl_init($remote_url); 59 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 60 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 61 curl_setopt($ch, CURLOPT_TIMEOUT, 600); 62 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 63 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); 64 curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, $chunk) { 65 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw binary download chunk 66 echo $chunk; 67 if (function_exists('flush')) { 68 flush(); 69 } 70 return strlen($chunk); 71 }); 72 $success = curl_exec($ch); 73 curl_close($ch); 74 if ($success) { 75 exit; 76 } 77 } 78 79 // Fallback to stream context / fopen 80 $opts = [ 81 'http' => [ 82 'method' => 'GET', 83 'timeout' => 600, 84 'header' => implode("\r\n", $headers), 85 ], 86 'ssl' => [ 87 'verify_peer' => false, 88 'verify_peer_name' => false, 89 ], 90 ]; 91 92 $context = stream_context_create($opts); 93 $handle = @fopen($remote_url, 'rb', false, $context); 94 95 if ($handle) { 96 while (!feof($handle)) { 97 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw binary download chunk 98 echo fread($handle, 65536); 99 if (function_exists('flush')) { 100 flush(); 101 } 102 } 103 fclose($handle); 104 exit; 105 } 106 107 wp_die('Unable to stream download file.', 'Download Error', ['response' => 502]); 108 } 109 110 private static function stream_local_file($file_path, $content_type, $filename) { 111 @ini_set('memory_limit', '512M'); 112 @set_time_limit(0); 113 114 while (ob_get_level()) { 115 ob_end_clean(); 116 } 117 118 if (!headers_sent()) { 119 nocache_headers(); 120 header('Content-Type: ' . ($content_type ?: 'application/octet-stream')); 121 header('Content-Disposition: attachment; filename="' . $filename . '"'); 122 if (file_exists($file_path)) { 123 header('Content-Length: ' . filesize($file_path)); 124 } 125 header('X-Accel-Buffering: no'); 126 } 127 128 $handle = @fopen($file_path, 'rb'); 129 if ($handle) { 130 while (!feof($handle)) { 131 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw binary download chunk 132 echo fread($handle, 65536); 133 if (function_exists('flush')) { 134 flush(); 135 } 136 } 137 fclose($handle); 138 } 39 139 } 40 140 … … 79 179 } 80 180 181 $rawCro = $params['includeCro'] ?? null; 182 $includeCro = !empty($rawCro) && ( 183 $rawCro === true || 184 $rawCro === 'true' || 185 $rawCro === 1 || 186 $rawCro === '1' || 187 filter_var($rawCro, FILTER_VALIDATE_BOOLEAN) 188 ); 189 error_log("[360-SEO_DEBUG] scan-controller received includeCro: " . var_export($rawCro, true) . " -> evaluated: " . ($includeCro ? 'TRUE' : 'FALSE')); 190 81 191 if (!$startUrl) { 82 192 return rest_ensure_response(['success' => false, 'message' => 'startUrl is required']); 83 193 } 84 194 85 $result = $client->create_scan($projectName, $startUrl, $maxPages, $maxDepth, $includeQueryParameters );195 $result = $client->create_scan($projectName, $startUrl, $maxPages, $maxDepth, $includeQueryParameters, $includeCro); 86 196 return rest_ensure_response($result['data'] ?? ['success' => false, 'message' => 'Unknown error', 'raw' => $result]); 87 197 } … … 155 265 } 156 266 267 public static function get_pdf_status($request) { 268 $id = sanitize_text_field($request['id'] ?? ''); 269 if (!$id) { 270 return new WP_Error('ai_seo_cro_invalid_scan_id', 'Invalid scan ID', ['status' => 400]); 271 } 272 273 $client = new AI_SEO_CRO_Scan_Client(); 274 $result = $client->get_pdf_status($id); 275 if (empty($result['success'])) { 276 $data = is_array($result['data'] ?? null) ? $result['data'] : []; 277 return new WP_Error( 278 'ai_seo_cro_pdf_status_failed', 279 $data['message'] ?? 'Unable to fetch PDF status', 280 ['status' => (int) ($result['status'] ?? 502), 'error' => $result] 281 ); 282 } 283 284 return rest_ensure_response($result['data'] ?? []); 285 } 286 287 public static function download_pdf($request) { 288 $id = sanitize_text_field($request['id'] ?? ''); 289 if (!$id) { 290 return new WP_Error('ai_seo_cro_invalid_scan_id', 'Invalid scan ID', ['status' => 400]); 291 } 292 293 $client = new AI_SEO_CRO_Scan_Client(); 294 $safe_id = preg_replace('/[^a-z0-9_-]/i', '-', $id); 295 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export/pdf/download'); 296 297 // Check if ZipArchive can unpack single PDF for a direct .pdf download 298 if (class_exists('ZipArchive')) { 299 require_once ABSPATH . 'wp-admin/includes/file.php'; 300 $tmp_zip = download_url($remote_url, 300); 301 302 if (!is_wp_error($tmp_zip)) { 303 $zip = new ZipArchive(); 304 if ($zip->open($tmp_zip) === true) { 305 if ($zip->numFiles === 1) { 306 $entry_name = $zip->getNameIndex(0); 307 $tmp_dir = get_temp_dir(); 308 $zip->extractTo($tmp_dir, $entry_name); 309 $zip->close(); 310 @unlink($tmp_zip); 311 312 $extracted_pdf = trailingslashit($tmp_dir) . $entry_name; 313 if (file_exists($extracted_pdf)) { 314 self::stream_local_file($extracted_pdf, 'application/pdf', sprintf('scan-%s.pdf', $safe_id)); 315 @unlink($extracted_pdf); 316 exit; 317 } 318 } else { 319 $zip->close(); 320 } 321 } 322 323 // If multi-part scan, stream the zip directly 324 self::stream_local_file($tmp_zip, 'application/zip', sprintf('scan-%s-pdf-parts.zip', $safe_id)); 325 @unlink($tmp_zip); 326 exit; 327 } 328 } 329 330 // Fallback: stream remote zip directly 331 $headers = $client->get_download_headers('zip'); 332 self::stream_remote_file($remote_url, 'application/zip', sprintf('scan-%s-pdf-parts.zip', $safe_id), $headers); 333 } 334 157 335 public static function export_scan($request) { 158 $id = $request['id'] ?? '';336 $id = sanitize_text_field($request['id'] ?? ''); 159 337 $format = self::allowed_export_format($request->get_param('format') ?: 'html'); 160 338 161 $client = new AI_SEO_CRO_Scan_Client(); 162 $result = $format === 'html' 163 ? self::render_html_export($id) 164 : $client->export_scan($id, $format); 165 166 if (empty($result['success']) || empty($result['body'])) { 167 return new WP_Error( 168 'ai_seo_cro_export_failed', 169 'Export failed', 170 ['status' => 502, 'error' => $result] 171 ); 172 } 173 174 $content_type = $result['content_type'] ?: 'application/octet-stream'; 175 $body = $result['body']; 339 if (!$id) { 340 return new WP_Error('ai_seo_cro_invalid_scan_id', 'Invalid scan ID', ['status' => 400]); 341 } 342 343 $client = new AI_SEO_CRO_Scan_Client(); 176 344 $safe_id = preg_replace('/[^a-z0-9_-]/i', '-', $id); 177 $filename = sprintf('scan-%s.%s', $safe_id, self::export_extension($format)); 178 179 self::send_raw_export($body, $content_type, $filename); 345 346 if ($format === 'html') { 347 $result = self::render_html_export($id); 348 if (empty($result['success']) || empty($result['body'])) { 349 return new WP_Error('ai_seo_cro_export_failed', 'Export failed', ['status' => 502, 'error' => $result]); 350 } 351 self::send_raw_export($result['body'], $result['content_type'] ?? 'text/html; charset=utf-8', sprintf('scan-%s.html', $id)); 352 return; 353 } 354 355 if ($format === 'pdf') { 356 // First trigger / check PDF job on Node API 357 $result = $client->export_scan($id, 'pdf'); 358 if (empty($result['success'])) { 359 $err_data = is_array($result['data'] ?? null) ? $result['data'] : []; 360 return new WP_Error( 361 'ai_seo_cro_pdf_export_failed', 362 $err_data['message'] ?? 'PDF export request failed', 363 ['status' => (int) ($result['status'] ?? 502), 'error' => $result] 364 ); 365 } 366 367 $data = is_array($result['data'] ?? null) ? $result['data'] : json_decode($result['body'] ?? '', true); 368 $status = $data['status'] ?? ''; 369 370 // Check if client expects JSON (e.g. AJAX polling) or if job is still in progress 371 $accept = (string) ($_SERVER['HTTP_ACCEPT'] ?? ''); 372 $wants_json = $request->get_param('json') === '1' || strpos($accept, 'application/json') !== false; 373 374 if ($wants_json || $status !== 'completed') { 375 return rest_ensure_response($data ?: $result); 376 } 377 378 // PDF is completed: stream single PDF or multi-part ZIP 379 return self::download_pdf($request); 380 } 381 382 // CSV or XLSX - stream in chunks directly from backend without RAM buffering 383 if ($format === 'xlsx') { 384 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export?format=xlsx'); 385 $headers = $client->get_download_headers('xlsx'); 386 $filename = sprintf('mega-export-%s.xlsx', $safe_id); 387 self::stream_remote_file($remote_url, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $filename, $headers); 388 return; 389 } 390 391 // CSV 392 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export?format=csv'); 393 $headers = $client->get_download_headers('csv'); 394 $filename = sprintf('scan-%s.csv', $safe_id); 395 self::stream_remote_file($remote_url, 'text/csv; charset=utf-8', $filename, $headers); 180 396 } 181 397 … … 190 406 191 407 $client = new AI_SEO_CRO_Scan_Client(); 192 $result = $format === 'html' 193 ? self::render_html_export($id) 194 : $client->export_scan($id, $format); 195 196 if (empty($result['success'])) { 197 return new WP_Error( 198 'ai_seo_cro_export_fetch_failed', 199 'Export fetch failed', 200 ['status' => 502, 'error' => $result] 201 ); 202 } 203 204 $body = $result['body'] ?? ''; 205 if ($body === '') { 206 return new WP_Error('ai_seo_cro_empty_export', 'Export body is empty', ['status' => 502]); 207 } 208 209 $content_type = $result['content_type'] ?? 'application/octet-stream'; 210 $attachment_mime_type = trim(strtok($content_type, ';')) ?: 'application/octet-stream'; 408 $safe_id = preg_replace('/[^a-z0-9_-]/i', '-', $id); 211 409 $ext = self::export_extension($format); 212 $safe_id = preg_replace('/[^a-z0-9_-]/i', '-', $id); 213 $filename = sprintf('scan-%s.%s', $safe_id, $ext); 410 $filename = $format === 'pdf' 411 ? sprintf('scan-%s-pdf-parts.zip', $safe_id) 412 : ($format === 'xlsx' ? sprintf('mega-export-%s.xlsx', $safe_id) : sprintf('scan-%s.%s', $safe_id, $ext)); 214 413 215 414 $upload_dir = wp_upload_dir(); … … 218 417 } 219 418 419 if (!wp_mkdir_p($upload_dir['path'])) { 420 return new WP_Error('ai_seo_cro_upload_dir_create_failed', 'Failed to create upload directory', ['status' => 500]); 421 } 422 220 423 $upload_path = $upload_dir['path'] . '/' . $filename; 221 424 $upload_url = $upload_dir['url'] . '/' . $filename; 222 223 if (!wp_mkdir_p($upload_dir['path'])) { 224 return new WP_Error('ai_seo_cro_upload_dir_create_failed', 'Failed to create upload directory', ['status' => 500]); 225 } 226 227 $written = file_put_contents($upload_path, $body); 228 if ($written === false) { 229 return new WP_Error('ai_seo_cro_write_failed', 'Failed to write file to disk', ['status' => 500]); 425 $post_title = sanitize_text_field('Scan export ' . $id); 426 427 if ($format === 'html') { 428 $result = self::render_html_export($id); 429 if (empty($result['success']) || empty($result['body'])) { 430 return new WP_Error('ai_seo_cro_export_fetch_failed', 'HTML render failed', ['status' => 502]); 431 } 432 $written = file_put_contents($upload_path, $result['body']); 433 if ($written === false) { 434 return new WP_Error('ai_seo_cro_write_failed', 'Failed to write HTML file to disk', ['status' => 500]); 435 } 436 $attachment_mime_type = 'text/html'; 437 } else { 438 // For PDF, CSV, XLSX: Use WordPress download_url() to stream directly to disk without RAM usage 439 require_once ABSPATH . 'wp-admin/includes/file.php'; 440 @ini_set('memory_limit', '512M'); 441 @set_time_limit(300); 442 443 if ($format === 'pdf') { 444 $status_result = $client->get_pdf_status($id); 445 $status_data = $status_result['data'] ?? []; 446 if (($status_data['status'] ?? '') !== 'completed') { 447 $client->export_scan($id, 'pdf'); 448 return new WP_Error( 449 'ai_seo_cro_pdf_not_ready', 450 'PDF generation is in progress. Please wait until completed before saving to Media Library.', 451 ['status' => 202, 'progress' => $status_data['progress'] ?? null] 452 ); 453 } 454 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export/pdf/download'); 455 $attachment_mime_type = 'application/zip'; 456 } elseif ($format === 'xlsx') { 457 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export?format=xlsx'); 458 $attachment_mime_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; 459 } else { 460 $remote_url = $client->get_endpoint_url('/v1/scans/' . rawurlencode($id) . '/export?format=csv'); 461 $attachment_mime_type = 'text/csv'; 462 } 463 464 $tmp_file = download_url($remote_url, 300); 465 if (is_wp_error($tmp_file)) { 466 return new WP_Error( 467 'ai_seo_cro_download_failed', 468 'Failed to download file from scan server: ' . $tmp_file->get_error_message(), 469 ['status' => 502] 470 ); 471 } 472 473 // If PDF, check if single part so we save native .pdf in Media Library 474 if ($format === 'pdf' && class_exists('ZipArchive')) { 475 $zip = new ZipArchive(); 476 if ($zip->open($tmp_file) === true) { 477 if ($zip->numFiles === 1) { 478 $entry_name = $zip->getNameIndex(0); 479 $tmp_dir = get_temp_dir(); 480 $zip->extractTo($tmp_dir, $entry_name); 481 $zip->close(); 482 @unlink($tmp_file); 483 484 $extracted_pdf = trailingslashit($tmp_dir) . $entry_name; 485 $filename = sprintf('scan-%s.pdf', $safe_id); 486 $upload_path = $upload_dir['path'] . '/' . $filename; 487 $upload_url = $upload_dir['url'] . '/' . $filename; 488 $attachment_mime_type = 'application/pdf'; 489 $post_title = sanitize_text_field('Scan PDF Report ' . $id); 490 491 if (!@rename($extracted_pdf, $upload_path)) { 492 if (!@copy($extracted_pdf, $upload_path)) { 493 @unlink($extracted_pdf); 494 return new WP_Error('ai_seo_cro_copy_failed', 'Failed to move PDF to uploads', ['status' => 500]); 495 } 496 @unlink($extracted_pdf); 497 } 498 } else { 499 $zip->close(); 500 $post_title = sanitize_text_field('Scan PDF Archive ' . $id); 501 if (!@rename($tmp_file, $upload_path)) { 502 if (!@copy($tmp_file, $upload_path)) { 503 @unlink($tmp_file); 504 return new WP_Error('ai_seo_cro_copy_failed', 'Failed to move ZIP to uploads', ['status' => 500]); 505 } 506 @unlink($tmp_file); 507 } 508 } 509 } else { 510 $post_title = sanitize_text_field('Scan PDF Archive ' . $id); 511 if (!@rename($tmp_file, $upload_path)) { 512 if (!@copy($tmp_file, $upload_path)) { 513 @unlink($tmp_file); 514 return new WP_Error('ai_seo_cro_copy_failed', 'Failed to move file to uploads', ['status' => 500]); 515 } 516 @unlink($tmp_file); 517 } 518 } 519 } else { 520 if (!@rename($tmp_file, $upload_path)) { 521 if (!@copy($tmp_file, $upload_path)) { 522 @unlink($tmp_file); 523 return new WP_Error('ai_seo_cro_copy_failed', 'Failed to move downloaded file to uploads', ['status' => 500]); 524 } 525 @unlink($tmp_file); 526 } 527 } 230 528 } 231 529 … … 233 531 'guid' => $upload_url, 234 532 'post_mime_type' => $attachment_mime_type, 235 'post_title' => sanitize_text_field('Scan export ' . $id),533 'post_title' => $post_title, 236 534 'post_content' => '', 237 'post_status' => 'inherit' 535 'post_status' => 'inherit', 238 536 ]; 239 537 -
360-digital-marketing-seo-analyzer/trunk/assets/css/admin.css
r3722466 r3800151 164 164 color: #175cd3; 165 165 overflow-wrap: anywhere; 166 } 167 .audit-linkrank-badge { 168 display: inline-block; 169 padding: 2px 8px; 170 border-radius: 9999px; 171 background: #eef4ff; 172 color: #3538cd; 173 font-size: 12px; 174 font-weight: 700; 166 175 } 167 176 .audit-stat-row { -
360-digital-marketing-seo-analyzer/trunk/assets/js/dashboard.js
r3728929 r3800151 251 251 const includeQueryParameters = 252 252 document.getElementById('scan-include-query-parameters')?.checked === true; 253 const includeCro = 254 document.getElementById('scan-include-cro')?.checked === true; 255 console.log('[360-SEO] Starting scan with includeCro:', includeCro, 'element:', document.getElementById('scan-include-cro')); 253 256 const startUrl = apiConfig.defaultUrl || window.location.origin; 254 257 … … 271 274 maxDepth, 272 275 includeQueryParameters, 276 includeCro, 273 277 }), 274 278 }); … … 2097 2101 auditScore('Site Health', report.siteScore) + 2098 2102 auditMetric('Crawled Pages', report.pagesCrawled, healthy + ' healthy') + 2099 auditScore('CRO Health', report.croScore) +2103 (report.includeCro && report.croScore !== null ? auditScore('CRO Health', report.croScore) : '') + 2100 2104 '</div>' + 2101 2105 '<div class=audit-grid data-layout=two><section class=audit-card><h4>AI Search Health <small>beta</small></h4><strong>' + … … 2386 2390 '</a></td><td><b>' + 2387 2391 reportNumber(page.score) + 2388 '%</b></td><td>' + 2392 '%</b></td><td><span class=audit-linkrank-badge title="Internal LinkRank (0-100)">' + 2393 reportNumber(page.internalLinkRank ?? 50) + 2394 '</span></td><td>' + 2389 2395 reportNumber(page.issueCount) + 2390 2396 '</td><td>' + … … 2397 2403 .join(''); 2398 2404 return ( 2399 '<div class=audit-table-wrap><table class=audit-table><thead><tr><th>Page</th><th>Health</th><th> Issues</th><th>Top findings</th></tr></thead><tbody>' +2405 '<div class=audit-table-wrap><table class=audit-table><thead><tr><th>Page</th><th>Health</th><th>LinkRank</th><th>Issues</th><th>Top findings</th></tr></thead><tbody>' + 2400 2406 rows + 2401 2407 '</tbody></table></div>' … … 3143 3149 siteExportBtn.addEventListener('click', async function () { 3144 3150 showSiteScanMessage(); 3145 const scanId = siteScanIdNode.innerText ;3146 const format = siteFormatSelect.value || ' html';3151 const scanId = siteScanIdNode.innerText ? siteScanIdNode.innerText.trim() : ''; 3152 const format = siteFormatSelect.value || 'csv'; 3147 3153 if (!scanId) { 3148 3154 showSiteScanMessage('No scan selected. Start or select a scan first.'); … … 3150 3156 } 3151 3157 3158 siteExportBtn.disabled = true; 3159 const originalBtnText = siteExportBtn.innerText; 3160 3152 3161 try { 3153 3162 const base = apiConfig.scansUrl || '/wp-json/seo-cro/v1/scans'; 3154 const url = 3155 base + 3156 '/' + 3157 encodeURIComponent(scanId) + 3158 '/export?format=' + 3159 encodeURIComponent(format); 3160 const resp = await fetch(url, { 3161 method: 'GET', 3162 headers: { 'X-WP-Nonce': apiConfig.nonce || '' }, 3163 }); 3164 if (!resp.ok) throw new Error('Export failed'); 3165 const blob = await resp.blob(); 3166 const downloadUrl = window.URL.createObjectURL(blob); 3167 const a = document.createElement('a'); 3168 a.href = downloadUrl; 3169 a.download = `scan-${scanId}.${format}`; 3170 document.body.appendChild(a); 3171 a.click(); 3172 a.remove(); 3173 window.URL.revokeObjectURL(downloadUrl); 3163 3164 if (format === 'pdf') { 3165 showSiteScanMessage('Requesting PDF export from server...'); 3166 siteExportBtn.innerText = 'Preparing PDF...'; 3167 3168 // 1. Trigger export 3169 const triggerResp = await fetch( 3170 `${base}/${encodeURIComponent(scanId)}/export?format=pdf&json=1`, 3171 { 3172 method: 'GET', 3173 headers: { 3174 'X-WP-Nonce': apiConfig.nonce || '', 3175 'Accept': 'application/json', 3176 }, 3177 } 3178 ); 3179 3180 const triggerData = await triggerResp.json(); 3181 if (!triggerResp.ok || triggerData.success === false) { 3182 throw new Error(triggerData.message || 'Unable to request PDF export'); 3183 } 3184 3185 // 2. Poll until completed if active/waiting 3186 let currentStatus = triggerData.status || ''; 3187 let pollData = triggerData; 3188 3189 while (currentStatus !== 'completed') { 3190 if (currentStatus === 'failed') { 3191 throw new Error(pollData.error || pollData.message || 'PDF export failed on the server'); 3192 } 3193 3194 const stageMsg = pollData.progress?.message || 'Processing PDF parts...'; 3195 const percent = pollData.progress?.percent !== undefined ? ` (${pollData.progress.percent}%)` : ''; 3196 showSiteScanMessage(`Generating PDF... ${stageMsg}${percent}`); 3197 siteExportBtn.innerText = `PDF: ${pollData.progress?.percent || 0}%`; 3198 3199 await new Promise((resolve) => setTimeout(resolve, 2500)); 3200 3201 const pollResp = await fetch( 3202 `${base}/${encodeURIComponent(scanId)}/export/pdf/status`, 3203 { 3204 method: 'GET', 3205 headers: { 3206 'X-WP-Nonce': apiConfig.nonce || '', 3207 'Accept': 'application/json', 3208 }, 3209 } 3210 ); 3211 3212 if (!pollResp.ok) { 3213 throw new Error('Failed to check PDF progress'); 3214 } 3215 3216 pollData = await pollResp.json(); 3217 currentStatus = pollData.status || ''; 3218 if (currentStatus === 'failed') { 3219 throw new Error(pollData.error || pollData.message || 'PDF generation failed'); 3220 } 3221 } 3222 3223 const partCount = Number(pollData.partCount) || 1; 3224 const isSinglePdf = partCount === 1; 3225 const downloadFilename = isSinglePdf ? `scan-${scanId}.pdf` : `scan-${scanId}-pdf-parts.zip`; 3226 3227 showSiteScanMessage(isSinglePdf ? 'PDF is ready! Downloading PDF report...' : 'PDF is ready! Downloading PDF archive...'); 3228 siteExportBtn.innerText = 'Downloading...'; 3229 3230 // 3. Download natively without memory buffering 3231 const directDownloadUrl = `${base}/${encodeURIComponent(scanId)}/export/pdf/download?_wpnonce=${encodeURIComponent(apiConfig.nonce || '')}`; 3232 const a = document.createElement('a'); 3233 a.href = directDownloadUrl; 3234 a.setAttribute('download', downloadFilename); 3235 document.body.appendChild(a); 3236 a.click(); 3237 a.remove(); 3238 showSiteScanMessage(isSinglePdf ? 'PDF report download started!' : `PDF archive (${partCount} parts) download started!`); 3239 } else { 3240 // CSV, XLSX, or HTML 3241 const formatLabel = format.toUpperCase(); 3242 showSiteScanMessage(`Starting ${formatLabel} report download...`); 3243 3244 const directDownloadUrl = `${base}/${encodeURIComponent(scanId)}/export?format=${encodeURIComponent(format)}&_wpnonce=${encodeURIComponent(apiConfig.nonce || '')}`; 3245 const a = document.createElement('a'); 3246 a.href = directDownloadUrl; 3247 const defaultName = format === 'xlsx' ? `mega-export-${scanId}.xlsx` : `scan-${scanId}.${format}`; 3248 a.setAttribute('download', defaultName); 3249 document.body.appendChild(a); 3250 a.click(); 3251 a.remove(); 3252 showSiteScanMessage(`${formatLabel} report download initiated!`); 3253 } 3174 3254 } catch (err) { 3175 3255 showSiteScanMessage(err.message || 'Export failed'); 3256 } finally { 3257 siteExportBtn.disabled = false; 3258 siteExportBtn.innerText = originalBtnText; 3176 3259 } 3177 3260 }); … … 3181 3264 siteSaveBtn.addEventListener('click', async function () { 3182 3265 showSiteScanMessage(); 3183 const scanId = siteScanIdNode.innerText ;3184 const format = siteFormatSelect.value || ' html';3266 const scanId = siteScanIdNode.innerText ? siteScanIdNode.innerText.trim() : ''; 3267 const format = siteFormatSelect.value || 'csv'; 3185 3268 if (!scanId) { 3186 3269 showSiteScanMessage('No scan selected. Start or select a scan first.'); … … 3193 3276 try { 3194 3277 const base = apiConfig.scansUrl || '/wp-json/seo-cro/v1/scans'; 3195 const url = 3196 base + 3197 '/' + 3198 encodeURIComponent(scanId) + 3199 '/save-export?format=' + 3200 encodeURIComponent(format); 3278 3279 if (format === 'pdf') { 3280 // Check if PDF needs generation 3281 siteSaveResult.innerText = 'Checking PDF status...'; 3282 const triggerResp = await fetch( 3283 `${base}/${encodeURIComponent(scanId)}/export?format=pdf&json=1`, 3284 { 3285 method: 'GET', 3286 headers: { 3287 'X-WP-Nonce': apiConfig.nonce || '', 3288 'Accept': 'application/json', 3289 }, 3290 } 3291 ); 3292 const triggerData = await triggerResp.json(); 3293 let currentStatus = triggerData.status || ''; 3294 let pollData = triggerData; 3295 3296 while (currentStatus !== 'completed') { 3297 if (currentStatus === 'failed') { 3298 throw new Error('PDF export failed on the server'); 3299 } 3300 siteSaveResult.innerText = `Building PDF... ${pollData.progress?.percent || 0}%`; 3301 await new Promise((resolve) => setTimeout(resolve, 2500)); 3302 const pollResp = await fetch( 3303 `${base}/${encodeURIComponent(scanId)}/export/pdf/status`, 3304 { 3305 method: 'GET', 3306 headers: { 3307 'X-WP-Nonce': apiConfig.nonce || '', 3308 'Accept': 'application/json', 3309 }, 3310 } 3311 ); 3312 if (!pollResp.ok) { 3313 throw new Error('Failed to check PDF progress'); 3314 } 3315 pollData = await pollResp.json(); 3316 currentStatus = pollData.status || ''; 3317 } 3318 siteSaveResult.innerText = 'Saving PDF archive to Media Library...'; 3319 } 3320 3321 const url = `${base}/${encodeURIComponent(scanId)}/save-export?format=${encodeURIComponent(format)}`; 3201 3322 const resp = await fetch(url, { 3202 3323 method: 'POST', -
360-digital-marketing-seo-analyzer/trunk/core/scan-client.php
r3722466 r3800151 58 58 $data = null; 59 59 $content_type = wp_remote_retrieve_header($resp, 'content-type'); 60 $content_disposition = wp_remote_retrieve_header($resp, 'content-disposition'); 60 61 if ($content_type && strpos($content_type, 'application/json') !== false) { 61 62 $data = json_decode($body, true); … … 68 69 'data' => $data, 69 70 'content_type' => $content_type, 71 'content_disposition' => $content_disposition, 70 72 ]; 71 73 } … … 76 78 } 77 79 78 public function create_scan($projectName, $startUrl, $maxPages = 100, $maxDepth = 6, $includeQueryParameters = null ) {80 public function create_scan($projectName, $startUrl, $maxPages = 100, $maxDepth = 6, $includeQueryParameters = null, $includeCro = false) { 79 81 $seokey = $this->get_seokey(); 80 82 … … 85 87 'maxDepth' => (int) $maxDepth, 86 88 'includeQueryParameters' => $includeQueryParameters === null ? self::include_query_parameters_default() : (bool) $includeQueryParameters, 89 'includeCro' => (bool) $includeCro, 87 90 'excludePatterns' => function_exists('ai_seo_cro_get_excluded_crawl_patterns') ? ai_seo_cro_get_excluded_crawl_patterns() : [], 88 91 'excludeUrls' => function_exists('ai_seo_cro_get_excluded_crawl_patterns') ? ai_seo_cro_get_excluded_crawl_patterns() : [], … … 93 96 $body['key'] = $seokey; 94 97 } 98 99 error_log("[360-SEO_DEBUG] scan-client outgoing payload: " . wp_json_encode($body)); 95 100 96 101 return $this->request('POST', '/v1/scans', $body); … … 123 128 public function export_scan($scanId, $format = 'html') { 124 129 $format = sanitize_text_field($format); 125 return $this->request('GET', '/v1/scans/' . rawurlencode($scanId) . '/export?format=' . rawurlencode($format), null, ['headers' => $this->default_headers(), 'timeout' => 60]); 130 $headers = [ 131 'Content-Type' => 'application/json', 132 ]; 133 134 if ($format === 'pdf') { 135 $headers['Accept'] = 'application/json'; 136 } else { 137 $headers['Accept'] = '*/*'; 138 } 139 140 return $this->request('GET', '/v1/scans/' . rawurlencode($scanId) . '/export?format=' . rawurlencode($format), null, [ 141 'headers' => $headers, 142 'timeout' => 90, 143 ]); 144 } 145 146 public function get_pdf_status($scanId) { 147 return $this->request('GET', '/v1/scans/' . rawurlencode($scanId) . '/export/pdf/status', null, [ 148 'headers' => ['Accept' => 'application/json'], 149 'timeout' => 30, 150 ]); 151 } 152 153 public function download_pdf($scanId) { 154 return $this->request('GET', '/v1/scans/' . rawurlencode($scanId) . '/export/pdf/download', null, [ 155 'headers' => ['Accept' => 'application/zip, application/octet-stream, */*'], 156 'timeout' => 120, 157 ]); 158 } 159 160 public function get_endpoint_url($path) { 161 return $this->base . '/' . ltrim($path, '/'); 162 } 163 164 public function get_download_headers($format = 'zip') { 165 $headers = [ 166 'Accept: ' . ($format === 'zip' ? 'application/zip, application/octet-stream, */*' : '*/*'), 167 ]; 168 $seokey = $this->get_seokey(); 169 if ($seokey !== '') { 170 $headers[] = 'x-seo-key: ' . $seokey; 171 } 172 return $headers; 126 173 } 127 174 -
360-digital-marketing-seo-analyzer/trunk/views/dashboard-view.php
r3730499 r3800151 83 83 </div> 84 84 85 <div style="margin-top: 10px;"> 86 <label for="scan-include-cro"> 87 <input type="checkbox" id="scan-include-cro" aria-describedby="scan-cro-help" /> 88 Include Conversion Rate Optimization (CRO) Audit 89 </label> 90 <p id="scan-cro-help" class="description">Off by default: runs pure technical and on-page SEO audit. Check this to include call-to-action (CTA), form friction, trust signal, and CRO scores in your audit report.</p> 91 </div> 92 85 93 <div id="site-scan-message" class="site-scan-message" role="alert" aria-atomic="true" hidden></div> 86 94
Note:
See TracChangeset
for help on using the changeset viewer.