<?php
/**
 * Plugin Name: DesignoPage Website Diagnostic
 * Description: Combined website health diagnostics and batch-based full-site link/resource scanning.
 * Version: 1.10.2
 * Author: DesignoPage
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Extract a meta tag value.
 */
function designopage_diag_meta( $html, $name ) {

    if ( preg_match_all( '~<meta\b[^>]*>~i', $html, $tags ) ) {

        foreach ( $tags[0] as $tag ) {

            $attributes = array();

            preg_match_all(
                '~([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(["\'])(.*?)\2~s',
                $tag,
                $matches,
                PREG_SET_ORDER
            );

            foreach ( $matches as $match ) {
                $attributes[ strtolower( $match[1] ) ] =
                    html_entity_decode(
                        $match[3],
                        ENT_QUOTES | ENT_HTML5,
                        'UTF-8'
                    );
            }

            if (
                isset( $attributes['name'] ) &&
                0 === strcasecmp( $attributes['name'], $name ) &&
                isset( $attributes['content'] )
            ) {
                return trim( $attributes['content'] );
            }
        }
    }

    return '';
}

/**
 * Format one report result.
 */
function designopage_diag_row( $status, $title, $detail ) {

    $labels = array(
        'pass'      => 'Pass',
        'warning'   => 'Warning',
        'attention' => 'Needs Attention',
        'unavailable' => 'Could Not Check',
    );

    $icons = array(
        'pass'      => '✓',
        'warning'   => '!',
        'attention' => '×',
        'unavailable' => '?',
    );

    if ( ! isset( $labels[ $status ] ) ) {
        $status = 'warning';
    }

    return array(
        'status' => $status,
        'label'  => $labels[ $status ],
        'icon'   => $icons[ $status ],
        'title'  => $title,
        'detail' => $detail,
    );
}

/**
 * Validate a URL and automatically add HTTPS if omitted.
 */
function designopage_diag_validate_url( $url ) {

    $url = trim( $url );

    if ( '' === $url ) {
        return new WP_Error(
            'invalid_url',
            'Please enter a website address.'
        );
    }

    if ( strlen( $url ) > 2048 ) {
        return new WP_Error(
            'invalid_url',
            'The website address is too long.'
        );
    }

    if ( ! preg_match( '~^https?://~i', $url ) ) {
        $url = 'https://' . $url;
    }

    if ( ! wp_http_validate_url( $url ) ) {
        return new WP_Error(
            'invalid_url',
            'Please enter a valid website address, such as example.com or www.example.com.'
        );
    }

    $parts = wp_parse_url( $url );

    if (
        empty( $parts['host'] ) ||
        ! empty( $parts['user'] ) ||
        ! empty( $parts['pass'] )
    ) {
        return new WP_Error(
            'invalid_url',
            'Please enter a valid public website address.'
        );
    }

    $host = strtolower( rtrim( $parts['host'], '.' ) );

    if (
        'localhost' === $host ||
        substr( $host, -6 ) === '.local' ||
        substr( $host, -4 ) === '.lan' ||
        substr( $host, -4 ) === '.int' ||
        substr( $host, -5 ) === '.test' ||
        substr( $host, -9 ) === '.internal'
    ) {
        return new WP_Error(
            'blocked_host',
            'Local and internal hostnames cannot be checked.'
        );
    }

    /*
     * Reject private and reserved IP address literals.
     * This is a basic check, not a complete DNS-rebinding defence.
     */
    if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {

        $flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;

        if ( ! filter_var( $host, FILTER_VALIDATE_IP, $flags ) ) {
            return new WP_Error(
                'blocked_ip',
                'Private and reserved IP addresses cannot be checked.'
            );
        }
    }

    return esc_url_raw( $url, array( 'http', 'https' ) );
}

/**
 * Make a remote request.
 */
function designopage_diag_request( $url, $timeout = 10, $limit = 500000 ) {

    return wp_safe_remote_get(
        $url,
        array(
            'timeout'             => $timeout,
            'redirection'         => 0,
            'limit_response_size' => $limit,
            'user-agent'          => 'DesignoPage-Website-Diagnostic/1.4',
            'reject_unsafe_urls'  => true,
        )
    );
}

/**
 * Measure time to first byte using a separate, bounded GET request.
 * This is available only when PHP cURL is enabled. Redirects are not followed,
 * and the response body is discarded so the test does not download the page twice.
 */
function designopage_diag_measure_ttfb( $url ) {

    if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'Time to First Byte (TTFB) could not be measured because PHP cURL is not available on this server. The existing response-time check is still reported separately.',
        );
    }

    if ( ! wp_http_validate_url( $url ) ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'TTFB was not measured because the address did not pass WordPress URL validation.',
        );
    }

    $parts = wp_parse_url( $url );
    if ( empty( $parts['scheme'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'TTFB was not measured because only HTTP and HTTPS addresses are supported.',
        );
    }

    $host = strtolower( rtrim( (string) $parts['host'], '.' ) );
    $port = ! empty( $parts['port'] ) ? (int) $parts['port'] : ( 'https' === strtolower( $parts['scheme'] ) ? 443 : 80 );

    /*
     * Resolve and pin a public IPv4 address before using raw cURL. This avoids
     * allowing a user-supplied hostname to resolve to a private/reserved IP
     * during the measurement request. IPv6-only hosts are reported unavailable.
     */
    if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
        $resolved_ips = array( $host );
    } else {
        $resolved_ips = function_exists( 'gethostbynamel' ) ? gethostbynamel( $host ) : false;
    }

    if ( empty( $resolved_ips ) || ! is_array( $resolved_ips ) ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'TTFB could not be measured safely because the hostname did not resolve to a public IPv4 address from this server.',
        );
    }

    $public_ip = '';
    foreach ( $resolved_ips as $resolved_ip ) {
        if ( ! filter_var( $resolved_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
            return array(
                'status' => 'unavailable',
                'detail' => 'TTFB was not measured because the hostname resolved to a private or reserved address, or could not be validated as a public IPv4 address.',
            );
        }
        if ( '' === $public_ip ) {
            $public_ip = $resolved_ip;
        }
    }

    $handle = curl_init( $url );
    if ( false === $handle ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'The server could not initialise the TTFB measurement request.',
        );
    }

    $options = array(
        CURLOPT_RETURNTRANSFER => false,
        CURLOPT_HEADER         => false,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_NOSIGNAL       => true,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_USERAGENT      => 'DesignoPage-Website-Diagnostic/1.8',
        CURLOPT_IPRESOLVE      => CURL_IPRESOLVE_V4,
        CURLOPT_RESOLVE        => array( $host . ':' . $port . ':' . $public_ip ),
        CURLOPT_WRITEFUNCTION  => static function ( $curl, $data ) {
            return strlen( $data );
        },
    );

    if ( defined( 'CURLOPT_PROTOCOLS_STR' ) ) {
        $options[ CURLOPT_PROTOCOLS_STR ] = 'http,https';
    } elseif ( defined( 'CURLOPT_PROTOCOLS' ) ) {
        $options[ CURLOPT_PROTOCOLS ] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
    }

    curl_setopt_array( $handle, $options );
    $success = curl_exec( $handle );
    $info = curl_getinfo( $handle );
    $error_number = curl_errno( $handle );
    curl_close( $handle );

    if ( false === $success || $error_number || ! isset( $info['starttransfer_time'] ) || ! is_numeric( $info['starttransfer_time'] ) ) {
        return array(
            'status' => 'unavailable',
            'detail' => 'TTFB could not be measured for this request. The website may block this test, the connection may have failed, or the server may not support the required cURL operation. This does not by itself mean the website is down.',
        );
    }

    $ttfb_ms = (int) round( (float) $info['starttransfer_time'] * 1000 );
    $http_code = isset( $info['http_code'] ) ? (int) $info['http_code'] : 0;

    if ( $ttfb_ms <= 800 ) {
        return array(
            'status' => 'pass',
            'detail' => 'The separate TTFB request received its first response byte in approximately ' . $ttfb_ms . ' ms' . ( $http_code ? ' (HTTP ' . $http_code . ')' : '' ) . '. This is a screening measurement, not a full browser performance test; repeated runs can vary.',
        );
    }

    if ( $ttfb_ms <= 1800 ) {
        return array(
            'status' => 'warning',
            'detail' => 'The separate TTFB request received its first response byte in approximately ' . $ttfb_ms . ' ms' . ( $http_code ? ' (HTTP ' . $http_code . ')' : '' ) . '. Consider repeating the test and reviewing server processing, caching, redirects and network conditions if the result persists. These are screening thresholds, not a complete performance score.',
        );
    }

    return array(
        'status' => 'attention',
        'detail' => 'The separate TTFB request took approximately ' . $ttfb_ms . ' ms to receive its first response byte' . ( $http_code ? ' (HTTP ' . $http_code . ')' : '' ) . '. Repeat the test and investigate server processing, caching, redirects and network conditions if the result persists. This does not measure total page-load time.',
    );
}

/**
 * Inspect a public HTTPS certificate using PHP's verified TLS stream.
 * Returns a result only when the host certificate can be inspected.
 */
