base ) { Urls_View::enqueue_pagespeed_assets(); \wp_set_script_translations( 'advan-pagespeed-js', '0-day-analytics' ); \wp_localize_script( 'advan-pagespeed-js', 'advanPagespeedSettings', array( 'url' => \esc_url( \rest_url( Endpoints::ENDPOINT_ROOT_NAME . '/v1' ) ), 'permalink' => \get_option( 'permalink_structure' ), 'nonce' => \wp_create_nonce( 'wp_rest' ), ) ); \wp_localize_script( 'advan-pagespeed-js', 'advanPagespeedMessage', array( 'homeurl' => \esc_url( \home_url( '/' ) ), ) ); } } } /** * Get PageSpeed API data for REST API. * * @param WP_REST_Request $request The REST request object. * @return WP_REST_Response * * @since 4.9.0 */ public static function get_pagespeed_api_data( $request ) { $params = $request->get_params(); if ( isset( $params['cached'] ) && $params['cached'] ) { $data = self::get_cached_pagespeed_data(); } else { $custom_url = isset( $params['url'] ) ? \esc_url_raw( \wp_unslash( $params['url'] ) ) : ''; $data = self::fetch_pagespeed_api_data( $custom_url ); } return new \WP_REST_Response( array( 'data' => $data, 'success' => true, 'message' => '', ), 200 ); } /** * Get cached PageSpeed data. * * @return array|false * * @since 4.9.0 */ private static function get_cached_pagespeed_data() { return \get_transient( 'aadvana_cached_pagespeed_api_data' ); } /** * Fetch PageSpeed API data. * * @param string $custom_url - Optional custom URL to analyze instead of the home URL. * * @return array * * @since 4.9.0 */ private static function fetch_pagespeed_api_data( string $custom_url = '' ) { $url = ( '' !== $custom_url ) ? $custom_url : \esc_url_raw( \home_url() ); $filter_response = Common_Table::fetch_pagespeed_for_url( $url ); // If the fetch returned false (e.g. no API key configured) bail out early so // we do not store broken data in the transient. if ( false === $filter_response ) { return false; } // Store the analysed URL so it can be pre-populated when cached data is restored. $filter_response['url'] = $url; // Cache the data. \set_transient( 'aadvana_cached_pagespeed_api_data', $filter_response, HOUR_IN_SECONDS ); return $filter_response; } /** * Prepares the PageSpeed API response. * * @param array $response The raw PageSpeed API response. * * @return array Prepared PageSpeed API data. * * @since 4.9.0 */ public static function prepare_pagespeed_api_response( $response ) { // Guard: detect an empty or errored API response (e.g. the URL was not publicly // accessible, was blocked by the API, or Lighthouse returned no usable data). if ( empty( $response ) || ! isset( $response['lighthouseResult'] ) || empty( $response['lighthouseResult']['categories'] ) || empty( $response['lighthouseResult']['audits'] ) ) { $error_message = __( 'PageSpeed data could not be retrieved. The URL may not be publicly accessible, or the API returned an incomplete response.', '0-day-analytics' ); if ( isset( $response['error']['message'] ) && '' !== $response['error']['message'] ) { $error_message = $response['error']['message']; } elseif ( isset( $response['error']['errors'][0]['message'] ) && '' !== $response['error']['errors'][0]['message'] ) { $error_message = $response['error']['errors'][0]['message']; } return array( 'error' => true, 'error_message' => $error_message, 'lighthouseResult' => array( 'configSettings' => array( 'formFactor' => '' ), 'categories' => array(), 'categoryAudits' => array(), 'screenshots' => array(), 'audits' => array(), ), ); } $categories = array(); $selected_audit_refs = array( 'FCP', 'LCP', 'TBT', 'CLS', 'SI' ); $selected_audits_id = array(); // Audit IDs to collect per extra category (accessibility, best-practices, seo). $extra_category_audit_ids = array(); // Audit groups that map to performance insights. $insight_groups = array( 'insights', 'hidden' ); // storing details of accessibility, best-practices, performance, pwa, and seo categories. if ( isset( $response['lighthouseResult'] ) && isset( $response['lighthouseResult']['categories'] ) && is_array( $response['lighthouseResult']['categories'] ) ) { foreach ( $response['lighthouseResult']['categories'] as $key => $category ) { $categories[ $key ]['id'] = $category['id'] ?? null; $categories[ $key ]['title'] = $category['title'] ?? null; $categories[ $key ]['score'] = $category['score'] ?? null; $categories[ $key ]['description'] = $category['description'] ?? null; $categories[ $key ]['manualDescription'] = $category['manualDescription'] ?? null; if ( 'performance' === $key ) { $audit_refs = array(); foreach ( $category['auditRefs'] as $audit_ref ) { if ( isset( $audit_ref['acronym'] ) && in_array( $audit_ref['acronym'], $selected_audit_refs, true ) ) { $audit_refs[] = $audit_ref; $selected_audits_id[] = $audit_ref['id']; } if ( 'diagnostics' === ( $audit_ref['group'] ?? '' ) ) { $selected_audits_id[] = $audit_ref['id']; } if ( in_array( $audit_ref['group'] ?? '', $insight_groups, true ) ) { $selected_audits_id[] = $audit_ref['id']; } } $categories[ $key ]['auditRefs'] = $audit_refs; } elseif ( in_array( $key, array( 'accessibility', 'best-practices', 'seo' ), true ) ) { // Collect audit refs for extra categories. $cat_audit_refs = array(); foreach ( $category['auditRefs'] as $audit_ref ) { $cat_audit_refs[] = $audit_ref; $extra_category_audit_ids[ $key ][] = $audit_ref['id']; $selected_audits_id[] = $audit_ref['id']; } $categories[ $key ]['auditRefs'] = $cat_audit_refs; } } } $selected_audits_id = array_unique( $selected_audits_id ); $data['lighthouseResult']['categories'] = $categories; // Helper closure to convert markdown links. $convert_description = function ( $desc ) { if ( ! $desc ) { return null; } return preg_replace( '/\[(.*?)\]\((.*?)\)/', '$1 (opens in a new tab)', $desc ); }; // Detail value types that are safe and useful to pass to the front-end. $renderable_detail_types = array( 'table', 'opportunity', 'list', 'criticalrequestchain' ); // Detail item value types that are scalar/renderable (skip node objects, etc.). $scalar_item_value_types = array( 'url', 'source-location', 'bytes', 'ms', 'timespanMs', 'numeric', 'text', 'code', 'link', '' ); $audits = array(); if ( isset( $response['lighthouseResult'] ) && isset( $response['lighthouseResult']['audits'] ) && is_array( $response['lighthouseResult']['audits'] ) ) { foreach ( $response['lighthouseResult']['audits'] as $key => $audit ) { if ( ! in_array( $key, $selected_audits_id, true ) ) { continue; } $audits[ $key ]['id'] = $audit['id'] ?? null; $audits[ $key ]['title'] = $audit['title'] ?? null; $audits[ $key ]['description'] = $convert_description( $audit['description'] ?? null ); $audits[ $key ]['score'] = $audit['score'] ?? null; $audits[ $key ]['scoreDisplayMode'] = $audit['scoreDisplayMode'] ?? null; $audits[ $key ]['displayValue'] = $audit['displayValue'] ?? null; // Collect renderable details. $details_raw = $audit['details'] ?? array(); $detail_type = $details_raw['type'] ?? ''; if ( in_array( $detail_type, $renderable_detail_types, true ) ) { $details = array( 'type' => $detail_type ); // headings — only keep label/key/valueType. if ( ! empty( $details_raw['headings'] ) ) { $details['headings'] = array_values( array_map( function ( $h ) { return array( 'key' => $h['key'] ?? '', 'label' => $h['label'] ?? ( $h['text'] ?? '' ), 'valueType' => $h['valueType'] ?? 'text', ); }, array_filter( $details_raw['headings'], function ( $h ) { return ! empty( $h['key'] ); } ) ) ); } // items — limit to 20, only keep scalar-typed heading keys. if ( ! empty( $details_raw['items'] ) ) { $allowed_keys = array(); if ( ! empty( $details['headings'] ) ) { foreach ( $details['headings'] as $h ) { $allowed_keys[] = $h['key']; } } $items = array(); foreach ( array_slice( $details_raw['items'], 0, 20 ) as $item ) { $row = array(); foreach ( $item as $ikey => $ivalue ) { if ( $allowed_keys && ! in_array( $ikey, $allowed_keys, true ) ) { continue; } // Accept strings, numbers, booleans; unwrap typed objects. if ( is_scalar( $ivalue ) ) { $row[ $ikey ] = $ivalue; } elseif ( is_array( $ivalue ) ) { $vtype = $ivalue['type'] ?? ''; if ( 'url' === $vtype ) { $row[ $ikey ] = $ivalue['url'] ?? ''; } elseif ( 'source-location' === $vtype ) { $row[ $ikey ] = ( $ivalue['url'] ?? '' ) . ( isset( $ivalue['line'] ) ? ':' . $ivalue['line'] : '' ); } elseif ( 'code' === $vtype ) { $row[ $ikey ] = $ivalue['value'] ?? ''; } elseif ( 'link' === $vtype ) { $row[ $ikey ] = $ivalue['text'] ?? ''; } elseif ( isset( $ivalue['value'] ) && is_scalar( $ivalue['value'] ) ) { $row[ $ikey ] = $ivalue['value']; } } } if ( ! empty( $row ) ) { $items[] = $row; } } $details['items'] = $items; $details['itemsTotal'] = count( $details_raw['items'] ); } $audits[ $key ]['details'] = $details; } } } // Build per-category audit lists for accessibility, best-practices, seo. $category_audits = array(); foreach ( $extra_category_audit_ids as $cat_key => $ids ) { foreach ( $ids as $id ) { if ( isset( $audits[ $id ] ) ) { $category_audits[ $cat_key ][ $id ] = $audits[ $id ]; } } } $data['lighthouseResult']['categoryAudits'] = $category_audits; // Screenshots: final-screenshot, full-page-screenshot, screenshot-thumbnails. $screenshot_keys = array( 'final-screenshot', 'full-page-screenshot', 'screenshot-thumbnails' ); $screenshots = array(); foreach ( $screenshot_keys as $sk ) { if ( isset( $response['lighthouseResult']['audits'][ $sk ] ) ) { $raw = $response['lighthouseResult']['audits'][ $sk ]; $screenshots[ $sk ] = array( 'id' => $raw['id'] ?? $sk, 'title' => $raw['title'] ?? '', ); // final-screenshot: details.data contains the base64 image. if ( isset( $raw['details']['data'] ) ) { $screenshots[ $sk ]['data'] = $raw['details']['data']; } // full-page-screenshot: details.screenshot.data. if ( isset( $raw['details']['screenshot']['data'] ) ) { $screenshots[ $sk ]['data'] = $raw['details']['screenshot']['data']; } // screenshot-thumbnails: details.items[].data. if ( isset( $raw['details']['items'] ) ) { $screenshots[ $sk ]['items'] = array_map( function ( $item ) { return array( 'data' => $item['data'] ?? '', 'timestamp' => $item['timestamp'] ?? null, 'timing' => $item['timing'] ?? null, ); }, $raw['details']['items'] ); } } } $data['lighthouseResult']['screenshots'] = $screenshots; $dignostics = array( 'diagnostics' => array( 'font-display' => array( 'type' => array( 'FCP', 'LCP', ), ), 'critical-request-chains' => array( 'type' => array( 'FCP', 'LCP', ), ), 'largest-contentful-paint-element' => array( 'type' => array( 'LCP', ), ), 'layout-shift-elements' => array( 'type' => array( 'CLS', ), ), 'long-tasks' => array( 'type' => array( 'TBT', ), ), 'render-blocking-resources' => array( 'type' => array( 'FCP', 'LCP', ), ), 'unused-css-rules' => array( 'type' => array( 'FCP', 'LCP', ), ), 'unminified-css' => array( 'type' => array( 'FCP', 'LCP', ), ), 'unminified-javascript' => array( 'type' => array( 'FCP', 'LCP', ), ), 'unused-javascript' => array( 'type' => array( 'LCP', ), ), 'uses-text-compression' => array( 'type' => array( 'FCP', 'LCP', ), ), 'uses-rel-preconnect' => array( 'type' => array( 'FCP', 'LCP', ), ), 'server-response-time' => array( 'type' => array( 'FCP', 'LCP', ), ), 'redirects' => array( 'type' => array( 'FCP', 'LCP', ), ), 'uses-rel-preload' => array( 'type' => array( 'FCP', 'LCP', ), ), 'efficient-animated-content' => array( 'type' => array( 'LCP', ), ), 'duplicated-javascript' => array( 'type' => array( 'TBT', ), ), 'legacy-javascript' => array( 'type' => array( 'TBT', ), ), 'total-byte-weight' => array( 'type' => array( 'LCP', ), ), 'dom-size' => array( 'type' => array( 'TBT', ), ), 'bootup-time' => array( 'type' => array( 'TBT', ), ), 'mainthread-work-breakdown' => array( 'type' => array( 'TBT', ), ), 'third-party-summary' => array( 'type' => array( 'TBT', ), ), 'third-party-facades' => array( 'type' => array( 'TBT', ), ), 'non-composited-animations' => array( 'type' => array( 'CLS', ), ), 'unsized-images' => array( 'type' => array( 'CLS', ), ), 'viewport' => array( 'type' => array( 'TBT', ), ), ), ); // Update audits with diagnostics type information if keys match. foreach ( $dignostics['diagnostics'] as $diag_key => $diag_value ) { if ( isset( $audits[ $diag_key ] ) ) { $audits[ $diag_key ]['type'] = $diag_value['type'] ?? array(); } } $data['lighthouseResult']['audits'] = $audits; if ( isset( $response['lighthouseResult'] ) && isset( $response['lighthouseResult']['configSettings'] ) ) { $data['lighthouseResult']['configSettings'] = $response['lighthouseResult']['configSettings']; } else { $data['lighthouseResult']['configSettings'] = array(); } return $data; } } }