<?php /** * Abstract Analyzer class. * * Base class for performing SEO checks for WordPress entities with consistent output for UI. * * @package SureRank\Inc\Analyzer */ namespace SureRank\Inc\Analyzer; use DOMDocument; use DOMXPath; use SureRank\Inc\Functions\Get; /** * Abstract Analyzer class. */ class Utils { /** * Get rendered XPath. * * @param string $rendered_content Rendered content. * @return DOMXPath|null */ public static function get_rendered_xpath( $rendered_content ) { if ( empty( $rendered_content ) ) { return null; } $dom = new DOMDocument(); libxml_use_internal_errors( true ); $encoded_content = mb_encode_numericentity( htmlspecialchars_decode( htmlentities( $rendered_content, ENT_NOQUOTES, 'UTF-8', false ), ENT_NOQUOTES ), [ 0x80, 0x10FFFF, 0, ~0 ], /** * Conversion map for mb_encode_numericentity: * 0x80 (128) is the first non-ASCII Unicode code point. * 0x10FFFF (1,114,111) is the highest valid Unicode code point. * 0 is the bitmask for the first byte (no filtering). * ~0 is the bitmask to include all characters in the range. */ 'UTF-8' ); if ( empty( $encoded_content ) ) { return null; } $dom->loadHTML( $encoded_content ); libxml_clear_errors(); return new DOMXPath( $dom ); } /** * Check for search engine title. * * @param string|null $title Title. * @return array<string, mixed> */ public static function analyze_title( $title ) { if ( $title === null ) { return [ 'status' => 'error', 'message' => __( 'Search engine title is missing on the page.', 'surerank' ), ]; } $length = mb_strlen( $title ); $exists = ! empty( $title ); $is_optimized = $exists && $length <= Get::TITLE_LENGTH; // translators: %s is the search engine title length. $working_message = sprintf( __( 'Search engine title is present and under %s characters.', 'surerank' ), Get::TITLE_LENGTH ); // translators: %s is the search engine title length. $exceeding_message = sprintf( __( 'Search engine title exceeds %s characters.', 'surerank' ), Get::TITLE_LENGTH ); // translators: %s is the search engine title length. $missing_message = __( 'Search engine title is missing on the page.', 'surerank' ); $message = $exists ? ( $length <= Get::TITLE_LENGTH ? $working_message : $exceeding_message ) : $missing_message; $description = $exists && ! $is_optimized ? [ // translators: %s is the search engine title. sprintf( __( 'The search engine title for the page is: "%s"', 'surerank' ), $title ), ] : []; return [ 'status' => $exists ? ( $is_optimized ? 'success' : 'warning' ) : 'error', 'message' => $message, ]; } /** * Check for search engine description. * * @param string|null $description Description. * @return array<string, mixed> */ public static function analyze_description( $description ) { if ( $description === null ) { return [ 'status' => 'warning', 'message' => __( 'Search engine description is missing on the page.', 'surerank' ), ]; } $length = mb_strlen( $description ); $exists = ! empty( $description ); $is_optimized = $exists && $length >= Get::DESCRIPTION_MIN_LENGTH && $length <= Get::DESCRIPTION_LENGTH; // translators: %s is the search engine description length. $working_message = sprintf( __( 'Search engine description is present and under %s characters.', 'surerank' ), Get::DESCRIPTION_LENGTH ); // translators: %s is the search engine description length. $exceeding_message = sprintf( __( 'Search engine description exceeds %s characters.', 'surerank' ), Get::DESCRIPTION_LENGTH ); // translators: %s is the search engine description length. $missing_message = __( 'Search engine description is missing on the page.', 'surerank' ); $message = $exists ? ( $length <= Get::DESCRIPTION_LENGTH ? $working_message : $exceeding_message ) : $missing_message; /* translators: %s is the search engine description */ $description = $exists && ! $is_optimized ? [ sprintf( __( 'The search engine description for the page is: "%s"', 'surerank' ), $description ) ] : []; return [ 'status' => $exists && $length <= Get::DESCRIPTION_LENGTH ? 'success' : 'warning', 'message' => $message, ]; } /** * Check for canonical URL. * * @param string|null $canonical Canonical URL. * @param string|null $permalink Permalink URL. * @return array<string, mixed> */ public static function analyze_canonical_url( $canonical, $permalink ) { if ( $canonical === null && $permalink === null ) { return [ 'status' => 'warning', 'message' => __( 'Canonical tag is not present on the page.', 'surerank' ), ]; } return [ 'status' => 'success', 'message' => __( 'Canonical tag is present on the page.', 'surerank' ), ]; } /** * Check for URL length. * * @param string|null $url URL. * @return array<string, mixed> */ public static function check_url_length( $url ) { if ( $url === null ) { return [ 'status' => 'warning', 'message' => __( 'No URL provided.', 'surerank' ), ]; } $length = mb_strlen( $url ); $exists = ! empty( $url ); $is_optimized = $exists && $length <= Get::URL_LENGTH; $working_message = __( 'Page URL is short and SEO-friendly.', 'surerank' ); /* translators: %s is the URL length. */ $exceeding_message = sprintf( __( 'Page URL is longer than %s characters and may affect SEO and readability.', 'surerank' ), Get::URL_LENGTH ); $missing_message = __( 'No URL provided.', 'surerank' ); $message = $exists ? ( $is_optimized ? $working_message : $exceeding_message ) : $missing_message; return [ 'status' => $exists ? ( $is_optimized ? 'success' : 'warning' ) : 'warning', 'message' => $message, ]; } /** * Get meta data. * * @param array<string, mixed> $meta Meta data. * @return array<string, mixed> */ public static function get_meta_data( array $meta ) { $meta_data = $meta['data'] ?? []; $global_data = $meta['global_default'] ?? []; if ( empty( $meta_data['page_title'] ) ) { $meta_data['page_title'] = $global_data['page_title'] ?? ''; $meta_data['page_title'] = str_replace( '%title%', '%term_title%', $meta_data['page_title'] ); } if ( empty( $meta_data['page_description'] ) ) { $meta_data['page_description'] = $global_data['page_description'] ?? ''; $meta_data['page_description'] = str_replace( '%excerpt%', '%term_description%', $meta_data['page_description'] ); } return $meta_data; } /** * Get existing broken links. * * @param array<string, mixed> $broken_links Broken links. * @param array<string> $urls URLs. * @return array<string, array<string, mixed>> */ public static function existing_broken_links( $broken_links, $urls ) { $description = $broken_links['description'] ?? []; $existing_broken_links = []; foreach ( $description as $item ) { if ( is_array( $item ) && isset( $item['list'] ) ) { $existing_broken_links = $item['list']; break; } } $filtered_broken_links = []; if ( is_array( $existing_broken_links ) ) { foreach ( $existing_broken_links as $key => $existing_link ) { if ( is_string( $existing_link ) ) { if ( in_array( $existing_link, $urls, true ) ) { $filtered_broken_links[ $key ] = [ 'url' => $existing_link, 'status' => 'error', 'details' => __( 'The link is broken.', 'surerank' ), ]; } } elseif ( is_array( $existing_link ) && isset( $existing_link['url'] ) ) { if ( in_array( $existing_link['url'], $urls, true ) ) { $filtered_broken_links[ $key ] = $existing_link; } } } } return $filtered_broken_links; } /** * Check for open graph tags. * * @return array<string, mixed> */ public static function open_graph_tags() { if ( apply_filters( 'surerank_disable_open_graph_tags', false ) ) { return [ 'status' => 'suggestion', 'message' => __( 'Open Graph tags are not present on the page.', 'surerank' ), ]; } return [ 'status' => 'success', 'message' => __( 'Open Graph tags are present on the page.', 'surerank' ), ]; } /** * Check if keyword exists in text (case-insensitive). * * @param string $text Text to search in. * @param string $keyword Keyword to search for. * @return bool */ private static function keyword_exists_in_text( $text, $keyword ) { if ( empty( $text ) || empty( $keyword ) ) { return false; } return stripos( $text, $keyword ) !== false; } /** * Analyze focus keyword in SEO title. * * @param string|null $title SEO title. * @param string $keyword Focus keyword. * @return array<string, mixed> */ public static function analyze_keyword_in_title( $title, $keyword ) { if ( empty( $keyword ) ) { return [ 'status' => 'suggestion', 'message' => __( 'No focus keyword set to analyze title.', 'surerank' ), ]; } if ( empty( $title ) ) { return [ 'status' => 'warning', 'message' => __( 'No SEO title found to analyze.', 'surerank' ), ]; } if ( self::keyword_exists_in_text( $title, $keyword ) ) { return [ 'status' => 'success', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" found in SEO title.', 'surerank' ), $keyword ), ]; } return [ 'status' => 'warning', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" not found in SEO title.', 'surerank' ), $keyword ), ]; } /** * Analyze focus keyword in meta description. * * @param string|null $description Meta description. * @param string $keyword Focus keyword. * @return array<string, mixed> */ public static function analyze_keyword_in_description( $description, $keyword ) { if ( empty( $keyword ) ) { return [ 'status' => 'suggestion', 'message' => __( 'No focus keyword set to analyze meta description.', 'surerank' ), ]; } if ( empty( $description ) ) { return [ 'status' => 'warning', 'message' => __( 'No meta description found to analyze.', 'surerank' ), ]; } if ( self::keyword_exists_in_text( $description, $keyword ) ) { return [ 'status' => 'success', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" found in meta description.', 'surerank' ), $keyword ), ]; } return [ 'status' => 'warning', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" not found in meta description.', 'surerank' ), $keyword ), ]; } /** * Analyze focus keyword in URL. * * @param string $url Page URL. * @param string $keyword Focus keyword. * @return array<string, mixed> */ public static function analyze_keyword_in_url( $url, $keyword ) { if ( empty( $keyword ) ) { return [ 'status' => 'suggestion', 'message' => __( 'No focus keyword set to analyze URL.', 'surerank' ), ]; } if ( empty( $url ) ) { return [ 'status' => 'warning', 'message' => __( 'No URL found to analyze.', 'surerank' ), ]; } // Convert keyword to URL-friendly format (lowercase, spaces to hyphens). $url_friendly_keyword = strtolower( str_replace( ' ', '-', $keyword ) ); $url_lower = strtolower( $url ); if ( strpos( $url_lower, $url_friendly_keyword ) !== false || self::keyword_exists_in_text( $url, $keyword ) ) { return [ 'status' => 'success', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" found in URL.', 'surerank' ), $keyword ), ]; } return [ 'status' => 'warning', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" not found in URL.', 'surerank' ), $keyword ), ]; } /** * Analyze focus keyword in content. * * @param string $content Page content. * @param string $keyword Focus keyword. * @return array<string, mixed> */ public static function analyze_keyword_in_content( $content, $keyword ) { if ( empty( $keyword ) ) { return [ 'status' => 'suggestion', 'message' => __( 'No focus keyword set to analyze content.', 'surerank' ), ]; } if ( empty( $content ) ) { return [ 'status' => 'warning', 'message' => __( 'No content found to analyze.', 'surerank' ), ]; } // Clean content of HTML tags for better analysis. $clean_content = wp_strip_all_tags( $content ); $clean_content = preg_replace( '/\s+/', ' ', $clean_content ); $clean_content = trim( (string) $clean_content ); if ( self::keyword_exists_in_text( $clean_content, $keyword ) ) { return [ 'status' => 'success', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" found in content.', 'surerank' ), $keyword ), ]; } return [ 'status' => 'warning', // translators: %s is the focus keyword. 'message' => sprintf( __( 'Focus keyword "%s" not found in content.', 'surerank' ), $keyword ), ]; } }