function designopage_diag_certificate_check( $url ) {

    $parts = wp_parse_url( $url );

    if ( empty( $parts['host'] ) || 'https' !== strtolower( $parts['scheme'] ?? '' ) ) {
        return array( 'status' => 'not_applicable', 'detail' => 'A certificate check applies only to HTTPS addresses.' );
    }

    if ( ! function_exists( 'stream_socket_client' ) || ! function_exists( 'openssl_x509_parse' ) ) {
        return array( 'status' => 'unavailable', 'detail' => 'The server does not provide the PHP TLS/OpenSSL functions needed to inspect certificate expiry. This is not a certificate failure.' );
    }

    $host = $parts['host'];
    $port = isset( $parts['port'] ) ? (int) $parts['port'] : 443;
    $context = stream_context_create( array(
        'ssl' => array(
            'verify_peer'       => true,
            'verify_peer_name'  => true,
            'peer_name'         => $host,
            'capture_peer_cert' => true,
            'SNI_enabled'       => true,
        ),
    ) );

    $errno = 0;
    $errstr = '';
    $socket = @stream_socket_client(
        'ssl://' . $host . ':' . $port,
        $errno,
        $errstr,
        6,
        STREAM_CLIENT_CONNECT,
        $context
    );

    if ( ! $socket ) {
        return array( 'status' => 'unavailable', 'detail' => 'The certificate expiry could not be independently inspected from this server. The TLS connection could not be completed by the certificate-check method; this does not by itself prove that the certificate is invalid.' );
    }

    $params = stream_context_get_params( $socket );
    fclose( $socket );

    $certificate = $params['options']['ssl']['peer_certificate'] ?? false;
    $parsed = $certificate ? @openssl_x509_parse( $certificate ) : false;

    if ( ! is_array( $parsed ) || empty( $parsed['validTo_time_t'] ) ) {
        return array( 'status' => 'unavailable', 'detail' => 'A TLS connection was made, but the certificate expiry date could not be read.' );
    }

    $expires = (int) $parsed['validTo_time_t'];
    $days_left = (int) floor( ( $expires - time() ) / DAY_IN_SECONDS );
    $expiry_date = gmdate( 'Y-m-d H:i', $expires ) . ' UTC';

    if ( $days_left < 0 ) {
        return array( 'status' => 'attention', 'detail' => 'The certificate inspected for this hostname expired on ' . $expiry_date . '. Renew it and verify the full certificate chain.' );
    }

    if ( $days_left <= 14 ) {
        return array( 'status' => 'warning', 'detail' => 'The verified TLS certificate expires on ' . $expiry_date . ' (' . $days_left . ' day(s) remaining). Renew it soon.' );
    }

    return array( 'status' => 'pass', 'detail' => 'The TLS certificate passed this server-side certificate and hostname verification check. Its recorded expiry is ' . $expiry_date . ' (' . $days_left . ' day(s) remaining). This does not constitute a complete security audit.' );
}

/**
 * Run the website diagnostic.
 */
