Plugin Directory

Changeset 3800151


Ignore:
Timestamp:
11/13/2026 09:59:52 AM (36 hours ago)
Author:
kjkamal2024
Message:

version update

Location:
360-digital-marketing-seo-analyzer/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • 360-digital-marketing-seo-analyzer/trunk/api/rest-routes.php

    r3722466 r3800151  
    1111
    1212    $permission_callback = function () {
     13        $nonce = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'] ?? ($_REQUEST['_wpnonce'] ?? '')));
    1314        return current_user_can('manage_options') && wp_verify_nonce(
    14             sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'] ?? '')),
     15            $nonce,
    1516            'wp_rest'
    1617        );
    … …  
    190191    ]);
    191192
     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
    192205    register_rest_route('seo-cro/v1', '/scans/(?P<id>[^/]+)/save-export', [
    193206        'methods' => 'POST',
  • 360-digital-marketing-seo-analyzer/trunk/api/scan-controller.php

    r3722466 r3800151  
    2020            'csv' => 'csv',
    2121            'xlsx' => 'xlsx',
    22             'pdf' => 'pdf',
     22            'pdf' => 'zip',
    2323        ];
    2424
    … …  
    3737        echo $body;
    3838        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        }
    39139    }
    40140
    … …  
    79179        }
    80180
     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
    81191        if (!$startUrl) {
    82192            return rest_ensure_response(['success' => false, 'message' => 'startUrl is required']);
    83193        }
    84194
    85         $result = $client->create_scan($projectName, $startUrl, $maxPages, $maxDepth, $includeQueryParameters);
     195        $result = $client->create_scan($projectName, $startUrl, $maxPages, $maxDepth, $includeQueryParameters, $includeCro);
    86196        return rest_ensure_response($result['data'] ?? ['success' => false, 'message' => 'Unknown error', 'raw' => $result]);
    87197    }
    … …  
    155265    }
    156266
     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
    157335    public static function export_scan($request) {
    158         $id = $request['id'] ?? '';
     336        $id = sanitize_text_field($request['id'] ?? '');
    159337        $format = self::allowed_export_format($request->get_param('format') ?: 'html');
    160338
    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();
    176344        $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);
    180396    }
    181397
    … …  
    190406
    191407            $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);
    211409            $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));
    214413
    215414            $upload_dir = wp_upload_dir();
    … …  
    218417            }
    219418
     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
    220423            $upload_path = $upload_dir['path'] . '/' . $filename;
    221424            $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                }
    230528            }
    231529
    … …  
    233531                'guid' => $upload_url,
    234532                'post_mime_type' => $attachment_mime_type,
    235                 'post_title' => sanitize_text_field('Scan export ' . $id),
     533                'post_title' => $post_title,
    236534                'post_content' => '',
    237                 'post_status' => 'inherit'
     535                'post_status' => 'inherit',
    238536            ];
    239537
  • 360-digital-marketing-seo-analyzer/trunk/assets/css/admin.css

    r3722466 r3800151  
    164164    color: #175cd3;
    165165    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;
    166175}
    167176.audit-stat-row {
  • 360-digital-marketing-seo-analyzer/trunk/assets/js/dashboard.js

    r3728929 r3800151  
    251251            const includeQueryParameters =
    252252                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'));
    253256            const startUrl = apiConfig.defaultUrl || window.location.origin;
    254257
    … …  
    271274                        maxDepth,
    272275                        includeQueryParameters,
     276                        includeCro,
    273277                    }),
    274278                });
    … …  
    20972101            auditScore('Site Health', report.siteScore) +
    20982102            auditMetric('Crawled Pages', report.pagesCrawled, healthy + ' healthy') +
    2099             auditScore('CRO Health', report.croScore) +
     2103            (report.includeCro && report.croScore !== null ? auditScore('CRO Health', report.croScore) : '') +
    21002104            '</div>' +
    21012105            '<div class=audit-grid data-layout=two><section class=audit-card><h4>AI Search Health <small>beta</small></h4><strong>' +
    … …  
    23862390                    '</a></td><td><b>' +
    23872391                    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>' +
    23892395                    reportNumber(page.issueCount) +
    23902396                    '</td><td>' +
    … …  
    23972403            .join('');
    23982404        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>' +
    24002406            rows +
    24012407            '</tbody></table></div>'
    … …  
    31433149        siteExportBtn.addEventListener('click', async function () {
    31443150            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';
    31473153            if (!scanId) {
    31483154                showSiteScanMessage('No scan selected. Start or select a scan first.');
    … …  
    31503156            }
    31513157
     3158            siteExportBtn.disabled = true;
     3159            const originalBtnText = siteExportBtn.innerText;
     3160
    31523161            try {
    31533162                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                }
    31743254            } catch (err) {
    31753255                showSiteScanMessage(err.message || 'Export failed');
     3256            } finally {
     3257                siteExportBtn.disabled = false;
     3258                siteExportBtn.innerText = originalBtnText;
    31763259            }
    31773260        });
    … …  
    31813264        siteSaveBtn.addEventListener('click', async function () {
    31823265            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';
    31853268            if (!scanId) {
    31863269                showSiteScanMessage('No scan selected. Start or select a scan first.');
    … …  
    31933276            try {
    31943277                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)}`;
    32013322                const resp = await fetch(url, {
    32023323                    method: 'POST',
  • 360-digital-marketing-seo-analyzer/trunk/core/scan-client.php

    r3722466 r3800151  
    5858        $data = null;
    5959        $content_type = wp_remote_retrieve_header($resp, 'content-type');
     60        $content_disposition = wp_remote_retrieve_header($resp, 'content-disposition');
    6061        if ($content_type && strpos($content_type, 'application/json') !== false) {
    6162            $data = json_decode($body, true);
    … …  
    6869            'data' => $data,
    6970            'content_type' => $content_type,
     71            'content_disposition' => $content_disposition,
    7072        ];
    7173    }
    … …  
    7678    }
    7779
    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) {
    7981        $seokey = $this->get_seokey();
    8082
    … …  
    8587            'maxDepth' => (int) $maxDepth,
    8688            'includeQueryParameters' => $includeQueryParameters === null ? self::include_query_parameters_default() : (bool) $includeQueryParameters,
     89            'includeCro' => (bool) $includeCro,
    8790            'excludePatterns' => function_exists('ai_seo_cro_get_excluded_crawl_patterns') ? ai_seo_cro_get_excluded_crawl_patterns() : [],
    8891            'excludeUrls' => function_exists('ai_seo_cro_get_excluded_crawl_patterns') ? ai_seo_cro_get_excluded_crawl_patterns() : [],
    … …  
    9396            $body['key'] = $seokey;
    9497        }
     98
     99        error_log("[360-SEO_DEBUG] scan-client outgoing payload: " . wp_json_encode($body));
    95100
    96101        return $this->request('POST', '/v1/scans', $body);
    … …  
    123128    public function export_scan($scanId, $format = 'html') {
    124129        $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;
    126173    }
    127174
  • 360-digital-marketing-seo-analyzer/trunk/views/dashboard-view.php

    r3730499 r3800151  
    8383            </div>
    8484
     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
    8593            <div id="site-scan-message" class="site-scan-message" role="alert" aria-atomic="true" hidden></div>
    8694
Note: See TracChangeset for help on using the changeset viewer.