function designopage_diag_run( $url ) {

    $rows = array();

    /*
     * Measure the initial request.
     * This includes request processing and any redirects followed.
     */
    $request_started_at = microtime( true );
    $response = designopage_diag_request( $url );
    $response_time_ms = ( microtime( true ) - $request_started_at ) * 1000;

    $parts = wp_parse_url( $url );

    $explicit_protocol = isset( $_POST['designopage_diag_url'] )
        && preg_match(
            '~^\s*https?://~i',
            sanitize_text_field(
                wp_unslash( $_POST['designopage_diag_url'] )
            )
        );

    /*
     * If HTTPS fails at the request level and the visitor did not
     * explicitly choose a protocol, try HTTP.
     */
    if (
        is_wp_error( $response ) &&
        ! $explicit_protocol &&
        isset( $parts['scheme'] ) &&
        'https' === strtolower( $parts['scheme'] )
    ) {

        $http_url = 'http://' . $parts['host'];

        if ( ! empty( $parts['port'] ) && 443 !== (int) $parts['port'] ) {
            $http_url .= ':' . (int) $parts['port'];
        }

        if ( ! empty( $parts['path'] ) ) {
            $http_url .= $parts['path'];
        }

        if ( ! empty( $parts['query'] ) ) {
            $http_url .= '?' . $parts['query'];
        }

        $http_url = designopage_diag_validate_url( $http_url );

        if ( ! is_wp_error( $http_url ) ) {

            $fallback_started_at = microtime( true );
            $fallback_response = designopage_diag_request( $http_url );
            $fallback_time_ms = ( microtime( true ) - $fallback_started_at ) * 1000;

            if ( ! is_wp_error( $fallback_response ) ) {

                $response = $fallback_response;
                $url = $http_url;
                $response_time_ms = $fallback_time_ms;

                $rows[] = designopage_diag_row(
                    'warning',
                    'HTTPS connection failed',
                    'The HTTPS request could not be completed, but the HTTP address responded. Check the HTTPS configuration and certificate before using the website publicly.'
                );
            }
        }
    }

    /*
     * Stop if the website request failed.
     */
    if ( is_wp_error( $response ) ) {

        $rows[] = designopage_diag_row(
            'attention',
            'Website could not be checked',
            'The request failed. The website may be unavailable, may block automated requests, or may have a connection or security restriction.'
        );

        return array(
            'rows' => $rows,
            'url'  => $url,
        );
    }

    $code = (int) wp_remote_retrieve_response_code( $response );
    $body = wp_remote_retrieve_body( $response );

    /*
     * HTTP response status.
     */
    if ( $code >= 200 && $code < 300 ) {

        $rows[] = designopage_diag_row(
            'pass',
            'Website response',
            'The website returned HTTP ' . $code . ', indicating a successful response.'
        );



} elseif ( $code >= 300 && $code < 400 ) {

    if ( 301 === $code ) {
        $redirect_type = 'permanent redirect';
    } elseif ( 302 === $code ) {
        $redirect_type = 'temporary redirect';
    } elseif ( 303 === $code ) {
        $redirect_type = 'redirect to another request';
    } elseif ( 307 === $code ) {
        $redirect_type = 'temporary redirect that preserves the request method';
    } elseif ( 308 === $code ) {
        $redirect_type = 'permanent redirect that preserves the request method';
    } else {
        $redirect_type = 'redirect or additional navigation';
    }

    $rows[] = designopage_diag_row(
        'warning',
        'Website Redirect Detected',
        'The website returned HTTP ' . $code .
        ', indicating a ' . $redirect_type .
        '. For the most accurate diagnostic results, enter the final destination URL directly and run the check again. FAQ: https://designopage.com/faq/site-redirection'
    );



    } else {

        $rows[] = designopage_diag_row(
            'attention',
            'Website response',
            'The website returned HTTP ' . $code . '. Review the website response and availability.'
        );
    }

    /*
     * Response-time classification.
     *
     * Under 500 ms: Fast Response
     * 500-1000 ms: Moderate Response
     * 1001-2000 ms: Slow Response
     * Over 2000 ms: Very Slow Response
     *
     * These are indicative screening thresholds, not official
     * Google PageSpeed or Core Web Vitals classifications.
     */
    $response_time_ms = (int) round( $response_time_ms );

    if ( $response_time_ms < 500 ) {

        $rows[] = designopage_diag_row(
            'pass',
            'Fast Response',
            'The diagnostic request received a response in approximately ' .
            $response_time_ms .
            ' ms. This is a quick response for this test.'
        );

    } elseif ( $response_time_ms <= 1000 ) {

        $rows[] = designopage_diag_row(
            'warning',
            'Moderate Response',
            'The diagnostic request received a response in approximately ' .
            $response_time_ms .
            ' ms. Repeat the check later to see whether this result is consistent.'
        );

    } elseif ( $response_time_ms <= 2000 ) {

        $rows[] = designopage_diag_row(
            'warning',
            'Slow Response',
            'The diagnostic request took approximately ' .
            $response_time_ms .
            ' ms. If this result persists, investigate hosting performance, server processing, caching, redirects and network delays.'
        );

    } else {

        $rows[] = designopage_diag_row(
            'attention',
            'Very Slow Response',
            'The diagnostic request took approximately ' .
            $response_time_ms .
            ' ms. Investigate hosting resources, server processing, caching, redirects and network delays.'
        );
    }

    /*
     * TTFB is measured with a separate bounded request and is not the same
     * as the existing end-to-end diagnostic response time.
     */
    $ttfb_result = designopage_diag_measure_ttfb( $url );
    $rows[] = designopage_diag_row(
        isset( $ttfb_result['status'] ) ? $ttfb_result['status'] : 'warning',
        'Time to First Byte (TTFB)',
        isset( $ttfb_result['detail'] ) ? $ttfb_result['detail'] : 'TTFB could not be measured.'
    );

    /*
     * HTTPS check.
     * This confirms the URL scheme only, not certificate validity.
     */
    $scheme = strtolower(
        (string) wp_parse_url( $url, PHP_URL_SCHEME )
    );

    if ( 'https' === $scheme ) {

        $rows[] = designopage_diag_row(
            'pass',
            'HTTPS address',
            'The checked address uses HTTPS. This check does not independently verify the certificate or prove that the entire website is secure.'
        );

    } else {

        $rows[] = designopage_diag_row(
            'attention',
            'HTTPS address',
            'The checked address uses HTTP. Enable HTTPS and redirect visitors to the secure version.'
        );
    }

    /*
     * Inspect certificate expiry and selected HTTP security headers.
     * These are individual signals, not a complete security assessment.
     */
    if ( 'https' === $scheme ) {
        $certificate = designopage_diag_certificate_check( $url );
        $certificate_status = in_array( $certificate['status'], array( 'pass', 'warning', 'attention' ), true ) ? $certificate['status'] : 'warning';
        $certificate_title = 'SSL/TLS certificate';
        if ( 'unavailable' === $certificate['status'] ) {
            $certificate_title = 'SSL/TLS certificate expiry check unavailable';
        }
        $rows[] = designopage_diag_row( $certificate_status, $certificate_title, $certificate['detail'] );
    } else {
        $rows[] = designopage_diag_row( 'attention', 'SSL/TLS certificate', 'The checked address uses HTTP, so an HTTPS certificate check was not applicable to this address.' );
    }

    $header_checks = array(
        'strict-transport-security' => array( 'Strict-Transport-Security (HSTS)', 'Helps browsers remember to use HTTPS for this host.' ),
        'content-security-policy'   => array( 'Content-Security-Policy', 'Can restrict which sources a browser is allowed to load.' ),
        'x-content-type-options'    => array( 'X-Content-Type-Options', 'Helps prevent MIME-type sniffing when set to nosniff.' ),
        'referrer-policy'           => array( 'Referrer-Policy', 'Controls how much referrer information browsers send.' ),
    );

    foreach ( $header_checks as $header_name => $header_info ) {
        if ( 'strict-transport-security' === $header_name && 'https' !== $scheme ) {
            $rows[] = designopage_diag_row( 'warning', 'Strict-Transport-Security (HSTS) not assessed', 'HSTS is meaningful for HTTPS responses. The checked response used HTTP, so this header was not assessed.' );
            continue;
        }

        $header_value = trim( (string) wp_remote_retrieve_header( $response, $header_name ) );
        if ( '' !== $header_value ) {
            $rows[] = designopage_diag_row( 'pass', $header_info[0] . ' detected', 'The response includes this header. Value: ' . substr( sanitize_text_field( $header_value ), 0, 300 ) . ' This check confirms presence only; it does not prove the policy is correctly configured.' );
        } else {
            $rows[] = designopage_diag_row( 'warning', $header_info[0] . ' not detected', 'This response did not include this header. ' . $header_info[1] . ' Whether it is appropriate and how it should be configured depends on the website.' );
        }
    }

    $frame_options = trim( (string) wp_remote_retrieve_header( $response, 'x-frame-options' ) );
    $csp_value = (string) wp_remote_retrieve_header( $response, 'content-security-policy' );
    if ( '' !== $frame_options || preg_match( '~(?:^|;)\s*frame-ancestors\b~i', $csp_value ) ) {
        $rows[] = designopage_diag_row( 'pass', 'Clickjacking protection signal detected', 'The response includes X-Frame-Options or a Content-Security-Policy frame-ancestors directive. This check confirms presence only, not whether the policy is suitable.' );
    } else {
        $rows[] = designopage_diag_row( 'warning', 'Clickjacking protection header not detected', 'Neither X-Frame-Options nor a Content-Security-Policy frame-ancestors directive was detected in this response.' );
    }

    /*
     * Inspect HTML when the server returns a successful response.
     */
    if ( $code >= 200 && $code < 300 && '' !== $body ) {

        /*
         * Preliminary performance indicators. These inspect the returned HTML
         * only; they are not a browser-based speed test or Core Web Vitals test.
         */
        $html_bytes = strlen( $body );
        $html_kb = round( $html_bytes / 1024, 1 );

        if ( $html_bytes <= 100 * 1024 ) {
            $rows[] = designopage_diag_row( 'pass', 'HTML document size', 'The returned HTML document is approximately ' . $html_kb . ' KB. This is the HTML response size only, not the total size of images, scripts, stylesheets or other page resources.' );
        } elseif ( $html_bytes <= 250 * 1024 ) {
            $rows[] = designopage_diag_row( 'warning', 'HTML document size', 'The returned HTML document is approximately ' . $html_kb . ' KB. Consider reviewing excessive markup or embedded data if this size is typical. This is a screening threshold, not an official performance score.' );
        } else {
            $rows[] = designopage_diag_row( 'attention', 'Large HTML document', 'The returned HTML document is approximately ' . $html_kb . ' KB. A large HTML response can increase transfer and parsing work; review the page markup and embedded data. This does not measure total page weight or actual browser load time.' );
        }

        preg_match_all( '~<img\b[^>]*>~is', $body, $image_tag_matches );
        $image_tags = $image_tag_matches[0];
        $image_count = count( $image_tags );
        $images_missing_alt = 0;
        $images_missing_dimensions = 0;
        $images_lazy_loaded = 0;

        foreach ( $image_tags as $image_tag ) {
            if ( ! preg_match( '~\balt\s*=~i', $image_tag ) ) {
                $images_missing_alt++;
            }
            if ( ! preg_match( '~\bwidth\s*=~i', $image_tag ) || ! preg_match( '~\bheight\s*=~i', $image_tag ) ) {
                $images_missing_dimensions++;
            }
            if ( preg_match( '~\bloading\s*=\s*(["\'])lazy\1~i', $image_tag ) ) {
                $images_lazy_loaded++;
            }
        }

        if ( 0 === $image_count ) {
            $rows[] = designopage_diag_row( 'warning', 'Image optimisation indicators', 'No standard HTML img elements were detected in the returned HTML. Background images, CSS images and JavaScript-rendered images may not be included in this check.' );
        } else {
            $rows[] = designopage_diag_row( 'pass', 'Images detected in HTML', 'Found ' . $image_count . ' standard img element(s). This is an HTML source count, not a count of every image downloaded by the browser.' );

            if ( $images_missing_alt > 0 ) {
                $rows[] = designopage_diag_row( 'warning', 'Images missing alt attributes', $images_missing_alt . ' of ' . $image_count . ' inspected img element(s) do not have an alt attribute. Review these images; meaningful images generally need descriptive alt text, while decorative images may use alt="".' );
            } else {
                $rows[] = designopage_diag_row( 'pass', 'Image alt attributes detected', 'All ' . $image_count . ' inspected img element(s) have an alt attribute. This check does not judge whether the text is meaningful or correctly written.' );
            }

            if ( $images_missing_dimensions > 0 ) {
                $rows[] = designopage_diag_row( 'warning', 'Image dimensions not specified', $images_missing_dimensions . ' of ' . $image_count . ' inspected img element(s) are missing a width or height attribute. Explicit dimensions can help browsers reserve space and reduce layout shifts; CSS or other layout mechanisms may also provide sizing.' );
            } else {
                $rows[] = designopage_diag_row( 'pass', 'Image dimensions specified', 'All ' . $image_count . ' inspected img element(s) include width and height attributes. This is a source check and does not confirm that the dimensions match the actual files.' );
            }

            $rows[] = designopage_diag_row( 'warning', 'Lazy-loaded images indicator', $images_lazy_loaded . ' of ' . $image_count . ' inspected img element(s) explicitly use loading="lazy". Lazy loading is generally useful for off-screen images, but should not be applied blindly to the main above-the-fold or Largest Contentful Paint image.' );
        }

        if ( 'https' === $scheme ) {
            $mixed_content_found = false;
            $mixed_content_count = 0;

            if ( preg_match_all( '~<(?:img|script|iframe|frame|audio|video|source|track|embed|object|link)\b[^>]*>~is', $body, $resource_tags ) ) {
                foreach ( $resource_tags[0] as $resource_tag ) {
                    if ( preg_match_all( '~\b(?:src|srcset|poster|data|href)\s*=\s*(["\'])(.*?)\1~is', $resource_tag, $resource_attrs, PREG_SET_ORDER ) ) {
                        foreach ( $resource_attrs as $resource_attr ) {
                            if ( preg_match( '~(?:^|[\s,])http://~i', html_entity_decode( $resource_attr[2], ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) ) {
                                $mixed_content_found = true;
                                $mixed_content_count++;
                            }
                        }
                    }
                }
            }

            if ( preg_match_all( '~url\(\s*["\']?http://~i', $body, $css_urls ) ) {
                $mixed_content_count += count( $css_urls[0] );
                $mixed_content_found = true;
            }

            if ( $mixed_content_found ) {
                $rows[] = designopage_diag_row( 'warning', 'Possible mixed content detected', 'The returned HTML contains ' . $mixed_content_count . ' HTTP resource reference(s) in resource tags or CSS. This is a preliminary source scan; external CSS and JavaScript-generated resources are not fully inspected.' );
            } else {
                $rows[] = designopage_diag_row( 'pass', 'No mixed content found in inspected HTML', 'This HTML response did not reveal obvious HTTP resource references in the resource tags and CSS patterns checked. External stylesheets, scripts and dynamically loaded resources were not fully inspected.' );
            }
        }

        if (
            preg_match(
                '~<title\b[^>]*>(.*?)</title>~is',
                $body,
                $matches
            ) &&
            '' !== trim( wp_strip_all_tags( $matches[1] ) )
        ) {

            $title = trim( wp_strip_all_tags( $matches[1] ) );

            $rows[] = designopage_diag_row(
                'pass',
                'Page title found',
                'A page title was detected: "' .
                substr( $title, 0, 180 ) . '".'
            );

        } else {

            $rows[] = designopage_diag_row(
                'attention',
                'Page title missing',
                'No page title was detected in the returned HTML. Add a clear, relevant page title.'
            );
        }

        $description = designopage_diag_meta( $body, 'description' );

        if ( '' !== $description ) {

            $rows[] = designopage_diag_row(
                'pass',
                'Meta description found',
                'A meta description was detected. Review whether it accurately describes the page.'
            );

        } else {

            $rows[] = designopage_diag_row(
                'warning',
                'Meta description missing',
                'No meta description was detected. Consider adding a unique, useful description for the page.'
            );
        }

        if ( preg_match( '~<h1\b[^>]*>.*?</h1>~is', $body ) ) {

            $rows[] = designopage_diag_row(
                'pass',
                'H1 heading found',
                'At least one H1 heading was detected. Check that it describes the main topic of the page.'
            );

        } else {

            $rows[] = designopage_diag_row(
                'warning',
                'H1 heading not detected',
                'No H1 heading was detected in the returned HTML. JavaScript-rendered content may not be visible to this basic check.'
            );
        }

        if (
            preg_match(
                '~<meta\b[^>]*name\s*=\s*(["\'])viewport\1~i',
                $body
            ) ||
            preg_match(
                '~<meta\b[^>]*name\s*=\s*viewport\b~i',
                $body
            )
        ) {

            $rows[] = designopage_diag_row(
                'pass',
                'Mobile viewport found',
                'A viewport meta tag was detected. This is one part of mobile-friendly page setup.'
            );

        } else {

            $rows[] = designopage_diag_row(
                'warning',
                'Mobile viewport not detected',
                'A viewport meta tag was not detected. Check the page header and test the site on mobile devices.'
            );
        }

    } else {

        $rows[] = designopage_diag_row(
            'warning',
            'Page content not inspected',
            'The returned HTML could not be inspected reliably, so the page title, description, heading, and viewport were not determined.'
        );
    }

    /*
     * Check robots.txt and sitemap.xml.
     */
    $parts = wp_parse_url( $url );
    $base = $scheme . '://' . $parts['host'];

    if ( ! empty( $parts['port'] ) ) {
        $base .= ':' . (int) $parts['port'];
    }

    $files = array(
        'robots.txt'  => 'Robots.txt',
        'sitemap.xml' => 'Sitemap',
    );

    foreach ( $files as $path => $label ) {

        $check_url = $base . '/' . $path;
        $check = designopage_diag_request( $check_url, 8, 100000 );

        if ( is_wp_error( $check ) ) {

            $rows[] = designopage_diag_row(
                'warning',
                $label . ' check',
                'The file could not be checked. It may be unavailable or automated requests may be blocked.'
            );

            continue;
        }

        $check_code = (int) wp_remote_retrieve_response_code( $check );
        $check_body = trim( wp_remote_retrieve_body( $check ) );

        if (
            $check_code >= 200 &&
            $check_code < 300 &&
            '' !== $check_body
        ) {

            $rows[] = designopage_diag_row(
                'pass',
                $label . ' response',
                'A response was found at ' . $path . '. This basic check does not confirm that the file is complete or valid.'
            );

        } else {

            $rows[] = designopage_diag_row(
                'warning',
                $label . ' response',
                'No usable successful response was detected at ' . $path . '. The file may be located elsewhere or generated dynamically.'
            );
        }
    }

    return array(
        'rows' => $rows,
        'url'  => $url,
    );
}

/**
 * Shortcode: 
    

    

Free Website Health Check

Check important technical and SEO elements of your website.

*/ function designopage_diagnostic_shortcode() { $results = array(); $error = ''; $checked_url = ''; if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) && isset( $_POST['designopage_diag_submit'] ) ) { if ( ! isset( $_POST['designopage_diag_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['designopage_diag_nonce'] ) ), 'designopage_diag_action' ) ) { $error = 'Your form session has expired. Refresh the page and try again.'; } else { $raw_url = isset( $_POST['designopage_diag_url'] ) ? sanitize_text_field( wp_unslash( $_POST['designopage_diag_url'] ) ) : ''; $validated = designopage_diag_validate_url( $raw_url ); if ( is_wp_error( $validated ) ) { $error = $validated->get_error_message(); } else { /* * Basic per-IP rate limit. * Shared networks can share this limit. */ $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $rate_key = 'designopage_diag_' . md5( $ip ); if ( get_transient( $rate_key ) ) { $error = 'Please wait 30 seconds before running another check.'; } else { set_transient( $rate_key, 1, 30 ); $report = designopage_diag_run( $validated ); $results = $report['rows']; $checked_url = $report['url']; } } } } $summary_counts = array( 'passed' => 0, 'review' => 0, 'unavailable' => 0, ); foreach ( $results as $summary_row ) { if ( isset( $summary_row['status'] ) && 'pass' === $summary_row['status'] ) { $summary_counts['passed']++; } elseif ( isset( $summary_row['status'] ) && 'unavailable' === $summary_row['status'] ) { $summary_counts['unavailable']++; } else { // Warnings and attention findings mean review is recommended, // not that the website is necessarily broken or compromised. $summary_counts['review']++; } } ob_start(); ?> <style> .dpdiag-wrap { max-width: 920px; margin: 30px auto; color: #253142; font-family: inherit; } .dpdiag-hero { padding: 32px 24px; border-radius: 14px; background: #263746; color: #fff; text-align: center; } .dpdiag-hero h2 { margin: 0 0 10px; color: #fff; font-size: 30px; line-height: 1.25; } .dpdiag-hero p { margin: 0; color: #edf2f6; } .dpdiag-form { display: flex; gap: 10px; margin: 24px 0; } .dpdiag-form input[type="text"] { flex: 1; min-width: 0; padding: 14px; border: 1px solid #cbd5df; border-radius: 7px; font-size: 16px; } .dpdiag-form button, .dpdiag-cta a { display: inline-block; padding: 14px 20px; border: 0; border-radius: 7px; background: #087f8c; color: #fff; font-weight: 700; text-decoration: none; cursor: pointer; } .dpdiag-form button:hover, .dpdiag-cta a:hover { background: #066772; color: #fff; } .dpdiag-error { padding: 14px 16px; margin: 16px 0; border-left: 4px solid #b42318; border-radius: 5px; background: #fff0ef; color: #7a271a; } .dpdiag-results { margin-top: 28px; } .dpdiag-results h3 { margin-bottom: 8px; font-size: 24px; } .dpdiag-checked-url { margin-bottom: 20px; overflow-wrap: anywhere; color: #526171; font-size: 14px; } .dpdiag-summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 18px 0 24px; } .dpdiag-summary-card { padding: 16px; border: 1px solid #dce5eb; border-radius: 10px; background: #f8fafb; } .dpdiag-summary-number { display: block; margin-bottom: 4px; font-size: 28px; font-weight: 800; line-height: 1.2; } .dpdiag-summary-label { display: block; font-size: 13px; font-weight: 700; } .dpdiag-summary-note { margin: 0 0 20px; color: #526171; font-size: 13px; line-height: 1.6; } .dpdiag-row { display: flex; align-items: flex-start; gap: 14px; padding: 18px; margin-bottom: 12px; border: 1px solid #e0e6eb; border-radius: 10px; background: #fff; } .dpdiag-icon { display: flex; flex: 0 0 32px; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 50%; font-size: 19px; font-weight: 700; } .dpdiag-pass .dpdiag-icon { background: #e5f6ec; color: #16703b; } .dpdiag-warning .dpdiag-icon { background: #fff2cc; color: #855b00; } .dpdiag-attention .dpdiag-icon { background: #fde8e7; color: #b42318; } .dpdiag-content { flex: 1; min-width: 0; } @media (max-width: 600px) { .dpdiag-summary-grid { grid-template-columns: 1fr; } .dpdiag-summary-card { padding: 13px 15px; } .dpdiag-summary-number { font-size: 24px; } } .dpdiag-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 6px; } .dpdiag-heading strong { font-size: 16px; } .dpdiag-badge { padding: 4px 8px; border-radius: 20px; font-size: 11px; font-weight: 700; } .dpdiag-pass .dpdiag-badge { background: #e5f6ec; color: #16703b; } .dpdiag-warning .dpdiag-badge { background: #fff2cc; color: #855b00; } .dpdiag-attention .dpdiag-badge { background: #fde8e7; color: #b42318; } .dpdiag-detail { margin: 0; color: #526171; font-size: 14px; line-height: 1.65; } .dpdiag-cta { padding: 28px 22px; margin-top: 28px; border: 1px solid #dce5eb; border-radius: 12px; background: #f5f8fa; text-align: center; } .dpdiag-cta h3 { margin-top: 0; } .dpdiag-cta p { margin-bottom: 20px; } .dpdiag-note { margin-top: 20px; color: #667584; font-size: 12px; line-height: 1.6; } @media (max-width: 600px) { .dpdiag-hero { padding: 25px 18px; } .dpdiag-hero h2 { font-size: 25px; } .dpdiag-form { flex-direction: column; } .dpdiag-form button { width: 100%; } .dpdiag-row { padding: 14px; } } /* DesignoPage v1.10.1 - Report dropdown spacing */ .dpfs-integrated .dpfs-report-filters select { box-sizing: border-box; min-height: 44px; padding: 10px 36px 10px 14px !important; line-height: 1.5; text-indent: 0; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; background-color: #ffffff; color: #253142; } </style> <div class="dpdiag-wrap"> <div class="dpdiag-hero"> <h2>Free Website Health Check</h2> <p>Check important technical and SEO elements of your website.</p> </div> <form class="dpdiag-form" method="post"> <?php wp_nonce_field( 'designopage_diag_action', 'designopage_diag_nonce' ); ?> <input type="text" inputmode="url" autocomplete="url" name="designopage_diag_url" placeholder="Enter your website, e.g. example.com" aria-label="Website URL" value="<?php echo esc_attr( $checked_url ); ?>" required > <button type="submit" name="designopage_diag_submit" value="1" > Check My Website </button> </form> <?php if ( '' !== $error ) : ?> <div class="dpdiag-error" role="alert"> <?php echo esc_html( $error ); ?> </div> <?php endif; ?> <?php if ( ! empty( $results ) ) : ?> <div class="dpdiag-results"> <h3>Your Website Health Report</h3> <p class="dpdiag-checked-url"> Website checked: <strong><?php echo esc_html( $checked_url ); ?></strong> </p> <div class="dpdiag-summary-grid" aria-label="Health report summary"> <div class="dpdiag-summary-card"> <span class="dpdiag-summary-number"><?php echo esc_html( number_format_i18n( $summary_counts['passed'] ) ); ?></span> <span class="dpdiag-summary-label">Passed</span> </div> <div class="dpdiag-summary-card"> <span class="dpdiag-summary-number"><?php echo esc_html( number_format_i18n( $summary_counts['review'] ) ); ?></span> <span class="dpdiag-summary-label">Needs review</span> </div> <div class="dpdiag-summary-card"> <span class="dpdiag-summary-number"><?php echo esc_html( number_format_i18n( $summary_counts['unavailable'] ) ); ?></span> <span class="dpdiag-summary-label">Could not check</span> </div> </div> <p class="dpdiag-summary-note">These counts summarise the checks shown below. “Needs review” identifies a potential issue or improvement, not proof that the website is broken or compromised. “Could not check” means no conclusion was possible for that check.</p> <?php foreach ( $results as $row ) : ?> <div class="dpdiag-row dpdiag-<?php echo esc_attr( $row['status'] ); ?>"> <div class="dpdiag-icon" aria-hidden="true"> <?php echo esc_html( $row['icon'] ); ?> </div> <div class="dpdiag-content"> <div class="dpdiag-heading"> <strong> <?php echo esc_html( $row['title'] ); ?> </strong> <span class="dpdiag-badge"> <?php echo esc_html( $row['label'] ); ?> </span> </div> <p class="dpdiag-detail"> <?php echo esc_html( $row['detail'] ); ?> </p> </div> </div> <?php endforeach; ?> <div class="dpdiag-cta"> <h3>Need Help Improving Your Website?</h3> <p> DesignoPage can help with WordPress maintenance, website troubleshooting, technical fixes, updates, backups and ongoing webmaster support. </p> <a href="<?php echo esc_url( home_url( '/contact/' ) ); ?>"> Request Website Support </a> </div> <section class="dpfs-integrated" aria-labelledby="dpfs-integrated-title"> <div class="dpfs-head"> <h3 id="dpfs-integrated-title">Full Website Link & File Scan</h3> <p>Now checking discovered pages, internal and external links, images, PDFs and other linked files.</p> </div> <div id="dpfs-progress" class="dpfs-progress" role="status" aria-live="polite"> <strong id="dpfs-message">Preparing full website scan…</strong> <div class="dpfs-track"><div class="dpfs-bar" id="dpfs-bar"></div></div> <div id="dpfs-counts" class="dpfs-small"></div> </div> <div id="dpfs-error" class="dpfs-error" role="alert" hidden></div> <div class="dpfs-report-summary"> <div class="dpfs-summary-card"> <strong id="dpfs-total">0</strong> <span>Resources checked</span> </div> <div class="dpfs-summary-card"> <strong id="dpfs-working-total">0</strong> <span>Working</span> </div> <div class="dpfs-summary-card"> <strong id="dpfs-broken-total">0</strong> <span>Confirmed broken</span> </div> <div class="dpfs-summary-card"> <strong id="dpfs-review-total">0</strong> <span>Needs review</span> </div> <div class="dpfs-summary-card"> <strong id="dpfs-redirect-total">0</strong> <span>Redirects</span> </div> </div> <div class="dpfs-report-filters"> <label for="dpfs-status-filter">Show status</label> <select id="dpfs-status-filter"> <option value="All">All resources</option> <option value="Broken">Broken only</option> <option value="Needs review">Needs review only</option> <option value="Redirect">Redirects only</option> <option value="Working">Working only</option> </select> <label for="dpfs-type-filter">Link location</label> <select id="dpfs-type-filter"> <option value="All">Internal and external</option> <option value="Internal">Internal links</option> <option value="External">External links</option> </select> </div> <div class="dpfs-filter-group"> <label for="dpfs-filter-category">Report Filter Category</label> <select id="dpfs-filter-category"> <option value="content">Content Type</option> <option value="resource">Resource Type</option> </select> </div> <div class="dpfs-filter-group" id="dpfs-content-filter-group"> <label for="dpfs-content-filter">Content Type</label> <select id="dpfs-content-filter"> <option value="All">All content types</option> <option value="Pages">Published Pages</option> <option value="Blog Posts">Published Blog Posts</option> <option value="Categories">Categories</option> <option value="Tags">Tags</option> <option value="Archives">Archives</option> <option value="Password-Protected Pages">Password-Protected Pages</option> <option value="Other / Unknown">Other / Unknown</option> </select> </div> <div class="dpfs-filter-group" id="dpfs-resource-filter-group" hidden> <label for="dpfs-resource-filter">Resource Type</label> <select id="dpfs-resource-filter"> <option value="All">All resource types</option> <option value="CSS stylesheet">CSS stylesheets</option> <option value="JavaScript">JavaScript</option> <option value="Font">Fonts</option> <option value="API">APIs</option> <option value="Media Files">All media files</option> <option value="Image">Images</option> <option value="Video">Videos</option> <option value="Audio">Audio</option> <option value="PDF document">PDF documents</option> <option value="Word document">Word documents</option> <option value="Document">Other documents</option> <option value="Spreadsheet">Spreadsheets</option> <option value="Presentation">Presentations</option> <option value="Archive/download">Archives and downloads</option> <option value="Linked resource">Other linked resources</option> </select> </div> <div id="dpfs-report"></div> <p class="dpfs-small">Only publicly discoverable resources can be checked. Hidden, login-protected, unlinked or JavaScript-only pages may not be discovered. Some servers block automated checks; those results are marked for review. Scan limits help protect hosting resources.</p> </section> <style> .dpfs-integrated{margin:28px 0 0;padding-top:24px;border-top:2px solid #e0e6eb;color:#253142} .dpfs-head{padding:24px;border-radius:12px;background:#263746;color:#fff;text-align:center} .dpfs-head h3{color:#fff;margin:0 0 8px;font-size:24px} .dpfs-head p{color:#edf2f6;margin:0} .dpfs-progress{margin:18px 0;padding:16px;border:1px solid #dce5eb;border-radius:10px} .dpfs-track{height:10px;background:#e8edf1;border-radius:20px;overflow:hidden;margin:10px 0} .dpfs-bar{height:100%;width:0;background:#087f8c;transition:width .2s} .dpfs-section{margin:26px 0} .dpfs-section h4{font-size:21px;margin-bottom:12px} .dpfs-item{padding:14px;margin:10px 0;border:1px solid #e0e6eb;border-radius:9px;overflow-wrap:anywhere;background:#fff} .dpfs-item strong{display:block;margin-bottom:5px} .dpfs-small{font-size:13px;color:#526171;line-height:1.55;overflow-wrap:anywhere} .dpfs-state{font-weight:700} .dpfs-working{color:#16703b}.dpfs-broken{color:#b42318}.dpfs-redirect{color:#855b00}.dpfs-review{color:#6b5b00} .dpfs-error{padding:12px;background:#fff0ef;color:#7a271a;border-radius:6px} @media(max-width:600px){.dpfs-head{padding:20px 16px}.dpfs-head h3{font-size:21px}.dpfs-item{padding:12px}} .dpfs-report-summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin: 20px 0; } .dpfs-summary-card { padding: 14px 10px; border: 1px solid #dce5eb; border-radius: 9px; background: #f8fafb; text-align: center; } .dpfs-summary-card strong { display: block; margin-bottom: 5px; font-size: 25px; color: #263746; } .dpfs-summary-card span { font-size: 12px; font-weight: 700; } .dpfs-report-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 20px 0; } .dpfs-report-filters label { font-size: 14px; font-weight: 700; } .dpfs-report-filters select { max-width: 100%; padding: 10px; border: 1px solid #cbd5df; border-radius: 6px; background: #fff; color: #253142; } @media (max-width: 700px) { .dpfs-report-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .dpfs-report-filters { align-items: stretch; flex-direction: column; } .dpfs-report-filters select { width: 100%; } } </style> <script> (function () { const wrap = document.querySelector('.dpdiag-wrap'); if (!wrap || wrap.dataset.dpfsStarted === '1') { return; } wrap.dataset.dpfsStarted = '1'; const progress = wrap.querySelector('#dpfs-progress'); const message = wrap.querySelector('#dpfs-message'); const counts = wrap.querySelector('#dpfs-counts'); const bar = wrap.querySelector('#dpfs-bar'); const report = wrap.querySelector('#dpfs-report'); const errorBox = wrap.querySelector('#dpfs-error'); // Report filter controls. const statusFilter = wrap.querySelector('#dpfs-status-filter'); const typeFilter = wrap.querySelector('#dpfs-type-filter'); const contentFilter = wrap.querySelector('#dpfs-content-filter'); const resourceFilter = wrap.querySelector('#dpfs-resource-filter'); // Main filter category selector and dropdown containers. const filterCategory = wrap.querySelector('#dpfs-filter-category'); const contentFilterGroup = wrap.querySelector('#dpfs-content-filter-group'); const resourceFilterGroup = wrap.querySelector('#dpfs-resource-filter-group'); const totalEl = wrap.querySelector('#dpfs-total'); const workingEl = wrap.querySelector('#dpfs-working-total'); const brokenEl = wrap.querySelector('#dpfs-broken-total'); const reviewEl = wrap.querySelector('#dpfs-review-total'); const redirectEl = wrap.querySelector('#dpfs-redirect-total'); const ajaxUrl = <?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>; const nonce = <?php echo wp_json_encode(wp_create_nonce('dpfs_scan_nonce')); ?>; const siteUrl = <?php echo wp_json_encode($checked_url); ?>; let scanId = ''; let busy = false; let latestData = null; // Safely display text supplied by the scanner. function esc(value) { return String(value == null ? '' : value).replace(/[&<>"']/g, function (char) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]; }); } // Send requests to the existing WordPress scanner. function post(action, data) { const body = new URLSearchParams( Object.assign({ action: action, nonce: nonce }, data || {}) ); return fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: body.toString(), credentials: 'same-origin' }).then(function (response) { if (!response.ok) { throw new Error('The server returned HTTP ' + response.status + '.'); } return response.json(); }); } // Render a section of the report. function renderSection(title, items) { let html = '<section class="dpfs-section"><h4>' + esc(title) + ' (' + items.length + ')</h4>'; if (!items.length) { html += '<p class="dpfs-small">No resources match this filter.</p>'; } items.forEach(function (item) { const status = item.status || 'Needs review'; const statusClass = status === 'Working' ? 'dpfs-working' : status === 'Broken' ? 'dpfs-broken' : status === 'Redirect' ? 'dpfs-redirect' : 'dpfs-review'; const sourcePages = Array.isArray(item.source_pages) ? item.source_pages : []; // Support link-text fields if the existing scanner provides them. const linkText = item.link_text || item.anchor_text || item.text || ''; html += '<div class="dpfs-item">' + '<strong class="dpfs-state ' + statusClass + '">' + esc(status) + (item.code ? ' — HTTP ' + esc(item.code) : '') + ' · ' + esc(item.type || 'Resource') + '</strong>'; if (linkText) { html += '<p class="dpfs-small"><b>Link text:</b> ' + esc(linkText) + '</p>'; } html += '<div>' + esc(item.url || '') + '</div>' + '<p class="dpfs-small">' + esc(item.detail || '') + '</p>' + '<p class="dpfs-small"><b>Found on:</b> ' + (sourcePages.length ? sourcePages.map(esc).join('<br>') : 'Source page not supplied') + '</p>'; if (item.redirect_to) { html += '<p class="dpfs-small"><b>Redirect destination:</b> ' + esc(item.redirect_to) + '</p>'; } html += '</div>'; }); return html + '</section>'; } // Update the summary cards from the actual returned report data. function updateSummary(allResources) { const totals = { Working: 0, Broken: 0, Redirect: 0, 'Needs review': 0 }; allResources.forEach(function (item) { const status = item.status || 'Needs review'; if (Object.prototype.hasOwnProperty.call(totals, status)) { totals[status]++; } else { totals['Needs review']++; } }); if (totalEl) { totalEl.textContent = allResources.length; } if (workingEl) { workingEl.textContent = totals.Working; } if (brokenEl) { brokenEl.textContent = totals.Broken; } if (reviewEl) { reviewEl.textContent = totals['Needs review']; } if (redirectEl) { redirectEl.textContent = totals.Redirect; } } // Apply report filters without running the scan again. function renderFilteredReport() { if (!latestData || !report) { return; } const internal = Array.isArray(latestData.internal) ? latestData.internal : []; const external = Array.isArray(latestData.external) ? latestData.external : []; const selectedStatus = statusFilter ? statusFilter.value : 'All'; const selectedType = typeFilter ? typeFilter.value : 'All'; const selectedContentType = contentFilter ? contentFilter.value : 'All'; const selectedResourceType = resourceFilter ? resourceFilter.value : 'All'; function matchesStatus(item) { return selectedStatus === 'All' || (item.status || 'Needs review') === selectedStatus; } function matchesContentType(item) { if (selectedContentType === 'All') { return true; } const sourceTypes = Array.isArray(item.source_types) ? item.source_types : ['Other / Unknown']; return sourceTypes.includes(selectedContentType); } function matchesResourceType(item) { if (selectedResourceType === 'All') { return true; } const resourceType = item.type || 'Web page/link'; if (selectedResourceType === 'Media Files') { return ['Image', 'Image/media', 'Video', 'Audio'] .includes(resourceType); } return resourceType === selectedResourceType; } function matchesFilters(item) { return matchesStatus(item) && matchesContentType(item) && matchesResourceType(item); } const filteredInternal = ( selectedType === 'External' ? [] : internal ).filter(matchesFilters); const filteredExternal = ( selectedType === 'Internal' ? [] : external ).filter(matchesFilters); report.innerHTML = renderSection( '1. Internal Links Within the Website', filteredInternal ) + renderSection( '2. Links Connecting to External Sites', filteredExternal ); } // Display each batch and refresh progress and report information. function show(data) { latestData = data; const internal = Array.isArray(data.internal) ? data.internal : []; const external = Array.isArray(data.external) ? data.external : []; const allResources = internal.concat(external); updateSummary(allResources); bar.style.width = data.done ? '100%' : Math.min( 95, Math.round( (Number(data.pages_scanned || 0) / Math.max(Number(data.pages_found || 0), 1)) * 100 ) ) + '%'; message.textContent = data.message || 'Scanning website…'; counts.textContent = 'Pages discovered: ' + (data.pages_found || 0) + ' · Pages scanned: ' + (data.pages_scanned || 0) + ' · Resources checked: ' + (data.resources_checked || 0); renderFilteredReport(); } // Continue the existing batch-based scan. async function nextBatch() { if (busy) { return; } busy = true; try { const response = await post('dpfs_batch', { scan_id: scanId }); if (!response.success) { throw new Error( response.data && response.data.message ? response.data.message : 'The full-site scan could not continue.' ); } show(response.data); if (!response.data.done) { busy = false; window.setTimeout(nextBatch, 250); return; } message.textContent = 'Full website scan complete.'; bar.style.width = '100%'; } catch (error) { errorBox.hidden = false; errorBox.textContent = error.message; message.textContent = 'The full-site scan stopped.'; } finally { busy = false; } } // Filters only update the displayed report. if (statusFilter) { statusFilter.addEventListener('change', renderFilteredReport); } if (typeFilter) { typeFilter.addEventListener('change', renderFilteredReport); } if (contentFilter) { contentFilter.addEventListener('change', renderFilteredReport); } if (resourceFilter) { resourceFilter.addEventListener('change', renderFilteredReport); } // Show only the selected filter category. function updateFilterCategory() { if (!filterCategory || !contentFilterGroup || !resourceFilterGroup) { return; } const selectedCategory = filterCategory.value; contentFilterGroup.hidden = selectedCategory !== 'content'; resourceFilterGroup.hidden = selectedCategory !== 'resource'; } // Update the visible dropdown when the category changes. if (filterCategory) { filterCategory.addEventListener('change', updateFilterCategory); } // Set the correct initial visibility when the scanner loads. updateFilterCategory(); // Start the integrated scan automatically, as before. async function startScan() { progress.hidden = false; errorBox.hidden = true; message.textContent = 'Starting full website scan…'; bar.style.width = '2%'; try { const response = await post('dpfs_start', { url: siteUrl }); if (!response.success) { throw new Error( response.data && response.data.message ? response.data.message : 'Could not start the full-site scan.' ); } scanId = response.data.scan_id; await nextBatch(); } catch (error) { errorBox.hidden = false; errorBox.textContent = error.message; message.textContent = 'Full-site scan could not start.'; } } startScan(); })(); </script> <p class="dpdiag-note"> This is a basic automated check of publicly accessible website responses. Response time measures the diagnostic request, including any redirects followed, not the complete browser page-load time. It is not a complete SEO, performance, accessibility or security audit. Results may be affected by caching, firewalls, redirects and network conditions. Response-time categories are indicative screening thresholds, not official Google scores. </p> </div> <?php endif; ?> </div> <?php return ob_get_clean(); } add_shortcode( 'designopage_diagnostic', 'designopage_diagnostic_shortcode' ); /* Integrated full-site scanner backend. The legacy scanner shortcode remains available. */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** Validate public HTTP(S) URLs. This is a baseline safeguard, not a complete DNS-rebinding defence. */ function dpfs_validate_public_url( $url ) { $url = trim( (string) $url ); if ( '' === $url || strlen( $url ) > 2048 ) { return false; } if ( ! preg_match( '~^https?://~i', $url ) ) { $url = 'https://' . $url; } if ( ! wp_http_validate_url( $url ) ) { return false; } $p = wp_parse_url( $url ); if ( empty( $p['host'] ) || ! empty( $p['user'] ) || ! empty( $p['pass'] ) ) { return false; } $host = strtolower( rtrim( $p['host'], '.' ) ); foreach ( array( 'localhost', '.local', '.lan', '.int', '.test', '.internal' ) as $suffix ) { if ( $host === ltrim( $suffix, '.' ) || ( '.' === $suffix[0] && substr( $host, -strlen( $suffix ) ) === $suffix ) ) { return false; } } if ( filter_var( $host, FILTER_VALIDATE_IP ) && ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return false; } return esc_url_raw( $url, array( 'http', 'https' ) ); } function dpfs_host( $url ) { $p = wp_parse_url( $url ); return isset( $p['host'] ) ? strtolower( rtrim( $p['host'], '.' ) ) : ''; } function dpfs_normalize_url( $url ) { $url = preg_replace( '/#.*$/', '', trim( $url ) ); $p = wp_parse_url( $url ); if ( ! is_array( $p ) || empty( $p['scheme'] ) || empty( $p['host'] ) ) { return $url; } $scheme = strtolower( $p['scheme'] ); $host = strtolower( $p['host'] ); $port = isset( $p['port'] ) ? ':' . (int) $p['port'] : ''; $path = isset( $p['path'] ) && '' !== $p['path'] ? $p['path'] : '/'; $query = isset( $p['query'] ) ? '?' . $p['query'] : ''; return $scheme . '://' . $host . $port . $path . $query; } function dpfs_request( $url, $timeout = 6 ) { $safe = dpfs_validate_public_url( $url ); if ( ! $safe ) { return new WP_Error( 'dpfs_blocked_url', 'URL failed public-address validation.' ); } return wp_safe_remote_get( $safe, array( 'timeout' => $timeout, 'redirection' => 0, 'limit_response_size' => 300000, 'user-agent' => 'DesignoPage-Full-Website-Scanner/1.0', 'reject_unsafe_urls' => true, 'headers' => array( 'Range' => 'bytes=0-299999' ), ) ); } function dpfs_resource_type( $url, $tag = '', $attribute = '' ) { $path = strtolower( (string) wp_parse_url( $url, PHP_URL_PATH ) ); $ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ); // Technical resources. if ( 'css' === $ext ) { return 'CSS stylesheet'; } if ( 'js' === $ext || 'mjs' === $ext ) { return 'JavaScript'; } if ( in_array( $ext, array( 'woff', 'woff2', 'ttf', 'otf', 'eot' ), true ) ) { return 'Font'; } // Common API endpoint patterns. if ( preg_match( '~(?:^|/)(?:wp-json|api|graphql)(?:/|$)~i', $path ) ) { return 'API'; } // Images and downloadable resources. $map = array( 'jpg' => 'Image', 'jpeg' => 'Image', 'png' => 'Image', 'gif' => 'Image', 'webp' => 'Image', 'svg' => 'Image', 'avif' => 'Image', 'bmp' => 'Image', 'ico' => 'Image', 'pdf' => 'PDF document', 'doc' => 'Word document', 'docx' => 'Word document', 'odt' => 'Document', 'rtf' => 'Document', 'xls' => 'Spreadsheet', 'xlsx' => 'Spreadsheet', 'csv' => 'Spreadsheet', 'ods' => 'Spreadsheet', 'ppt' => 'Presentation', 'pptx' => 'Presentation', 'odp' => 'Presentation', 'zip' => 'Archive/download', 'rar' => 'Archive/download', '7z' => 'Archive/download', 'tar' => 'Archive/download', 'gz' => 'Archive/download', 'mp3' => 'Audio', 'wav' => 'Audio', 'ogg' => 'Audio', 'oga' => 'Audio', 'm4a' => 'Audio', 'aac' => 'Audio', 'flac' => 'Audio', 'wma' => 'Audio', 'mp4' => 'Video', 'm4v' => 'Video', 'webm' => 'Video', 'mov' => 'Video', 'avi' => 'Video', 'wmv' => 'Video', 'mpeg' => 'Video', 'mpg' => 'Video', 'ogv' => 'Video', '3gp' => 'Video', 'mkv' => 'Video', 'm3u8' => 'Video', 'ts' => 'Video', ); if ( isset( $map[ $ext ] ) ) { return $map[ $ext ]; } if ( 'img' === $tag || ( in_array( $attribute, array( 'src', 'srcset', 'data-src' ), true ) && in_array( $tag, array( 'img', 'source' ), true ) ) ) { return 'Image/media'; } if ( 'script' === $tag ) { return 'JavaScript'; } if ( 'link' === $tag ) { return 'Linked resource'; } return 'Web page/link'; } function dpfs_is_html_candidate( $url, $type ) { if ( 'Web page/link' !== $type ) { return false; } $path = strtolower( (string) wp_parse_url( $url, PHP_URL_PATH ) ); return ! preg_match( '/\.(?:pdf|docx?|odt|rtf|xlsx?|csv|ods|pptx?|odp|zip|rar|7z|tar|gz|jpe?g|png|gif|webp|svg|avif|bmp|ico|mp3|wav|ogg|mp4|webm|mov|css|js|xml|json)(?:$)/i', $path ); } /** * Identify the type of page containing a link. * Uses WordPress body classes when available. * Returns Other / Unknown when classification is uncertain. */ /** * Classify the publicly accessible page where a resource was found. */ function dpfs_classify_page( $html ) { if ( ! class_exists( 'DOMDocument' ) || '' === trim( $html ) ) { return 'Other / Unknown'; } $old_errors = libxml_use_internal_errors( true ); $dom = new DOMDocument(); $loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html ); libxml_clear_errors(); libxml_use_internal_errors( $old_errors ); if ( ! $loaded ) { return 'Other / Unknown'; } $body = $dom->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body || ! $body->hasAttribute( 'class' ) ) { return 'Other / Unknown'; } $classes = preg_split( '/\s+/', strtolower( trim( $body->getAttribute( 'class' ) ) ) ); // Password-protected WordPress content. if ( in_array( 'post-password-required', $classes, true ) ) { return 'Password-Protected Pages'; } // Individual WordPress blog posts. if ( in_array( 'single-post', $classes, true ) || in_array( 'single-posts', $classes, true ) ) { return 'Blog Posts'; } // Categories. foreach ( $classes as $class ) { if ( 'category' === $class || 0 === strpos( $class, 'category-' ) ) { return 'Categories'; } } // Tags. foreach ( $classes as $class ) { if ( 'tag' === $class || 0 === strpos( $class, 'tag-' ) ) { return 'Tags'; } } // Archive-style listings. foreach ( $classes as $class ) { if ( in_array( $class, array( 'archive', 'date', 'author', 'blog', 'search' ), true ) || 0 === strpos( $class, 'date-' ) || 0 === strpos( $class, 'author-' ) ) { return 'Archives'; } } // Standard WordPress pages. if ( in_array( 'page', $classes, true ) ) { return 'Pages'; } return 'Other / Unknown'; } function dpfs_ajax_start() { check_ajax_referer( 'dpfs_scan_nonce', 'nonce' ); // Basic start-rate limit for public scans; shared IPs may share this limit. $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $rate_key = 'dpfs_start_' . md5( $ip ); if ( get_transient( $rate_key ) ) { wp_send_json_error( array( 'message' => 'Please wait 60 seconds before starting another full website scan.' ), 429 ); } set_transient( $rate_key, 1, 60 ); $raw = isset( $_POST['url'] ) ? sanitize_text_field( wp_unslash( $_POST['url'] ) ) : ''; $url = dpfs_validate_public_url( $raw ); if ( ! $url ) { wp_send_json_error( array( 'message' => 'Please enter a valid public website URL.' ), 400 ); } $id = wp_generate_password( 24, false, false ); $state = array( 'root' => $url, 'root_host' => dpfs_host( $url ), 'page_queue' => array( $url ), 'visited_pages' => array(), 'tested_urls' => array(), 'resource_queue' => array(), 'results' => array( 'internal' => array(), 'external' => array() ), 'pages_found' => 1, 'pages_scanned' => 0, 'resources_checked' => 0, 'done' => false, 'started' => time(), ); set_transient( 'dpfs_' . $id, $state, HOUR_IN_SECONDS ); wp_send_json_success( array( 'scan_id' => $id, 'message' => 'Scan started.' ) ); } add_action( 'wp_ajax_dpfs_start', 'dpfs_ajax_start' ); add_action( 'wp_ajax_nopriv_dpfs_start', 'dpfs_ajax_start' ); function dpfs_ajax_batch() { check_ajax_referer( 'dpfs_scan_nonce', 'nonce' ); $id = isset( $_POST['scan_id'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', wp_unslash( $_POST['scan_id'] ) ) : ''; if ( '' === $id ) { wp_send_json_error( array( 'message' => 'Invalid scan session.' ), 400 ); } $key = 'dpfs_' . $id; $s = get_transient( $key ); if ( ! is_array( $s ) ) { wp_send_json_error( array( 'message' => 'This scan has expired. Please start a new scan.' ), 410 ); } // Check a small number of resources per request to reduce hosting timeouts. $resource_budget = 12; while ( $resource_budget > 0 && ! empty( $s['resource_queue'] ) ) { $item = array_shift( $s['resource_queue'] ); $u = $item['url']; $norm = dpfs_normalize_url( $u ); if ( isset( $s['tested_urls'][ $norm ] ) ) { $idx = $s['tested_urls'][ $norm ]; $section = $item['external'] ? 'external' : 'internal'; if ( isset( $s['results'][ $section ][ $idx ] ) ) { $sources = $s['results'][ $section ][ $idx ]['source_pages']; if ( ! in_array( $item['source'], $sources, true ) ) { $s['results'][ $section ][ $idx ]['source_pages'][] = $item['source']; } if ( ! isset( $s['results'][ $section ][ $idx ]['source_types'] ) ) { $s['results'][ $section ][ $idx ]['source_types'] = array(); } if ( ! in_array( $item['source_type'] ?? 'Other / Unknown', $s['results'][ $section ][ $idx ]['source_types'], true ) ) { $s['results'][ $section ][ $idx ]['source_types'][] = $item['source_type'] ?? 'Other / Unknown'; } } continue; } $response = dpfs_request( $u, 5 ); $code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); $location = ''; if ( ! is_wp_error( $response ) && in_array( $code, array( 301, 302, 303, 307, 308 ), true ) ) { $location = (string) wp_remote_retrieve_header( $response, 'location' ); } if ( is_wp_error( $response ) ) { $status = 'Needs review'; $detail = 'The request failed or was blocked; this does not necessarily mean the resource is broken.'; } elseif ( $code >= 200 && $code < 300 ) { $status = 'Working'; $detail = 'The resource returned a successful HTTP response.'; } elseif ( in_array( $code, array( 301, 302, 303, 307, 308 ), true ) ) { $status = 'Redirect'; $detail = 'The resource redirects to another address. Check the destination if this redirect is unexpected.'; } elseif ( in_array( $code, array( 404, 410 ), true ) ) { $status = 'Broken'; $detail = 'The resource returned HTTP ' . $code . ' and may have been removed or moved.'; } elseif ( 403 === $code || 429 === $code || $code >= 500 || 0 === $code ) { $status = 'Needs review'; $detail = 'The server returned HTTP ' . $code . '. Access rules, rate limits or server problems may affect this result.'; } else { $status = 'Needs review'; $detail = 'The resource returned HTTP ' . $code . '; review this result before treating it as broken.'; } $section = $item['external'] ? 'external' : 'internal'; $idx = count( $s['results'][ $section ] ); $s['tested_urls'][ $norm ] = $idx; $s['results'][ $section ][] = array( 'url' => $u, 'type' => $item['type'], 'status' => $status, 'code' => $code, 'detail' => $detail, 'source_pages' => array( $item['source'] ), 'source_types' => array( $item['source_type'] ?? 'Other / Unknown' ), 'redirect_to' => $location, ); $s['resources_checked']++; $resource_budget--; } // Crawl a small number of pages per request. $page_budget = 3; while ( $page_budget > 0 && ! empty( $s['page_queue'] ) ) { $page = array_shift( $s['page_queue'] ); $page = dpfs_normalize_url( $page ); if ( isset( $s['visited_pages'][ $page ] ) ) { continue; } $s['visited_pages'][ $page ] = true; $response = dpfs_request( $page, 6 ); $s['pages_scanned']++; $page_budget--; if ( is_wp_error( $response ) ) { continue; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $content_type = (string) wp_remote_retrieve_header( $response, 'content-type' ); if ( $code < 200 || $code >= 300 || ( false === stripos( $content_type, 'text/html' ) && false === stripos( $body, '<html' ) && false === stripos( $body, '<!doctype html' ) ) ) { continue; } if ( ! class_exists( 'DOMDocument' ) ) { continue; } $old = libxml_use_internal_errors( true ); $dom = new DOMDocument(); $loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $body ); libxml_clear_errors(); libxml_use_internal_errors( $old ); if ( ! $loaded ) { continue; } $source_page_type = dpfs_classify_page( $body ); $found = array(); $nodes = $dom->getElementsByTagName( '*' ); foreach ( $nodes as $node ) { $tag = strtolower( $node->nodeName ); $attrs = array(); if ( 'a' === $tag || 'area' === $tag ) { $attrs[] = 'href'; } if ( in_array( $tag, array( 'img', 'script', 'iframe', 'source', 'video', 'audio', 'embed', 'track', 'input' ), true ) ) { $attrs[] = 'src'; } if ( 'object' === $tag ) { $attrs[] = 'data'; } if ( 'link' === $tag ) { $attrs[] = 'href'; } if ( 'img' === $tag || 'source' === $tag ) { $attrs[] = 'data-src'; } foreach ( $attrs as $attr ) { if ( ! $node->hasAttribute( $attr ) ) { continue; } $raw_target = trim( $node->getAttribute( $attr ) ); if ( '' === $raw_target || 0 === strpos( $raw_target, '#' ) || preg_match( '~^(?:mailto:|tel:|javascript:|data:|blob:)~i', $raw_target ) ) { continue; } $absolute = WP_Http::make_absolute_url( $raw_target, $page ); $target = dpfs_validate_public_url( $absolute ); if ( ! $target ) { continue; } $target = dpfs_normalize_url( $target ); $external = dpfs_host( $target ) !== $s['root_host']; $type = dpfs_resource_type( $target, $tag, $attr ); $found[ $target ] = array( 'url' => $target, 'external' => $external, 'type' => $type, 'source' => $page, 'source_type' => $source_page_type, ); if ( ! $external && dpfs_is_html_candidate( $target, $type ) && ! isset( $s['visited_pages'][ $target ] ) && ! in_array( $target, $s['page_queue'], true ) && count( $s['visited_pages'] ) + count( $s['page_queue'] ) < 500 ) { $s['page_queue'][] = $target; $s['pages_found']++; } } } foreach ( $found as $target => $item ) { // Keep the total resource workload bounded for public, anonymous scans. if ( count( $s['tested_urls'] ) + count( $s['resource_queue'] ) < 1500 ) { $s['resource_queue'][] = $item; } } } if ( empty( $s['page_queue'] ) && empty( $s['resource_queue'] ) ) { $s['done'] = true; } set_transient( $key, $s, HOUR_IN_SECONDS ); wp_send_json_success( array( 'done' => $s['done'], 'pages_found' => $s['pages_found'], 'pages_scanned' => $s['pages_scanned'], 'resources_checked' => $s['resources_checked'], 'internal' => $s['results']['internal'], 'external' => $s['results']['external'], 'message' => $s['done'] ? 'Scan complete.' : 'Scanning pages and resources…', ) ); } add_action( 'wp_ajax_dpfs_batch', 'dpfs_ajax_batch' ); add_action( 'wp_ajax_nopriv_dpfs_batch', 'dpfs_ajax_batch' ); function dpfs_scanner_shortcode() { $nonce = wp_create_nonce( 'dpfs_scan_nonce' ); ob_start(); ?> <div class="dpfs-wrap"> <style> .dpfs-wrap{max-width:920px;margin:28px auto;color:#253142;font-family:inherit}.dpfs-head{padding:24px;border-radius:12px;background:#263746;color:#fff;text-align:center}.dpfs-head h2{color:#fff;margin:0 0 8px}.dpfs-form{display:flex;gap:10px;margin:20px 0}.dpfs-form input{flex:1;min-width:0;padding:13px;border:1px solid #cbd5df;border-radius:7px;font-size:16px}.dpfs-form button{padding:13px 18px;border:0;border-radius:7px;background:#087f8c;color:#fff;font-weight:700;cursor:pointer}.dpfs-progress{margin:18px 0;padding:16px;border:1px solid #dce5eb;border-radius:10px}.dpfs-track{height:10px;background:#e8edf1;border-radius:20px;overflow:hidden;margin:10px 0}.dpfs-bar{height:100%;width:0;background:#087f8c;transition:width .2s}.dpfs-section{margin:26px 0}.dpfs-item{padding:14px;margin:10px 0;border:1px solid #e0e6eb;border-radius:9px;overflow-wrap:anywhere}.dpfs-item strong{display:block;margin-bottom:5px}.dpfs-small{font-size:13px;color:#526171;line-height:1.55}.dpfs-state{font-weight:700}.dpfs-working{color:#16703b}.dpfs-broken{color:#b42318}.dpfs-redirect{color:#855b00}.dpfs-review{color:#6b5b00}.dpfs-error{padding:12px;background:#fff0ef;color:#7a271a;border-radius:6px}@media(max-width:600px){.dpfs-form{flex-direction:column}.dpfs-form button{width:100%}} </style> <div class="dpfs-head"><h2>Full Website Link & File Scan</h2><p>Scan discovered pages, internal and external links, images, PDFs and other linked files.</p></div> <form class="dpfs-form" id="dpfs-form"><input id="dpfs-url" type="url" placeholder="https://example.com" aria-label="Website URL" required><button id="dpfs-start" type="submit">Scan Entire Website</button></form> <div id="dpfs-progress" class="dpfs-progress" hidden><strong id="dpfs-message">Preparing scan…</strong><div class="dpfs-track"><div class="dpfs-bar" id="dpfs-bar"></div></div><div id="dpfs-counts" class="dpfs-small"></div></div> <div id="dpfs-error" class="dpfs-error" hidden></div> <div id="dpfs-report"></div> <p class="dpfs-small">Only publicly discoverable resources can be checked. Pages that are hidden, login-protected, unlinked or rendered exclusively by JavaScript may not be discovered. Some servers block automated checks; those results are marked for review. Scan limits help protect hosting resources.</p> <script> (function(){ const form=document.getElementById('dpfs-form'), start=document.getElementById('dpfs-start'), urlInput=document.getElementById('dpfs-url'), progress=document.getElementById('dpfs-progress'), message=document.getElementById('dpfs-message'), counts=document.getElementById('dpfs-counts'), bar=document.getElementById('dpfs-bar'), report=document.getElementById('dpfs-report'), errorBox=document.getElementById('dpfs-error'); const ajaxUrl=<?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, nonce=<?php echo wp_json_encode( $nonce ); ?>; let scanId='', busy=false; function post(action,data){const body=new URLSearchParams(Object.assign({action:action,nonce:nonce},data||{}));return fetch(ajaxUrl,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString(),credentials:'same-origin'}).then(r=>r.json());} function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));} function renderSection(title,items){let html='<section class="dpfs-section"><h3>'+esc(title)+' ('+items.length+')</h3>';if(!items.length){html+='<p class="dpfs-small">No resources recorded in this section yet.</p>'; }items.forEach(it=>{let cls=it.status==='Working'?'dpfs-working':it.status==='Broken'?'dpfs-broken':it.status==='Redirect'?'dpfs-redirect':'dpfs-review';html+='<div class="dpfs-item"><strong class="dpfs-state '+cls+'">'+esc(it.status)+(it.code?' — HTTP '+esc(it.code):'')+' · '+esc(it.type)+'</strong><div>'+esc(it.url)+'</div><p class="dpfs-small">'+esc(it.detail)+'</p><p class="dpfs-small"><b>Found on:</b> '+it.source_pages.map(esc).join('<br>')+'</p>'+(it.redirect_to?'<p class="dpfs-small"><b>Redirect destination:</b> '+esc(it.redirect_to)+'</p>':'')+'</div>';});return html+'</section>';} function show(data){const total=data.pages_found+data.resources_checked;bar.style.width=data.done?'100%':Math.min(95,Math.round((data.pages_scanned/Math.max(data.pages_found,1))*100))+'%';message.textContent=data.message;counts.textContent='Pages discovered: '+data.pages_found+' · Pages scanned: '+data.pages_scanned+' · Resources checked: '+data.resources_checked;report.innerHTML=renderSection('1. Internal Links Within the Website',data.internal||[])+renderSection('2. Links Connecting to External Sites',data.external||[]);} async function nextBatch(){if(busy)return;busy=true;try{const r=await post('dpfs_batch',{scan_id:scanId});if(!r.success)throw new Error(r.data&&r.data.message?r.data.message:'The scan could not continue.');show(r.data);if(!r.data.done){busy=false;window.setTimeout(nextBatch,250);return;}start.disabled=false;start.textContent='Scan Again';}catch(e){errorBox.hidden=false;errorBox.textContent=e.message;start.disabled=false;start.textContent='Try Again';}finally{busy=false;}} form.addEventListener('submit',async function(e){e.preventDefault();errorBox.hidden=true;report.innerHTML='';progress.hidden=false;start.disabled=true;start.textContent='Starting…';message.textContent='Starting scan…';bar.style.width='2%';try{const r=await post('dpfs_start',{url:urlInput.value});if(!r.success)throw new Error(r.data&&r.data.message?r.data.message:'Could not start scan.');scanId=r.data.scan_id;start.textContent='Scanning…';nextBatch();}catch(err){errorBox.hidden=false;errorBox.textContent=err.message;start.disabled=false;start.textContent='Scan Entire Website';}}); })(); </script> </div> <?php return ob_get_clean(); } add_shortcode( 'designopage_full_site_scan', 'dpfs_scanner_shortcode' );